Advanced NSOperations

Episode #180 | 14 minutes | published on July 30, 2015
Subscribers Only
In this episode, Ben continues exploring NSOperation-based architecture, this time writing operations that present UI for a login screen, load the Core Data stack, and operations that are composed of multiple lower-level operations.

Correction: in the episode I forgot to make LoginOperation present on the main queue. Thanks to lorenzob for pointing this out! The source code has been updated.

Episode Links

  • Source Code
  • Advanced NSOperations talk at WWDC'15 - This talk by Dave DeLong inspired the foray into NSOperation-based architectures.
  • Earthquakes Sample - This Apple sample app is chock-full of advanced NSOperation usage. If this episode piqued your interest, definitely check out the sample and see more.
  • PSOperation - This is an up-to-date implementation of the ideas presented in the WWDC session, which includes changes to work with Swift 2 and Xcode 7. Worth taking a look at.

Presenting a Login screen with an operation

This operation is interesting for a few reasons:

  • It returns immediately if you're already logged in. This allows you to make this operation a dependency of many other operations, and if you're ever not logged in, then the login screen will pop up.
  • It presents UI. We need something to present off of, so in the absence of a provided view controller, we can reach into the application's keyWindow and use the rootViewController (if we have one).
  • It's asynchronous in nature (the user has to type stuff) so the operation stays running for quite a while. We need a callback in order to call finish() and wrap up the operation.

class LoginOperation : Operation, LoginViewControllerDelegate {
    lazy var storyboard: UIStoryboard = {
        return UIStoryboard(name: "Main", bundle: nil)
    }()

    lazy var loginNavController: UINavigationController = {
        let nav = self.storyboard.instantiateViewControllerWithIdentifier("loginNavigationController") as! UINavigationController
        let loginVC = nav.viewControllers.first! as! LoginViewController
        loginVC.delegate = self
        return nav
    }()

    lazy var presentationContext: UIViewController = {
        return UIApplication.sharedApplication().keyWindow!.rootViewController!
    }()

    override func execute() {
        if AuthStore.instance.isLoggedIn() {
            finish()
            return
        }

        dispatch_async(dispatch_get_main_queue(), {
            self.presentationContext.presentViewController(self.loginNavController, animated: true, completion: nil)
        }
    }

    func loginViewControllerDidLogin(loginViewController: LoginViewController) {
        finish()
    }
}

Loading the Core Data Stack

In this operation, the logic for loading the core data stack is tucked away in the operation, and the caller just is presented with the initialized main context.

class LoadDataModelOperation : Operation {
    let loadHandler: NSManagedObjectContext -> Void
    var context: NSManagedObjectContext?

    init(loadHandler: NSManagedObjectContext -> Void) {
        self.loadHandler = loadHandler
        super.init()
    }

    // a few needed properties...   

    override func execute() {
        var storeError: NSError?
        if let store = persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil, error: &storeError) {

        } else {
            println("Couldn't create store")
            abort()
        }

        dispatch_async(dispatch_get_main_queue()) {
            self.context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
            self.context!.persistentStoreCoordinator = self.persistentStoreCoordinator

            self.loadHandler(self.context!)
            self.finish()
        }
    }
}

Composite Operations

Sometimes it is desirable to break an operation down into smaller operations to make things easier to understand. Building operations like this is fairly easy: you have an internal queue and add the operations to them. Then we have a dummy operation at the end that calls finish() for us.

class DownloadEpisodesOperation : Operation {
    var path: String
    var error: NSError?
    let context: NSManagedObjectContext

    private var internalQueue = NSOperationQueue()

    init(path: String, context: NSManagedObjectContext) {
        self.path = path
        self.context = context
    }

    override func execute() {
        internalQueue.suspended = true

        let fetchOperation = FetchRemoteEpisodesOperation(path: path)
        let importOperation = ImportEpisodesOperation(path: path, context: context)
        importOperation.addDependency(fetchOperation)

        internalQueue.addOperation(fetchOperation)
        internalQueue.addOperation(importOperation)

        let finalOperation = NSBlockOperation(block: {
            self.finish()
        })

        finalOperation.addDependency(importOperation)
        internalQueue.addOperation(finalOperation)

        internalQueue.suspended = false
    }
}
blog comments powered by Disqus