URLSession and Operation, sitting in a tree
I don’t choose lightly to go against Apple recommendations on how to use their frameworks thus I spent quite a bit of time pondering the conundrum explained in the last post.
Let me try to paint a picture of the problem…
On one hand I have this chain of values where ||
indicates that URLSessionDataTask
is private property populated with value created inside Operation
.
APIwrapper
⤷ URLSession
⤷ OperationQueue
⤷ Operation
⤷ NetworkPayload
⤷ URLSession, |URLSessionDataTask|
On the other hand, I have URLSessionDelegate
working like this:
APIwrapper ↰
⇣ ⤷ URLSession ⇡
⇣ ⤷ URLSessionDelegate ⤴︎
⇣ ⇣
⤷ func urlSession(..., dataTask: URLSessionDataTask...)
This one instance of URLSession is kept on API client wrapper, it’s passed into each Operation but all the data I get through it are received in a completely different chain of objects.
The only unique bridge between the delegate callbacks chain and the Operation is the URLSessionDataTask instance. It’s the very same instance in both chains. Thus I need to use it as bridge: take the data received in the URLSessionDelegate callbacks and attach them to the Task instance there; later read them inside the Operation.
This is what I end up with:
- Extend URLSessionTask with a bunch of closure properties
- Closures are set inside the Operation, so they have access to its scope and thus to NetworkPayload
- Those closures will receive input from URLSessionDelegate callbacks, passing in the received data
Again and again, I am very, very grateful to whoever added associated objects to Objective-C runtime, since it allows me to overcome Swift’s (current) inability to declare stored properties in type extension:
extension URLSessionTask {
public typealias NetworkTaskErrorCallback = (NetworkError) -> Void
...
private struct AssociatedKeys {
static var error = "Error"
...
}
public var errorCallback: NetworkTaskErrorCallback {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.error) as? NetworkTaskErrorCallback ?? {_ in}
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.error, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
...
}
Thus inside NetworkOperation I will declare the closure(s):
func setupCallbacks() {
guard let task = task else { return }
task.errorCallback = {
[weak self] error in
self?.payload.error = error
self?.finish()
}
...
}
and execute them from the appropriate URLSessionDelegate callback:
final func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Swift.Error?) {
guard
let dataTask = task as? URLSessionDataTask
else { return }
if let e = error {
dataTask.errorCallback( .urlError(e as? URLError) )
return
}
dataTask.finishCallback()
}
I now have a fully working solution that allows me to use Operations for network requests, while still adhering to Apple’s recommendations of (generally) using one URLSession per app.