2 min read

Networking

Last Updated - Platform 25.0 - SDK 20.0

The 'network layer' or networking within the PoqSDK was rewritten in v23.0 using Swift Concurrency and SwiftUI style API.

Session

The NetworkSession is responsible for executing network requests. Create and execute network tasks to execute network requests using a network session.

With Swift's evolution URLSession has grown and branched a few ways to execute network requests. Under the hood, the poq NetworkSession uses async / await but the SDK provides all other means of execution via asynchronous tasks.

The main session can be accessed via Container.shared.networkSession() which defaults to PoqNetworkSession, or MockSession.shared when running tests.

PoqNetworkSession

The PoqNetworkSession adds common headers, signs (if enabled) and authorises all requests before execution. By default it is initialised using the Container.shared.authenticator() which can be optional for no authentication.

MockSession

The MockSession subclasses the PoqNetworkSession but uses URLProtocol to replace requests using the mocks dictionary.

Requests are replaced with the MockResponder value from the first match where their path contains a key from the mocks dictionary. Requests with no matching mock return a 'file not found' error.

Mock requests can be set using the subscript and a mock json file added to the bundle. Use the response function to create a MockResponder and assign it to a path.

// Match only `/category/some-title`.
MockSession.shared["category/some-title"] = .response(from: .init(for: Self.self), json: "Category")
// Match all categories.
MockSession.shared["category"] = .response(from: .init(for: Self.self), json: "Category")

Authenticator

The Authenticator has the following two important async functions:

  • authorize(request:) is called by the session to authorise requests.
  • authenticate() is called by the session on 401 responses.

When using the PoqAccount SDK the authenticator defaults to the PoqAccountAuthenticator and uses the Account API for authorization and authentication. Otherwise, it defaults to the PoqAuthenticator that performs no authentication.

Requests

The NetworkRequest API allows easy and chainable creation of HTTPURLRequests using SwiftUI style syntax.

Most service implementations will create a network request then convert it to a network task. Network requests default on initialisation to the current environment api host.

// Creates the update cart request.
NetworkRequest(method: .POST)
.path("/cart")
.queryItem(name: "flag", value: true)
.body(cart)

Response Decoding

The NetworkResponseDecoder decodes the response from the session into the expected data type or a useful error from the response body. The network session decides if a response is a failure based on status but the decoder can also override this.

This can be replaced for specific requests when converting them to tasks by providing a decoder. Or, globally, using PoqNetworkSession(decoder:) and injecting into the shared container.

The default network decoder treats failures as PoqMessage responses and pulls the best error message from them.

Tasks

Requests can be converted to asynchronous network tasks using .task() and optionally providing a session. Tasks are constrained to the expected response data type be which can specified using expecting: or inferred from the surrounding context.

// Creates a task wrapping the shop request against the main session.
func shop() -> NetworkTask<CategoryTreeData> {
NetworkRequest()
.path("/shop")
.task()
}

Network tasks can also be converted from requests expecting one type but mapping to a different type.

// Creates a task wrapping the shop request and converting it to just name against the main session.
func shopMainCategory() -> NetworkTask<CategoryData> {
NetworkRequest()
.path("/shop")
.task(expecting: CategoryTreeData.self) { $0.category ?? { throw NetworkError.unknown }() }
}

Execution

Tasks do not execute immediately. Instead you execute them later using your preferred method (async / await, combine or completion handlers).

The task() function returns a NetworkTask; a special task type that returns the full network response returned by the decoder and session. This can be consumed by a repository to gain access to the exact statusCode and the response of the expected data type.

let response = await shop().execute()
if response.statusCode == 201 { print("Updated?") }

Network tasks provide convenience functions for directly executing and accessing the response data value using value().

// Async / Await
let shop = await shop().value()
// Closure
shop().value { result in }
// Combine
shop().valueProvider().sink { _ in } receiveValue: { shop in }

Type Overriding

Network tasks are also special in that the response type can be overridden outside of the service to reduce code. This is useful when your API does not match the contract and cannot be fulfilled using customData.

// Decode the raw response data to a different type using the session as the default response decoder.
let shop = await shop().decode(expecting: CustomShopCategoryData.self).value()
// Or pass a custom response decoder.
let shop = await shop().decode(expecting: CustomShopCategoryData.self, decoder: CustomResponseDecoder()).value()