Callouts are small, transient popups that display information or user interface controls. As part of my Swift node based calculator experiment, I wanted to add two new features, a numeric dial and a delete node button, which are ideal candidates to be contained in a callout container.

I've used a different approach for both new features and this post describes how I've implemented them.

Popovers



First off, the numeric dial is launched from a UIBarButtonItem in my Toolbar class

The numeric dial is my own component that I blogged about here. In this implementation, I've updated its layers to extend CAShapeLayer and draw its value line immediately through a drawValueCurve() method in the NumericDialTrack class.

The dial itself knows nothing of my presentation model; in keeping with the strategy I've used for years, my low-level UI components are framework and domain agnostic. My NumericDialViewController, which houses the control, does know about the presentation model and has a few observers on it:


        NodesPM.addObserver(self, selector: "nodeChangeHandler", notificationType: NodeNotificationTypes.NodeSelected)
        NodesPM.addObserver(self, selector: "nodeChangeHandler", notificationType: NodeNotificationTypes.NodeUpdated)

nodeChangeHandler() does little more than normalise the current node's value and update the control to reflect the relevant node's value:


    func nodeChangeHandler()
    {
        if let selectedNode = NodesPM.selectedNode
        {
            let value = selectedNode.value
            
            ignoreDialChangeEvents = true
                
            numericDial.currentValue = value / 100
                
            ignoreDialChangeEvents = false
        }
    }

When the user changes the dial position, the controller acts on a .ValueChanged control event on the dial and notifies the presentation model:


    func dialChangeHandler(numericDial: NumericDial)
    {
        let dialValue = Double(Int(numericDial.currentValue * 100))
        
        if !ignoreDialChangeEvents
        {
            NodesPM.changeSelectedNodeValue(dialValue)
        }
    }

Up in the Toolbar class, I create an instance of my NumericDialController and an instance of UIPopoverController with the numeric dial controller set as its contentViewController:


    let numericDialViewController: NumericDialViewController
    let popoverController: UIPopoverController

    [...]

    numericDialViewController = NumericDialViewController()
    popoverControllerUIPopoverController(contentViewController: numericDialViewController)

I want to know when the dial callout has been closed and to do that my toolbar implements the UIPopoverControllerDelegate and sets itself to the popover controller's delegate:


    popoverController.delegate = self

The user launches the numeric dial callout by clicking a UIBarButtonItem:


    let numericButtonShowDial = UIBarButtonItem(title: "Dial", style: UIBarButtonItemStyle.Plain, target: self, action: "showDial:")

The showDial() method actually displays the callout. It does this by checking we have a rootViewControler and then simply invoking presentPopoverFromBarButton():


    popoverController.presentPopoverFromBarButtonItem(value, permittedArrowDirections: UIPopoverArrowDirection.Any, animated: true)

One thing I found was that although the callout is meant to be modal, the little calculator buttons were still clickable. Changing the toolbar's userInteractionEnabled to false didn't seem to help, so I created a little function that manually enables or disables the individual button bar buttons:


    func enableItems(enable: Bool)
    {
        if let barButtonItems = items
        {
            for barButtonItem:AnyObject in barButtonItems
            {
                (barButtonItem as UIBarButtonItem).enabled = enable;
            }
        }
    }

This is invoked when the callout is opened and, because toolbar is the popover controller's delegate, invoked again when the callout is closed through the popoverControllerDidDismissPopover() method:


    func popoverControllerDidDismissPopover(popoverController: UIPopoverController)
    {
        enableItems(true)
    }

Action Sheets



Next up is an ActionSheet which is launched from my MenuButton class.

I could have used the same technique for the menu button, but UIKit offers a simpler alternative for creating action sheets, UIAlertController

The UIController contains a set of UIAlertAction instances. In my case, I have one for deleting the selected node and two others to toggle nodes between numeric and operator types. 

My first steps are to instantiate the UIAlertController and add the actions:


    var alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertControllerStyle.ActionSheet)

    [...]

    makeOperatorAction = UIAlertAction(title: NodeTypes.Operator.toRaw(), style: UIAlertActionStyle.Default, handler: changeNodeType)
    makeNumericAction = UIAlertAction(title: NodeTypes.Number.toRaw(), style: UIAlertActionStyle.Default, handler: changeNodeType)
    deleteAlertAction = UIAlertAction(title: "Delete Selected Node", style: UIAlertActionStyle.Default, handler: deleteSelectedNode)

    [...]

    alertController.addAction(deleteAlertAction)
    alertController.addAction(makeNumericAction)
    alertController.addAction(makeOperatorAction)

Now that the actions have been created, launching them is simply a matter of invoking presentViewController() on the root view controller in the menu button's overridden touchesBegan() method:


    override func touchesBegan(touches: NSSet, withEvent event: UIEvent)
    {
        if let viewController = UIApplication.sharedApplication().keyWindow.rootViewController
        {
            if let popoverPresentationController = alertController.popoverPresentationController
            {
                popoverPresentationController.sourceRect = frame
                popoverPresentationController.sourceView = viewController.view
                
                viewController.presentViewController(alertController, animated: true, completion: nil)
            }
        }
    }

A little note on NSTimer and userInfo

When I first plugged the numeric dial into this application, performance was pretty grim. Because of the frequent updates to the screen, the dial was very juddery. One of the approaches I took to help with this was to use an NSTimer inside the recursive nodeUpdate() method inside my presentation model.

Although I didn't end up using this approach, I did have to scratch my head for a few minutes to figure out how to pass data from a timer to its selector. I needed to pass a NodeVO and I did this by creating a NSMutableDictionary containing my node and setting that as the timer's userInfo property:


var dictionary = NSMutableDictionary()
    dictionary.setValue(candidateNode, forKeyPath: "node")
    

    var timer = NSTimer(timeInterval: timeInterval, target: self, selector: "timerComplete:", userInfo: dictionary, repeats: false)

Then, timerComplete() uses valeForKey() to extract the node from its argument:


    func timerComplete(value: AnyObject)
    {
        let srcTimer: NSTimer = value as NSTimer
        
        let node: NodeVO = srcTimer.userInfo?.valueForKey("node") as NodeVO
        
        NodesPM.nodeUpdate(node)
    }

All the updated source code is available in my GitHub repository. Please note: this code has been built under Xcode 6.0 and may not work under 6.1.

0

Add a comment

About Me
About Me
Labels
Labels
Blog Archive
Loading