Building a Unidirectional Data Flow App in Swift with Realm

Have you heard of Unidirectional Data Flow, but have no idea what it involves? Or perhaps looked at some of the frameworks but they appear too complex?

This tutorial will show you how to build a Realm app using a very simple implementation of the 1-way flow architectural style without using any existing unidirectional frameworks.

Overview

The MVC design pattern has always been the cornerstone of Cocoa development (and UI programming in general), but alternatives have surfaced recently in the web community. One such alternative is the idea of Unidirectional Data Flow, as used by the React & Flux patterns developed at Facebook, which help solve some of the problems experienced with two-way bindings in complex applications.

The Unidirectional Data Flow pattern is also starting to be applied to native mobile development, where it can significantly simplify multi-threaded or callback-heavy code, and help with a large class of bugs related to tracking state using mutable properties in your view controllers.

As an example, we’re going to build a simple time tracking app. This will support:

  • Adding a new project
  • Starting and stopping an activity
  • Viewing the elapsed time on each project
  • Deleting a project

Introduction to Unidirectional Data Flow

Unidirectional Data Flow

The key elements of this style are:

  • Encapsulate all the state for your application in a single data structure - ‘a single source of truth’ (the Store).
  • Ensure all state changes (Actions) are performed directly on this data structure, and published as change notification events.
  • Make sure views are only updated in response to state change notifications.

For more details, check out Benjamin Encz’s great introduction to Unidirectional Data Flow

Let’s get started, but if you want to jump to the end product, you can see the final code on GitHub here.

Tutorial

Create a new Xcode project, using the “Single View Application” template. Be sure “Language” is set to Swift, and that “Use Core Data” is unchecked.

Add the Realm and RealmSwift frameworks using your preferred dependency approach (see instructions for CocoaPods, Carthage, and binary installs here).

Add a new Swift file called ‘Store.swift’ and create the Project and Activity Realm objects - these will be used to record the application state.

import RealmSwift

class Project: Object {
    dynamic var name: String = ""
    let activities = List<Activity>()
}

class Activity: Object {
    dynamic var startDate: NSDate?
    dynamic var endDate: NSDate?
}

We’ll also take this opportunity to add a couple of computed properties to the Project class, which will simplify some code a little later on.

extension Project {
    var elapsedTime: NSTimeInterval {
        return activities.reduce(0) { time, activity in
            guard let start = activity.startDate,
                let end = activity.endDate else { return time }
            return time + end.timeIntervalSinceDate(start)
        }
    }
    
    var currentActivity: Activity? {
        return activities.filter("endDate == nil").first
    }
}

Next we’re going to create the Store. The good news is that Realm already works very much like a unidirectional data store and we don’t need to write a lot of boilerplate code to implement this.

We’ll use the built-in Realm change notifications to trigger view updates - this will automatically detect & publish updates from realms on background threads, too.

First we’ll extend Realm with a computed property that returns the current application state - in this case it’s just a list of all our projects.

// MARK: Application/View state
extension Realm {
    var projects: Results<Project> {
        return objects(Project.self)
    }	
}

Next we’ll build the actions, again by extending Realm. Actions are the only methods that should change model data in the Realm, and they shouldn’t return a value - any changes to the model are published to the views via a notification. This ensures the view can be consistently redrawn each time the state is updated, regardless of where the change originated.

// MARK: Actions
extension Realm {
    func addProject(name: String) {
        do {
            try write {
                let project = Project()
                project.name = name
                add(project)
            }
        } catch {
            print("Add Project action failed: \(error)")
        }
    }
    
    func deleteProject(project: Project) {
        do {
            try write {
                delete(project.activities)
                delete(project)
            }
        } catch {
            print("Delete Project action failed: \(error)")
        }
    }
    
    func startActivity(project: Project, startDate: NSDate) {
        do {
            try write {
                let act = Activity()
                act.startDate = startDate
                project.activities.append(act)
            }
        } catch {
            print("Start Activity action failed: \(error)")
        }
    }
    
    func endActivity(project: Project, endDate: NSDate) {
        guard let activity = project.currentActivity else { return }
        
        do {
            try write {
                activity.endDate = endDate
            }
        } catch {
            print("End Activity action failed: \(error)")
        }
     }

}

At the bottom of the file, create a global instance of Store.

let store = try! Realm()

Now let’s implement the View layer. Open your ‘ViewController.swift’ file and change it from a UIViewController to a UITableViewController subclass. Add a projects property and override the required UITableViewDataSource methods. We’ll also add a UITableViewCell subclass while we’re at it - note that the cell will reset each of its subview properties whenever the project property changes; again, this is important to ensure the view is updated consistently when the model changes.

class ViewController: UITableViewController {

    let projects = store.projects
    	
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return projects.count
    }
    
    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("ProjectCell") as! ProjectCell
        cell.project = projects[indexPath.row]
        return cell
    }
}

class ProjectCell: UITableViewCell {
    @IBOutlet var nameLabel: UILabel!
    @IBOutlet var elapsedTimeLabel: UILabel!
    @IBOutlet var activityButton: UIButton!
    
    var project: Project? {
        didSet {
            guard let project = project else { return }
            nameLabel.text = project.name
            if project.currentActivity != nil {
                elapsedTimeLabel.text = "⌚️"
                activityButton.setTitle("Stop", forState: .Normal)
            } else {
                elapsedTimeLabel.text = NSDateComponentsFormatter().stringFromTimeInterval(project.elapsedTime)
                activityButton.setTitle("Start", forState: .Normal)
            }
        }
    }
    
    @IBAction func activityButtonTapped() {
        guard let project = project else { return }
        if project.currentActivity == nil {
            // TODO: start a new activity
        } else {
            // TODO: complete the activity
        }
    }
}

Next we’ll register the view controller as a notification observer on our store, so we know to reload the view when the state changes. Implement Realm notifications on ViewController as follows:

var notificationToken: NotificationToken?

override func viewDidLoad() {
    super.viewDidLoad()
    
    updateView()
    notificationToken = store.addNotificationBlock { [weak self] (_) in
        self?.updateView()
    }
}

func updateView() {
    tableView.reloadData()
}

Now we’ll wire everything up in Interface Builder. Open ‘Main.storyboard’ and do the following:

  • Delete the existing ‘View Controller’ scene
  • Drag on a Navigation Controller from the Object Library. This will also create a UITableViewController scene as the navigation root.
  • Tick “Is Initial View Controller” on the Navigation Controller
  • Select the Root View Controller and change the Custom Class on the Identity Inspector to ViewController
  • Select the Table View Cell and change the Custom Class on the Identity Inspector to ProjectCell. Also set the Reuse Identifier on the Attributes Inspector to “ProjectCell”
  • Drag two UILabels and a UIButton onto the cell prototype and set up autolayout as desired. Connect them to the nameLabel, elapsedTimeLabel and activityButton outlets on the ProjectCell. While you’re there, connect the activity button’s TouchUpInside to the activityButtonTapped action on the cell.
  • Select the Navigation Item and change the title to something better than ‘Root View Controller’!

At this point, all of the view controller presentation code is done - whenever the state changes, the table will update accordingly. You should be able to build & run the app now, although it won’t be very exciting as there are no projects (and no way to add any)!

So let’s add some actions to show how state updates work - we’ll start with adding a new project. Because a project only needs a name, the simplest approach will be to put the ‘add project’ view in the table header.

In the storyboard, set up the ‘add project’ elements as follows:

  • Drag a Bar Button Item from the Object Library to the right of the navigation bar. Xcode can be a bit temperamental here - I find it’s sometimes easier to drag to the navigation item in the Document Outline (left-hand Interface Builder pane).
  • In the Attributes Inspector, change the bar button System Item setting to ‘Add’
  • Drag a View to the top of the Table View, then add a Text Field and a Button and configure autolayout as you’d like it.
  • Change the button title to something like ‘Add’.
  • Open the Assistant Editor and make sure it’s displaying the ‘ViewController.swift’ file.
  • Control-drag the text field to the ViewController.swift source and create a new outlet called newProjectTextField
  • Control-drag the ‘Add’ button in the header to ViewController.swift and create a new action called addButtonTapped. Don’t forget to change the drop-down to ‘action’!
  • Control-drag the ‘+’ bar button item to ViewController.swift and create a new action called showNewProjectView. Again, use the Document Outline if Xcode is being recalcitrant.

