Building a network layer using Combine iOS, with a structure similar to Moya

Islam Moussa
4 min readMay 11, 2023

--

Here’s an example of how to build a network layer using Combine, with a structure similar to Moya.

When building iOS or macOS apps that communicate with a REST API, it’s often useful to have a generic API client that can handle requests to different endpoints and return the response as a generic type. This can help reduce code duplication and make it easier to add new endpoints to the app.

In this article, we’ll explore how to implement a generic API client in Swift using protocol-oriented programming and URLSession.

API Endpoint Protocol

The first step in building a generic API client is to define a protocol that represents an API endpoint. An API endpoint typically includes a base URL, a path, an HTTP method, and any additional headers or parameters required for the request. Here’s an example implementation:

protocol APIEndpoint {
var baseURL: URL { get }
var path: String { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var parameters: [String: Any]? { get }
}

extension APIEndpoint {
var headers: [String: String]? {
return nil
}

var parameters: [String: Any]? {
return nil
}
}


enum APIError: Error {
case invalidResponse
case invalidData
}

HTTP Method Enum

Next, we’ll define an enum that represents HTTP methods. This enum will be used by the APIEndpoint protocol to specify which HTTP method to use for a particular request. Here's an example implementation:

enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}

APIClient Protocol

The APIClient protocol represents a generic API client that can make requests using an APIEndpoint. Here's an example implementation:

protocol APIClient {
associatedtype EndpointType: APIEndpoint
func request<T: Decodable>(_ endpoint: EndpointType) -> AnyPublisher<T, Error>
}

The APIClient protocol includes an associated type EndpointType, which represents the APIEndpoint type that the client will use. The request method takes an EndpointType as an argument and returns a AnyPublisher<T, Error>.

URLSessionAPIClient Class

The URLSessionAPIClient class is an implementation of the APIClient protocol that uses Combine to make requests and handle responses. Here's an example implementation:

class URLSessionAPIClient<EndpointType: APIEndpoint>: APIClient {
func request<T: Decodable>(_ endpoint: EndpointType) -> AnyPublisher<T, Error> {
let url = endpoint.baseURL.appendingPathComponent(endpoint.path)
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue

// Set up any request headers or parameters here
endpoint.headers?.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }

return URLSession.shared.dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.invalidResponse
}
return data
}
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}

The URLSessionAPIClient class is generic over an EndpointType that conforms to the APIEndpoint protocol. The request method takes an EndpointType and returns a AnyPublisher<T, Error>. The method uses URLSession.shared.dataTaskPublisher(for:) to create a Publisher that emits a tuple of the response data and URL response, and then uses Combine operators to transform and decode the data into the specified generic type.

Example Usage

To use the generic API client, you’ll need to define an implementation of the APIEndpoint protocol that represents the endpoint you want to use. Here's an example implementation:

enum UserEndpoint: APIEndpoint {
case getUsers
case getUser(id: Int)
case updateUser(id: Int, name: String, email: String)

var baseURL: URL {
return URL(string: "https://example.com/api")! // Set your base URL here
}

var path: String {
switch self {
case .getUsers:
return "/users"
case .getUser(let id):
return "/users/\(id)"
case .updateUser(let id, _, _):
return "/users/\(id)"
}
}

var method: HTTPMethod {
switch self {
case .getUsers, .getUser:
return .get
case .updateUser:
return .put
}
}

var headers: [String: String]? {
switch self {
case .updateUser:
return ["Content-Type": "application/json"]
default:
return nil
}
}

var parameters: [String: Any]? {
switch self {
case .updateUser(_, let name, let email):
return ["name": name, "email": email]
default:
return nil
}
}
}

In this example, the UserEndpoint enum defines three endpoints - getUsers, getUser(id:), and updateUser(id:name:email:). The baseURL property specifies the base URL for the API, and the path property specifies the path for the endpoint. The method property specifies the HTTP method to use for the request, and the headers and parameters properties specify any additional headers or parameters required for the request.

To use the generic API client to make a request, you can create an instance of the URLSessionAPIClient with the desired EndpointType and call the request method with an instance of the desired APIEndpoint:

let apiClient = URLSessionAPIClient<UserEndpoint>()
apiClient.request(UserEndpoint.getUsers)
.sink(
receiveCompletion: { completion in
// Handle completion
},
receiveValue: { users in
// Handle users
}
)
.store(in: &cancellables)

In this example, we’re creating an instance of URLSessionAPIClient<UserEndpoint> and using it to make a request to the getUsers endpoint. The sink operator is used to handle the response. The store(in:) method is used to store the AnyCancellable returned by sink, which will be used to cancel the request if necessary.

Conclusion

Using Combine to build a generic API client in Swift can simplify network code and reduce duplication across an application. By defining protocols for API endpoints and clients, and using Combine to handle requests and responses, we can create a flexible and reusable networking layer.

--

--

Islam Moussa

Professional iOS Developer, cross-platform developer and backend developer from Egypt