Nick Harris

UIAlertController + NSOperation

with one comment

I’ve been investigating NSOperation and dependency chaining lately including my posts about syncing data with CloudKit. While working on that code I had two things come up that I didn’t get around to investigating:

1. How can I pass data back after an operation completes in a more consistent way
2. How can I allow user input within an operation

I put together a little project tonight that I used to explore these issues. I like it so I thought I’d share it.

Sample App

This code is available on BitBucket

Its so much easier to explain ideas within the context of a sample app. This app is super simple:

– Tap the button
– Get prompted for what operation you want to run
– View the results

The code creates and executes an operation chain when you tap the button. The operations are executed or canceled depending on which option is chosen. The console documents everything that is happening and when. Simple.

The operation choice is encapsulated in an enum:

enum OperationChoice {
    case Unknown
    case OperationOne
    case OperationTwo
    case OperationThree
    
    func actionSheetOptionTitle() -> String {
        switch self {
        case .Unknown: return "Unknown"
        case .OperationOne: return "Operation One"
        case .OperationTwo: return "Operation Two"
        case .OperationThree: return "Operation Three"

        }

    }

}

Operation Return Values 

I really dislike how my operations in my CloudKit proof of concept return data. Its done with multiple properties on the operation class. For another developer to use those operations they would need to know more details of the operation then they really need to. Moving forward I think I’ll use a tuple instead:

var operationResult: (error: NSError?, operationChoice: OperationChoice)

 

I’ve used this approach in some other Swift class operations but I like how it fits with operations. The consumer of the operation knows the one property they need to interact with on completion. It’s much simpler then lines of documentation about what public properties will have the changes. It also reflects a more functional approach to programming with monad type behavior.

User Interaction in an NSOperation

Every NSOperation I’ve ever created has used main() to execute instead of start(). To be honest, the responsibilities of overriding functions and KVO calls using start() to be a good citizen made me avoid it.

That is until I read a post about combining NSURLSession and NSOperation by Marcus Zarra. Its a great approach that I plan on using. It also demystified some of my apprehensions with overriding the life-cycle of an NSOperation.

I had tried using UIAlertController’s in a main() override. The problem is that the operation reaches the end of main() before the user has a chance to respond, therefore releasing the operation before any dependencies could use the input. Controlling the life-cycle of the operation means that I can keep the operation executing until the user interaction is registered and other operations in the chain can then react appropriately on completion.

If you haven’t read Marcus’s post yet please do so now. I’ll wait…

Cool. Now you know that Swift is a little weird with overriding private properties which is essential to this approach with NSOperations. I also tried using the ivars directly and also found it didn’t work. I need this code in multiple operations. I initially tried using a protocol but ran into more issues about overriding private properties. I think the only solution in this situation is to subclass:

class ConcurrentOperation: NSOperation {
    
    override init() {
        
        override_executing = false
        override_finished = false
        
        super.init()
    }
    
    func completeOperation() {
        executing = false
        finished = true
    }
 
    private var override_executing : Bool
    override var executing : Bool {
        get { return override_executing }
        set {
            willChangeValueForKey("isExecuting")
            override_executing = newValue
            didChangeValueForKey("isExecuting")
        }
    }
    
    private var override_finished : Bool
    override var finished : Bool {
        get { return override_finished }
        set {
            willChangeValueForKey("isFinished")
            override_finished = newValue
            didChangeValueForKey("isFinished")
        }
    }
}

 

Sample Operation Code

With a way to keep my operation in memory until the user interaction is complete, I could create an operation that displays an Action Sheet to choose an operation and operations that use an Alert to display which option was chosen (I’ll only show OperationOne as the other two are identical):

class PromptUserOperation: ConcurrentOperation {
    
    var operationResult: (error: NSError?, operationChoice: OperationChoice)
    
    override init() {
        
        operationResult = (error: nil, operationChoice: .Unknown)
        
        super.init()
    }
    
    override func start() {
 
        print("PromptUserOperation.start()")
        promptUser()
    }
    
    func promptUser() {
        
        let alertController = UIAlertController(title: "Which operation should be performed?", message: nil, preferredStyle: .ActionSheet)
        
        alertController.addAction(createOperationAlertAction(OperationChoice.OperationOne))
        alertController.addAction(createOperationAlertAction(OperationChoice.OperationTwo))
        alertController.addAction(createOperationAlertAction(OperationChoice.OperationThree))
        
        dispatch_async(dispatch_get_main_queue(),{
            if let appDelegate = UIApplication.sharedApplication().delegate,
                let appWindow = appDelegate.window!,
                let rootViewController = appWindow.rootViewController {
                    rootViewController.presentViewController(alertController, animated: true, completion: nil)
            }
        })
    }
    