Hide the Assistant View and switch back to ‘ViewController.swift’. Add the code to show/hide the table header, and importantly, in the addButtonTapped method, call the addProject method on the store. You’ll also want to add a hideNewProjectView() call to stateDidUpdate.

func updateView() {
    tableView.reloadData()
    hideNewProjectView()
}

@IBAction func showNewProjectView(sender: AnyObject) {
    tableView.tableHeaderView?.frame = CGRect(origin: CGPointZero, size: CGSize(width: view.frame.size.width, height: 44))
    tableView.tableHeaderView?.hidden = false
    tableView.tableHeaderView = tableView.tableHeaderView // tableHeaderView needs to be reassigned to recognize new height
    newProjectTextField.becomeFirstResponder()
}

func hideNewProjectView() {
    tableView.tableHeaderView?.frame = CGRect(origin: CGPointZero, size: CGSize(width: view.frame.size.width, height: 0))
    tableView.tableHeaderView?.hidden = true
    tableView.tableHeaderView = tableView.tableHeaderView
    newProjectTextField.endEditing(true)
    newProjectTextField.text = nil
}

@IBAction func addButtonTapped() {
    guard let name = newProjectTextField.text else { return }
    store.addProject(name)
}

If you run the app now, you should be able to add new projects - awesome! The table updates automatically after the addProject method is called, even though we have no UI updating code in addButtonTapped - changes to the application state automatically flow through to the views. This is unidirectional data flow in action.

The remaining actions are fairly straightforward - we can fill in the start/stop behaviour in ProjectCell.activityButtonTapped:

@IBAction func activityButtonTapped() {
    guard let project = project else { return }
    if project.currentActivity == nil {
        store.startActivity(project, startDate: NSDate())
    } else {
        store.endActivity(project, endDate: NSDate())
    }
}

And also implement swipe-to-delete in ViewController using the appropriate UITableViewController method overrides:

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    store.deleteProject(projects[indexPath.row])
}

That’s it! Build & run your super-useful time-tracking app, and pat yourself on the back for having implemented Unidirectional Data Flow from scratch.

Recap

In this tutorial we’ve seen the concept of Unidirectional Data Flow, and how it’s possible to implement this approach in a Realm app using the built-in Realm notification features. In particular, we’ve applied the principles by:

  • Storing all our application state in a ‘single source of truth’ (the Realm)
  • Only performing state changes via the action methods we defined on the Realm extension
  • Ensure all our view updates are performed in response to a Realm update notification.

Taking Things Further

There are a number of areas where our app could be improved, and these are important considerations worth taking into account if you’re adopting these techniques in your own apps.

  1. Reactive Updates: This pattern really lends itself to using a Functional Reactive Programming library to distribute state update events throughout your application. Take the plunge and learn how to use a library like ReactiveCocoa, RxSwift, or ReactKit.
  2. Local State: You’ll notice if you start/stop an existing project in the middle of adding a new one, the new project text field & button will disappear as we’re resetting the whole view each time the AppState changes. If you have some state like this that is view-specific and doesn’t belong at the AppState level, it’s okay to store it in the ViewController, however it’s best to define a struct to hold all local state and use a single mutable property. Using a Reactive library will help you consolidate update events on local and app state into a single handler.
  3. Count-Up Timer: To keep things simple we’re just showing a watch emoji on the currently running activity, however it would make more sense to update the label each second with the current elapsed time. While it’s possible to calculate this at the model layer and fire update events once per second, this is pretty heavyweight and isn’t relevant to the application state (it’s display only). It would be better to use a custom iOS equivalent of WKInterfaceTimer and let the label handle displaying the updated time values.

Now you’re an expert on Unidirectional Data Flow, it might be worth reviewing some of the existing frameworks to see if it will make your life easier. Check out:

  • ReSwift - ReduxKit & Swift-Flow merged to form ReSwift - there’s also a router project to help with navigation.
  • SwiftFlux - A Flux implementation for Swift
  • Few.swift - An attempt at a React-style declarative view layer in Swift


Realm Cocoa Team

Realm Cocoa Team