Reputation: 4502
I have 2 methods I need to call, the second method must be executed using the result of the first method and the second method also returns a value.
I have put together a simple playground that demonstrates a simple version of the flow
import UIKit
protocol TokenLoader {
func load(_ key: String, completion: @escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: @escaping (String?) -> Void) {
print("was called")
completion("some.access.token")
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
completion(.success((data, response)))
}
})
task.resume()
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
I need to use the token returned by AuthTokenLoader
as a header on the request sent by dispatch
method in my Networking
class.
Networking
also returns a task so this request can be cancelled.
As I cannot return from inside the completion block of the AuthTokenLoader
load completion, I unsure how to achieve this.
Upvotes: 0
Views: 535
Reputation: 119242
Turns out it was harder to do in Combine than I thought. Like most people, I'm still quite new to this. Would happily accept edits from people who know better :)
The general principle is that, instead of taking a completion block, your functions should return a Publisher
, that you can then choose to do things with, and chain together.
So your token loader can look like this:
protocol TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error>
}
Instead of taking a completion block, you now return a publisher which will send you a string on success, or an error if something goes wrong.
And your implementation, well I wasn't sure what you were planning on doing in there but here's an example of sorts:
class AuthTokenLoader: TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error> {
print("was called")
// Do async stuff to create your token, ending in a result
let result = Result<String, Error>.success("some.access.token")
return result.publisher.eraseToAnyPublisher()
}
}
Your client can look like this:
protocol Client {
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error>
}
Now this is the complicated bit. What you want to do is take the publisher from your token loader, and when it gives a result, make your URL request, and then make another publisher from that URL request. URLSession
can give you a publisher for a data task, and there is a flatMap
operator which is supposed to allow you to turn the results of one publisher into a new publisher, but you get stuck in the weeds of the type system so the code is uglier than it ought to be:
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error> {
return loader.load("someKey")
.flatMap {
token -> AnyPublisher<Data, Error> in
var finalRequest = request
finalRequest.setValue(token, forHTTPHeaderField: "x-token")
return URLSession.shared.dataTaskPublisher(for: finalRequest)
.map { $0.data }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
You'd use this code like so:
let request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
let sub = client.dispatch(request)
.sink(receiveCompletion: {
switch $0 {
case .finished: print("Done")
case .failure(let error): print("Error :\(error)")
}
}, receiveValue: { data in
print("Data: \(data)")
})
sub
is an AnyCancellable
, so if it is deallocated or you call cancel
on it, this passes back up the chain and it will cancel the URL task for you.
If you want to do things with the data then there are operators for mapping or decoding or whatever which makes the whole thing very nice to work with.
Upvotes: 1
Reputation: 2478
You can create a wrapper for your task and return that instead.
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
Your entire playground would become
protocol TokenLoader {
func load(_ key: String, completion: @escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: @escaping (String?) -> Void) {
print("was called")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion("some.access.token")
}
}
}
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task {
let task = URLSessionTaskWrapper(completion)
loader.load("token") { token in
task.wrapped = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
task.complete(with: .failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
task.complete(with: .success((data, response)))
}
})
task.wrapped?.resume()
}
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
Upvotes: 1