iosdevswift

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:

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.