TL;DR The source code can be found here: https://github.com/steffendsommer/TodaysReactiveMenu

I’ve been working with ReactiveCocoa the last 8 months and I’m overall very satisfied with the framework and the approach in general. I’ve been following the progress on ReactiveCocoa 3.0 (mainly focused around Swift) and when it recently hit beta-4, I decided to dig in.

First of all, I want to put out a disclaimer. I’m new to Swift and I’m definitely new to RAC 3.0. Because of that, the majority of the hours I put into making this app, I felt like I had no idea of what I was doing:

I have no idea what I'm doing

Also, it’s worth noting that the intention of this project and blog post is just to get any interested reader kickstarted with ReactiveCocoa 3.0. It is not an introduction to ReactiveCocoa or MVVM. It might be easier to understand this post, if you’ve read the RAC 3 changelog beforehand.

A small API for this purpose

Using rails and some mad scraping skills I created a small backend to parse and expose the list of menus. The API can be found here, and using a limit parameter here.

The limited version will result in a JSON response similar to this:

[
    {
        "cake": false,
        "created_at": "2015-05-29T06:58:09.000Z",
        "id": 241,
        "identifier": "1070757152939264",
        "link": "http://www.facebook.com/Cuisine.dk/posts/1070757152939264",
        "main_course": "Helstegt okseculotte og kartoffelsalat med chili og purløg.",
        "serving_date": 1432857600,
        "sides": "Chilimix. Brune ris med pærer, koriander, rejer og chili. Blandet salat med dressing. Dagens råkost med ananas. Knoldselleri med citron, æble, æbleeddike og persille. Spinatsalat med frisk mango og chili. Pålægsfad med afskåret pålæg, hjemmelavede salater samt. 3 slags ost. dagens kage. Friskbagt solsikkerugbrød, maltbrød og græskarkernebrød. Frisk frugt.",
        "updated_at": "2015-05-29T07:30:09.035Z"
    }
]

The model

The purpose of the app is to show whatever is on the today’s menu of my workplace. Interesting attributes could therefore be:

  • An identifier
  • A link to the menu (if any exernal resource exist)
  • A date for serving
  • The main course
  • The sides
  • And if there’s going to be served cake

Using ObjectMapper, and the built-in DateTransform(), this can be fairly simple mapped:

var identifier: String?
var link:       String?
var servedAt:   NSDate?
var mainCourse: String?
var sides:      String?
var cake:       Bool?

func mapping(map: Map) {
    identifier  <- map["identifier"]
    link        <- map["link"]
    servedAt    <- (map["serving_date"], DateTransform())
    mainCourse  <- map["main_course"]
    sides       <- map["sides"]
    cake        <- map["cake"]
}

Talking to the API

Noticing built-in extensions for NSURLSession in ReactiveCocoa 3, I decided not to include any external networking frameworks. Using these extensions and the Result framwork, fetching the today’s menu seems like a breeze:

func fetchTodaysMenu() -> SignalProducer<Menu?, NSError> {
  
  let session = NSURLSession.sharedSession()
  let request = NSURLRequest(URL: NSURL(string: "http://unwire-menu.herokuapp.com/menus?limit=1")!)
  
  return session.rac_dataWithRequest(request)
    |> map { data, response in
      let json = try { (NSJSONSerialization.JSONObjectWithData(data, options: nil, error: $0) as? [NSDictionary])?.first }
      return Mapper<Menu>().map(json.value)
  }
}

Preparing data for the view

Now for the fun part! So, the view model is responsible for any massaging of data while exposing whatever the view needs to display. This is often here you would do a lot of chained operations for making your code reactive and to achieve the content you want. Being new to both RAC 3 and Swift, this is here I struggled the most.

The basics

Let’s start with covering some of the most basic questions:

  • Use self.something <~ self.anotherThing instead of RAC(self, something) = RACObserve(self, anotherThing).
  • Use someResult |> filter { someVar in return someVar } instead of [someResult filter:^BOOL(id someVar) { return someVar; }];
  • Look into ConstantProperty, MutableProperty and SignalProducer for making these RAC bindings.
  • Yes, you need to be a lot more strict with handling your types. (Dont worry, you will start to like it).

Declaring a RAC property

Having these fundamentals in place, let’s look into some binding examples. The first one, being a constant that never changes: let headline = ConstantProperty<String>("Today's Menu")

Content manipulation

The next one, being a minor manipulation using map:

private let menu = MutableProperty<Menu?>(nil)
let sides = MutableProperty<String>("")

self.sides <~ self.menu.producer
    |> ignoreNil
    |> map { menu in
        menu.sides!
    }

Note how the ignoreNil is used as menu might be nil.

Signal manipulation

The last example looks into solving the following challenge: to fetch today’s menu every time the view gets active.

Let’s start out by declaring two helpers, as we’re not using ReactiveViewModel:

let active = NSNotificationCenter.defaultCenter().rac_addObserverForName(UIApplicationDidBecomeActiveNotification, object: nil).toSignalProducer()
    |> ignoreError
    |> map { _ in
        true
    }
let inactive = NSNotificationCenter.defaultCenter().rac_addObserverForName(UIApplicationWillResignActiveNotification, object: nil).toSignalProducer()
    |> ignoreError
    |> map { _ in
        false
    }

self.viewIsActive <~ merge([active, inactive])

Two signal producers to tell us whether or not the view is active. Note how toSignalProducer() has been used to go from RACSignal to SignalProducer. Note how ignoreError is being used to tell the compiler that we only care about values, not errors. The ignoreError and merge operator are small helpers I made during this project and is located in the repo as well.

Having the knowledge for when the view is active, let’s try to use this for triggering a menu fetch:

self.menu <~ self.viewIsActive.producer
    |> filter { isActive in
        return isActive
    }
    |> flatMap(FlattenStrategy.Latest) { _ in
        return fetchTodaysMenu()
            |> observeOn(QueueScheduler.mainQueueScheduler)
            |> on(error: { _ in
                // TODO: Make this more declarative.
                self.hideMenu.put(true)
            })
            |> ignoreError
    }
    |> observeOn(QueueScheduler.mainQueueScheduler)

Now this is cool! Let’s go through it:

  • Start with the signal producer that sends values to tell whether or not the view is active.
  • Filter each time, using the above value, to make sure we only proceed when the view is active.
  • flatMap(FlattenStrategy.Latest) (or flattenMap and switchToLatest in RAC 2.x) that into another signal producer that fetches the menu remotely.
  • Make sure to return on the mainQueueScheduler as this will be presented in the UI.

Showing the menu of today

Having the hardest part taking care of, the only thing we’re missing now, is to actually present our content in the view. It seems like there’s no extensions for UI elements yet, as Colin Eberhardt also mentions in his blog post, but he was so kind to make some for us

Having these extensions, it’s a no-brainer to wire up our UI elements with our view model. Here’s some examples:

self.headline.rac_hidden <~ self.viewModel.hideMenu
self.headline.rac_text <~ self.viewModel.headline
self.logo.rac_image <~ self.viewModel.logo

That’s a wrap

We're done!

That’s it! I hope this post will help you get started with ReactiveCocoa 3 as it took me quite a while just to setup basic bindings. I did take some shortcuts and I consider the project as being experimental and as work in progress. Feel free to ping me at @steffendsommer or leave a comment with any feedback or questions.

Some other useful resources

ReactiveCocoa 3.0 is, at the time of writing, still very new and because of that, the resources are limited. I also know that this post is jumping straight into the bits and pieces of the app instead of explaining the concepts of ReactiveCocoa 3.0. If you’re looking for other resources, here are some I used while creating this project: