I started to work with iOS in 2008, back in the days there was no GCD, no RxSwift or similar solutions. NSURLSession was a dream, and the only thing available was NSURLConnection. Even blocks were not available at the time.

Starting with this article, I will discuss some of the old ways of working in Cocoa and how to apply them in Swift. Today I will talk about NSOperation (or Operation in Swift), probably the most important class for concurrency back in the days, which seems to be forgotten these days.

Operation

What is NSOperation and why it was so important in the early days?

When iOS was released, there were a couple of ways to do asynchronous computation (excluding the low-level C ways): NSThread and NSOperation. So NSOperation was a daily class to deal with, if you did not want to deal with threads directly, and before NSURLSession it was necessary to have background network calls without headaches.

NSOperation is an abstract class, as described by the official documentation:

An abstract class that represents the code and data associated with a single task.

This class requires to have the task coded in the main method and offers a start method to setup the process and do the necessary computation upfront, remember: blocks were not available back in the days. A great advantage of NSOperation is the fact that has also a queue associated, named NSOperationQueue. This class is responsible for ensuring that operations are performed in the right order and without exceeding the maximum number of concurrent operations. Back in the days, network connections were a way slower than today and perform 4-5-6 requests at the same time was not a great idea and were subject to timeouts (for example when requesting images). The only viable way to avoid this was to make sure requests were limited in number.

Another great thing is the fact that a single operation in the queue can have a lower or higher priority and this was a great plus when dealing with important requests like retrieving data and less important like sending analytics data or crash reports back.

Operation Today

I am still habit to use NSOperation in my apps, especially in the ones were external dependencies are limited or completely forbidden. One way to use Operation in Swift, is to make sure requests are limited and performed in no-max of a certain number at the same time.

Let's see how this works...

I want to create a remote logging API, which logs things directly in the server (no local storage), so a logging text is sent immediately to the server for testing or debugging purposes.

If the number of logs is high and the number of users is high as well, the risk to harm the server is high, so limiting the requests from a single device is a good way to balance the load.

Creating a LogOperation is pretty easy:

class LogOperation: Operation {
    var text: String
    init(text: String) {
        self.text = text
    }
    
    static let url:URL = {
        return URL(string: "http://example.com/logs")!
    }()
}

The variable text is our logging text and a POST request sends it inside a json string containing just the text. The request points to the configured url.

After the definition of the general information, an instance of URLSession and a dedicate OperationQueue are more than welcomed.

static let queue: OperationQueue = {
    var queue = OperationQueue()
    queue.maxConcurrentOperationCount = 2 // reduces the load
    queue.isSuspended = false // ensure the queue is active
    return queue
}()

static let session: URLSession = {
    var config = URLSessionConfiguration.default 
    var s = URLSession(configuration: config)
    return s
}()

Note: I am not focusing on making the class extensible or scalable, in a production code I would split this code in multiple classes and make them easier to combine.

After this, it's time to make the operation more flexible in Swift using isFinished and isExecuting. These two properties are essential to the life-cycle of the operation and are used by the system to understand when an operation completes and when is running. So to access these properties, we need to a trick in Swift:

fileprivate var _finished: Bool = false
override var isFinished: Bool {
    get {
        return _finished
    }
    set {
        willChangeValue(forKey: "isFinished")
        _finished = newValue
        didChangeValue(forKey: "isFinished")
    }
}

fileprivate var _executing: Bool = false
override var isExecuting: Bool{
    get {
        return _executing
    }
    set {
        willChangeValue(forKey: "isExecuting")
        _executing = newValue
        didChangeValue(forKey: "isExecuting")
    }
}

Working with isFinished is essential, as stated in the documentation:

The isFinished key path lets clients know that an operation finished its task successfully or was cancelled and is exiting. An operation object does not clear a dependency until the value at the isFinished key path changes to true. Similarly, an operation queue does not dequeue an operation until the isFinished property contains the value true. Thus, marking operations as finished is critical to keeping queues from backing up with in-progress or cancelled operations.

Same for isExecuting:

The isExecuting key path lets clients know whether the operation is actively working on its assigned task. The isExecuting property must report the value true if the operation is working on its task or false if it is not.

Both properties have to be managed if the operation override the start method:

If you replace the start() method or your operation object, you must also replace the isFinished/isExecuting property and generate KVO notifications when the operation finishes executing or is cancelled.

Why replacing/overriding the start method instead of the main method in this case? The main method is performed in a background thread and would wait until we reach the end of the method before completing the operation. Using this method with URLSession would then require a usage of a semaphore to wait for its completion, things that can be avoided using start.

There's also the isAsynchronous that indicates an operation is performed asynchronous, in this case it's not necessary to override this property becasue the example uses a queue:

When you add an operation to an operation queue, the queue ignores the value of the isAsynchronous property and always calls the start() method from a separate thread. Therefore, if you always run operations by adding them to an operation queue, there is no reason to make them asynchronous.

Time to write the start function:

override func start() {
    // Setup
    guard isCancelled == false else { return } // skip if already cancelled
    isExecuting = true
    isFinished = false
    
    // Serialize data
    let dic = ["txt": text]
    let data = try? JSONSerialization.data(withJSONObject: dic, options: [])
    
    // Create the request
    var request = URLRequest(url: LogOperation.url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = data

    // Create and start the data task
    let task = LogOperation.session.dataTask(with: request) { (data, response, error) in
        self.isFinished = true
        self.isExecuting = false
    }

    task.resume()
}

The previous code does essentially 3 things:

  • Setup of the operation with early exit if cancelled
  • Creation of the request
  • Creation and start of the data task

In this way, we can have a simple helper method to log directly from the LogOperation class:

static func log(text: String, priority: Operation.QueuePriority = .normal) {
    let operation = LogOperation(text: text)
    operation.queuePriority = priority
    LogOperation.queue.addOperation(operation)
}

which can then be used in the following way:

LogOperation.log(text: "[MyViewController] viewDidLoad")
//...
LogOperation.log(text: "[MyViewController] viewDidDisappear")
//..
LogOperation.log(text: "[MyViewController] button X pressed")
//..
LogOperation.log(text: "\(crashReport)", priority: .veryHigh)
// etc...

With this approach, there's a control on which request should be performed before, using priorities.

Conclusion

Operation is a simple, but powerful class that seems to be forgotten, but it's still a great tool that is there when we need to create asynchronous code. I would not recommend to use the code I showed before as it is in a production app, but I use a similar concept and more advanced network strategy in most of the apps where I can't use third party libraries like RxSwift.

In the next article, I will extend the discussion about Operation and OperationQueue showing how to create dependency graphs and make an operation depending to others.