A Modern Network Operation
When Apple introduced the NSURLSession
API, the landscape of network operations changed. In some ways, the new API made things worse as the block API that was added tempts developers to become very sloppy with network calls. The block API makes it trivial to add network calls anywhere in an application. Very little thought or structure is required. From this (and some third party libraries who followed the same pattern) we have seen an explosion of increasingly poor network handling in applications.
For my own purposes I have resisted this slippery slope as often as possible. However I still preferred the way NSURLConnection
worked and how it integrated nicely with NSOperation
subclasses. My attempts at using the NSURLSession
were cumbersome and didn’t feel “right”.
Fortunately, I have recently worked on a design that I have been quite pleased with. In this design I am happily using NSOperation
subclasses again and I am using the NSURLSession
API.
Changes to NSOperaton
implementation
One of the reasons that it took me so long to discover this pattern was due to forgetting about a way to implement NSOperation
subclasses and taking control of the life-cycle of the NSOperation
. This was not a feature I used very often and therefore did not think about using it with the NSURLSession
API.
After various experiments and research I re-discovered this option and the following update to my network operation design was born.
The most complicated change (and probably a change that someone will suggest a better solution to) has to do with the finished
property on the NSOperation
. This property is defined as readonly
which is a bit more enforced in Swift than it was in Objective-C. In Objective-C it was a simple matter of creating a setter accessor and manipulating the underlying ivar
that the property generates. However, in Swift, I have not found a way to do that exactly the same. Therefore I ended up overriding the property completely:
var junk: Bool = false
override var finished: Bool {
get {
return junk
}
set (newAnswer) {
willChangeValueForKey("isFinished")
junk = newAnswer
didChangeValueForKey("isFinished")
}
}
Note that I am triggering a KVO notification on isFinished
as opposed to finished
which is what the property name is. This seems to be a Swift-ism as the NSOperation
get accessor is called isFinished
instead of getFinished
and I suspect that is part of why I need to tickle the accessor name instead of the property name.
Since I am building a base class to extend my actual network operations from, there are a couple of other variables that I added:
let incomingData = NSMutableData()
var sessionTask: NSURLSessionTask?
var localURLSession: NSURLSession {
return NSURLSession(configuration: localConfig, delegate: self, delegateQueue: nil)
}
var localConfig: NSURLSessionConfiguration {
return NSURLSessionConfiguration.defaultSessionConfiguration()
}
The incomingData
property will hold the data coming back from the network operation. This property can be used in a few ways.
The sessionTask
will hold a reference to the task that will be created so that we have access to it even when it is not responding back to its delegate. Speaking of delegates, this subclass of NSOperation
also implements the NSURLSessionDataDelegate
as is hinted at in the localURLSession
property.
Both the localURLSession
property and the localConfig
property can be overridden by a subclass. This is intentional as using a non-default configuration or session is usual for me but I want to leave the option open.
start()
With these variables made available, the start function is fairly straight-forward.
override func start() {
if cancelled {
finished = true
return
}
guard let url = NSURL(string: “aURL”) else { fatalError("Failed to build URL") }
let request = NSMutableURLRequest(URL: url)
sessionTask = localURLSession.dataTaskWithRequest(request)
sessionTask!.resume()
}
Because I am now completely in control of the life-cycle of this NSOperation
it is prudent to check isCancelled
immediately upon entry into this function. It is entirely possible that the operation has been flagged as cancelled before it began. Therefore I will immediately set the finished
flag to true
if the operation has been cancelled.
I could have built the url
property without the guard and just unwrapped it but I prefer the guard syntax for that so that when I do have a problem I get a human readable error message. Personal preference.
The start()
function merely kicks off the task and completes.
Because we have overridden the start()
function and specifically not called super.start()
the NSOperation
will continue to reside in the queue, “running”, as far as the NSOperationQueue
is concerned until I set the finished
property to true
.
From here I needed to implement a minimum of three functions to complete the delegate implementation.
didReceiveResponse:
func URLSession(session: NSURLSession,
dataTask: NSURLSessionDataTask,
didReceiveResponse response: NSURLResponse,
completionHandler: (NSURLSessionResponseDisposition) -> Void) {
if cancelled {
finished = true
sessionTask?.cancel()
return
}
//Check the response code and react appropriately
completionHandler(.Allow)
}
This is another opportunity to watch for the cancellation of the operation and to abort if the flag has been set to true
. If it hasn’t then the next step (left as only a comment for brevity) is to check the status of the response and make a decision how to proceed.
The normal procedure here is to execute the completionHandler
and pass it the value of .Allow
to tell the NSURLSession
to continue with the task.
didReceiveData:
func URLSession(session: NSURLSession,
dataTask: NSURLSessionDataTask,
didReceiveData data: NSData) {
if cancelled {
finished = true
sessionTask?.cancel()
return
}
incomingData.appendData(data)
}
This delegate method is expected to be called numerous times during the life-cycle of this NSOperation
. Each time it is called it adds a bit more data to what has been received. Once we have confirmed that the operation has not been cancelled, the next step is to add the packet that was received to the incomingData
property.
didCompleteWithError:
func URLSession(session: NSURLSession,
task: NSURLSessionTask,
didCompleteWithError error: NSError?) {
if cancelled {
finished = true
sessionTask?.cancel()
return
}
if NSThread.isMainThread() { log("Main Thread!") }
if error != nil {
log("Failed to receive response: \(error)")
finished = true
return
}
processData()
finished = true
}
This is the final of the delegate methods we need to implement. There are others that we can implement if we need to deal with authentication or redirects.
In this function, after confirming that it is not cancelled, we need to check to see if there was an error. If there is an error we effectively abort the operation, throw away the data and change the finished
flag. Depending on your application and your expectations, you may want to fire a NSNotification
or handle some other kind of behavior to notify the application that the network operation failed.
When the operation doesn’t fail I then call processData()
which does nothing in the base class. The expectation is that the subclass of this base class will implement the processData()
function and handle its specific processing of that data.
Wrap Up
With this implementation I am able to build network operations that are small discrete units of work that I can easily test with a unit test. I can expand upon this by passing in a NSManagedObjectContext
and have my processData()
function create a private child of that passed in context and consume the data received from the server.
I can also attach a completion block to the NSOperation and get bandwidth information from the operation if I want and make adjustments to the assumptions of the application based on the current bandwidth.
By using NSOperation
subclasses I can now prioritize my network calls (posting to twitter is a higher priority than receiving avatars from twitter for example), cancel operations that are no longer needed (avatars or images that are no longer on screen) and in general be a much better citizen by only keeping the radios on for as long as I need them and not any longer.
Further, I can now watch for application life-cycle events in my NetworkController
and terminate operations when my application goes into the background, further improving my citizenship status. I can also look for operations that need to finish when the application goes into the background and request additional background time.
I hope that you find this article helpful and if you have any questions about this design, please feel free to contact me at marcus@cimgf.com
.