A is for Action - The Basics

So I've been playing around with ReactiveCocoa 4 and Swift 2.1 (but who hasn't, eh?). And finally decided to blog about what I have found and succeeded to achieve with somewhat minimal documentation. I know, I know, don't complain and instead create one, so this article hopefully shed some light on how one can use RAC's actions.

Action is a class that encompasses some work that needs to be done when it is presented with an input. It is typed class that accepts 3 parameters as part of type declaration:

Action<Input, Output, Error: ErrorType>  
The Opportunity

I decided to implement setting screen for the app I am working on using the RAC's Actions. It seemed very fitting since as per documentation, they

<...> are useful for performing side-effecting work upon user interaction, like when a button is clicked.

Replace "button" with any UIControl imaginable and you have yourself experimental work for at least half a day.

So let's say that my app has some sort of event that is triggered with NSTimer. Knowing this, I would like to implement user's preference to whether the device should vibrate when the event is fired. It seems that UISwitch is perfect for this.

I have a SettingsViewController.swift that represents table view controller with static cells, each allowing a change to a user setting. Now my task is to hook up an action to a switch so that each time it is toggled, I store the value to user defaults.

Let's start with an action itself:

let action = Action<Bool, Void, NoError> { value in  
    return SignalProducer<Void, NoError> { observer, _ in
        let defaults = NSUserDefaults.standardUserDefaults()
        defaults.setBool(value, forKey: Settings.VibrateKey)
        defaults.synchronize()
        sendCompleted(observer)
    }
}

If we want to go into some details, it's quite simple. Let's start with a declaration of convenience initialiser:

init(_ execute: Input -> ReactiveCocoa.SignalProducer<Output, Error>)  

It accepts a closure that accepts a value of action's Input type and returns a SignalProducer<Output, Error> (read: that sends a value of Output type or an error of Error type). The SignalProducer will be returned once the action's apply() method is called. I'll talk about who calls this method later on.

The main work is done inside of SignalProducer's initialisation closure. We get the defaults and store the passed in value under the key we want (I prefer having keys listed as static lets in some public struct) and then say that the event has completed by sending completion to the observer.

Notice that we're not calling setObject on defaults. Since we know that Action closure's value is of Input type (which is Bool in our case), we can safely call the method made for Bool values. We don't want to return anything from there so the Output type is Void in this case.

This is half of the work we needed to do. The value's type transition looks very simple:

Bool -> Void

Next - make action to actually do some work when switch is toggled. We will utilise one more class provided by RAC framework: CocoaAction. It is created specifically for this purpose: hook up an action to any UIControl.

It's way easier to create a CocoaAction instance once you have an Action in place:

vibrateAction = CocoaAction(action, { value in  
    let vibrateSwitch = value as! UISwitch
    return vibrateSwitch.on
})

We initialise CocoaAction with a closure that is sort of a prelude to action's... well... action! The first parameter is an action created above and the second parameter is a closure that accepts the control that will trigger this cocoa action as added target! Read the previous sentence a few times just to be sure you understand.

You might have noticed that I created an action with let while vibrateAction was only defined, but not declared. The vibrateAction is a property of view controller. This is because the later called addTarget on UISwitch does not actually retain the vibrateAction, but only keeps weak reference to it. So if you don't retain the instance of CocoaAction in your VC you will lose it once the method's scope ends.

Since we are hooking up this action to a UISwitch, the value inside closure is its instance. But the initialiser defines closure as AnyObject? -> Input it sort of tells us that "Hey, this is your control that attempts to perform an event. What value should I extract from it?". So it's sort of a prequel where we take UISwitch and return Bool (which is a type of Input we declared for our Action). The value's type transition in CocoaAction looks like this:

UISwitch -> Bool

And if we combine it with value's type transition through Action we get a chain like this:

UISwitch -> Bool -> Void

So you could probably already see the pattern here:

  1. Use CocoaAction to get needed value from any UIControl and return it from closure
  2. The return value from step 1 enters action's closure as Input where you can do the work and return the stuff as Output (if necessary in case it's not Void);

Now that we have CocoaAction in place - let's hook it up to our switch:

switch.addTarget(cocoaAction, action: CocoaAction.selector, forControlEvents: .ValueChanged)  

Et voila! Now action is executed each time a UISwitch is toggled.