Introduction
When working in a large codebase with a significant number of async events, I often found myself in situations where I couldn’t combine events effectively. This resulted in optimization problems and inefficient consumption of OS resources.
The codebase contained closures
and async/await
, so it wasn’t possible to use operators like merge
or combineLatest
. After discovering this limitation, I decided to add new methods using Combine
.
I will be demonstrating this with a simple NetworkService
responsible only for executing and validating requests using Combine. Let’s dive into the implementation.
First Step
The first step is to create the NetworkService
with request(_ endpoint: Endpoint)
method.
func request<T: Decodable>(_ endpoint: Endpoint) -> AnyPublisher<T, Error> {}
Quick Explanation of What AnyPublisher Is
The AnyPublisher returns a publisher from a method without exposing the specific type of publisher you are using internally. It helps hide implementation details.
Second Step
The second step is to add a few helpers such as Endpoint
and NetworkError
.
struct Endpoint {
let url: String
let headers: [String: String]?
let body: Data?
let httpMethod: HTTPMethod
func urlRequest(with url: URL) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = httpMethod.rawValue
urlRequest.allHTTPHeaderFields = headers ?? [:]
urlRequest.httpBody = body
return urlRequest
}
enum HTTPMethod: String {
case GET
case POST
case PUT
case DELETE
}
}
enum NetworkError: Error {
case invalidURL
case invalidResponseType
case jsonDecoderError(_ error: Error)
}
Third Step
The third step is to implement the request
method.
func request<T: Decodable>(_ endpoint: Endpoint) -> AnyPublisher<T, Error> {
guard let url = URL(string: endpoint.url) else {
return Fail<T, Error>(error: NetworkError.invalidURL).eraseToAnyPublisher()
}
return URLSession.shared
.dataTaskPublisher(for: endpoint.urlRequest(with: url))
.tryMap { output in
guard output.response is HTTPURLResponse else {
throw NetworkError.invalidResponseType
}
return output.data
}
.decode(type: T.self, decoder: JSONDecoder())
.mapError { error in
NetworkError.jsonDecoderError(error)
}
.eraseToAnyPublisher()
}
Fourth Step
The fourth step is to test if NetworkService
works as expected.
import Combine
import Inject
import SwiftUI
public struct ContentView: View {
@ObserveInjection var inject
private let networkService: NetworkService = .init()
@State private var cancellables = Set<AnyCancellable>()
@State private var post: Post?
public init() {}
public var body: some View {
VStack {
Button("Fetch Data") {
networkService
.request(Endpoint(url: "https://jsonplaceholder.typicode.com/posts/1", httpMethod: .GET))
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case let .failure(error):
print(error)
}
}, receiveValue: { (post: Post) in
self.post = post
})
.store(in: &cancellables)
}
Text(post?.title ?? "")
}
.enableInjection()
}
}
Summary
Combine is best suited for handling multiple async events by using event-processing operators. Before integrating it into your codebase, make sure to weigh all the pros and cons.