    func createOperationAlertAction(operationChoice: OperationChoice) -> UIAlertAction {
        
        return UIAlertAction(title: operationChoice.actionSheetOptionTitle(), style: .Default) {
            [unowned self]
            action in
            
            self.userSelection(operationChoice)
        }
    }
 
    func userSelection(selectedOperation: OperationChoice) {
        print("PromptUserOperation.userSelection( \(selectedOperation.actionSheetOptionTitle()) )")
        
        operationResult = (error: nil, operationChoice: selectedOperation)
        completeOperation()
    }
    
    deinit {
        print("PromptUserOperation.deinit")
    }
}
 
class OperationOne: ConcurrentOperation {
 
    override func start() {
        
        if cancelled {
            print("OperationOne - CANCELLED")
            completeOperation()
            return
        }
        
        print("OperationOne.start()")
        promptUser()
    }
    
    func promptUser() {
        
        let alertController = UIAlertController(title: "You chose Operation One!", message: nil, preferredStyle: .Alert)
        
        let alertAction = UIAlertAction(title: "Yay!", style: .Default) {
            [unowned self]
            action in
            
            self.completeOperation()
        }
        alertController.addAction(alertAction)
            
        dispatch_async(dispatch_get_main_queue(),{
            if let appDelegate = UIApplication.sharedApplication().delegate,
                let appWindow = appDelegate.window!,
                let rootViewController = appWindow.rootViewController {
                    rootViewController.presentViewController(alertController, animated: true, completion: nil)
            }
        })
    }
    
    deinit {
        print("OperationOne.deinit")
    }
}
 
@IBAction func runOperations() {
        
    // create an operation queue
    let operationQueue = NSOperationQueue()
        
    // create the operations
    let promptUserOperation = PromptUserOperation()
    let operationOne = OperationOne()
    let operationTwo = OperationTwo()
    let operationThree = OperationThree()
       
    // create the process operation
    let processChoiceOperation = NSBlockOperation {
        [unowned promptUserOperation, unowned operationOne, unowned operationTwo, unowned operationThree] in
            
        print("processChoiceOperation")
            
        let promptUserOperationResult = promptUserOperation.operationResult
            
        if let error = promptUserOperationResult.error {
            print("Error while prompting the user: \(error)")
        }
        else {
            switch promptUserOperationResult.operationChoice {
            case OperationChoice.OperationOne:
                operationTwo.cancel()
                operationThree.cancel()
            case OperationChoice.OperationTwo:
                operationOne.cancel()
                operationThree.cancel()
            case OperationChoice.OperationThree:
                operationOne.cancel()
                operationTwo.cancel()
            default:
                fatalError("Unknown operation choice: \(promptUserOperationResult.operationChoice)")
            }
        }
    }
        
    // set the dependencies
    processChoiceOperation.addDependency(promptUserOperation)
    operationOne.addDependency(processChoiceOperation)
    operationTwo.addDependency(processChoiceOperation)
    operationThree.addDependency(processChoiceOperation)
        
    // queue the operations
    operationQueue.addOperation(promptUserOperation)
    operationQueue.addOperation(processChoiceOperation)
    operationQueue.addOperation(operationOne)
    operationQueue.addOperation(operationTwo)
    operationQueue.addOperation(operationThree)
}

 

And here’s what the console looks like after the user interacts with the app state in the screen shot:

initial_launch

PromptUserOperation.start()


prompt_choice

 

PromptUserOperation.userSelection( Operation One )

processChoiceOperation

OperationThree – CANCELLED

OperationThree.deinit

OperationOne.start()

OperationTwo – CANCELLED

OperationTwo.deinit


show_choice

 

OperationOne.deinit

PromptUserOperation.deinit

 

I love NSOperation Chaining even more now!

Advertisements

Written by Nick Harris

February 10, 2016 at 6:34 am

Posted in Uncategorized

One Response

Subscribe to comments with RSS.

  1. I find it disappointing you thought that combining NSURLSession and NSOperation by Marcus Zarra was a good approach.You see, each NSURLSession maintains it’s own operation queue, and each task added to a session is – under the hood – executed as an NSOperation. Therefore, it doesn’t make sense to maintain an operation queue that controls NSURLSession objects or NSURLSession tasks.

    highflyingtv

    March 30, 2016 at 10:18 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: