28
Jan
2016
 

A Modern Network Operation

by Marcus Zarra

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.

Signoff