Functional View Controllers: An Experiment

We tightly couple our view controllers to data models, networking, and the next view controller. Are there ways to avoid this? In this talk, Chris Eidhof experiments with a functional approach, live-coding an iOS GitHub client to discover just how far we can decouple a view controller, and still have something that controls a view.

You can view the code used in this talk on GitHub.


The App (0:00)

In the next twenty minutes, we will make a small app that shows you a list of GitHub organizations. These are the organizations that I belong to on GitHub, and the app allows you to click on an organization, then it loads the repositories. I can click on my favorite organization, BananaKit, and if I click on the project, I can see the issues. We’ll start from scratch; this is the code that we will start with.

import UIKit
import FunctionalViewControllers

func app() -> UIViewController {

}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        window = UIWindow(frame: UIScreen.mainScreen().bounds)
        window?.rootViewController = app()
        window?.makeKeyAndVisible()
        return true
    }
}

We have our app delegate, in which there is the line window?.rootViewController = app(). This app() is a UIViewController, and so, if we return that, we start with a black screen for the app.
swift func app() -> UIViewController { return UIViewController() }

View Controller Responsibilities (1:36)

If you’re building view controllers, your view controller is usually doing a lot of things. Back in the Objective-C days, people had view controllers that were more than 5,000 lines of code. In Swift, that would be maybe 2,500 lines, but that’s still a lot. So, we should think about what the view controller does. The one I am building shows the organizations. The moment it is loaded, it starts loading the organizations, and in our case it does a network request. Then, another thing a view controller does is load things from the nib, or set up the views. It also puts the data into the views, once the data is loaded. The final thing a view controller does is know what the next view controller will be. That’s a lot of things a view controller needs to do! It’s very tightly coupled to the data model, to the networking, and to the next view controller.

What I’m doing in code is an experiment, to see if we can remove a lot of those responsibilities. to make that clear, what I’m showing today is just me playing around in my free time, building this library. It’s not production-ready. I don’t know anybody who has shipped anything with this. It’s just an experiment to see how far we can push this idea, and remove these responsibilities.

Live Coding!

Initial View Controller (3:05)

Let’s start live coding. This is a very simple app currently. We’re going to use a resourceTableViewController, and in order to use this, we need to provide three parameters. The first is a resource of an array of Bs. In our case, it’s going to be a resource of an array of organizations. The next thing is the configuration, CellConfiguration of B. This is a way to display your tableViewCell for a certain B. In our case, the B is an organization, so we can put that into a variable. Let’s call this configuration, which will be a CellConfiguration to display an organization. We can do this by using this simple library function called standardCell. This takes a function that, given an organization, needs to produce a string. In GitHub lingo, an organization doesn’t have a name, it has a login. So we can say that configuration is a function that takes an organization and returns the organization’s login. Then we can pass in configuration for the second parameter. Then we need a navigation item, and again, this is very simple: we just say defaultNavigationItem. We can try to run this, and it compiles.

func app() -> UIViewController {
	let configuration: CellConfiguration<Organization> = standardCell( {
		organization in
		organization.login
	})

	resourceTableViewController(organizations(), configuration,
			navigationItem: defaultNavigationItem)

	return UIViewController()
}

This is a very good first step, but of course it doesn’t really do anything because we are not using this resourceTableViewController. So, we put it into a variable called orgsVC for the organizations’ view controller, and now we can try to return it. If we do this, we will see that we get a build fail, because this function expects a UIViewController as a result, and we are instead returning a screen of organization, Screen<Organization>.

Perhaps we can call some methods on this. We can, there is run. To quickly explain what it does, you need to pass run a function. This gets called when you select an organization. It takes one parameter selectedOrg and then we can, for example, println(). This compiles, and it loaded from the network the organizations, which we can select. At the bottom, you can see that it has printed the organization. We can change that to the organization’s login, so that we can see that it actually selected the right one.

func app() -> UIViewController {
	let configuration: CellConfiguration<Organization> = standardCell( {
		organization in
		organization.login
	})

	let orgsVC = resourceTableViewController(organizations(), configuration,
			navigationItem: defaultNavigationItem)

	return orgsVC.run { selectedOrg in
		println(selectedOrg.login)
	}
}

Nested views (6:21)

We’ll now repeat this a couple of times to have nested screens. The first thing we want to do when we select an organization is display the organization’s repositories. We can make a function that takes in an organization, and returns the screen that allows a user to select a respository. We know how to make the screen by using resourceTableViewController, and we will need those same three arguments as before: a resource, the CellConfiguration, and the navigationItem.

The resource is easy to get — we just ask the organization for its reposResource. For the configuration, we can write this in-line, and say that given a repo should just return its name. Then we can use the same defaultNavigationItem. We also need to use the function standardCell.

let reposScreen: Organization -> Screen<Repository> = { org in
        return resourceTableViewController(org.reposResource, standardCell { repo in
            (repo.name)
        }, navigationItem: defaultnavigationItem)
    }

The build succeeds, but if we click on an organization, nothing happens. We need some way to hook these screens up. I’m going to use a magical function from the library, and — you might hate me for this — I also made a custom operator. Before we do this, we need to think about what’s happening here. If we look at the app, we can see that it’s just a viewController. We want to put this into a navigationController so that when somebody pushes it, the next view controller will be pushed. The first step is to create a navigationController. We want to turn the screen into a screen that’s inside a navigationController, and it also has a run function.

func app() -> UIViewController {
	let configuration: CellConfiguration<Organization> = standardCell( {
		organization in
		organization.login
	})

	let orgsVC = resourceTableViewController(organizations(), configuration,
			navigationItem: defaultNavigationItem)

	let reposScreen: Organization -> Screen<Repository> = { org in
        return resourceTableViewController(org.reposResource, standardCell { repo in
            (repo.name)
        }, navigationItem: defaultnavigationItem)
    }

	return navigationController(orgsVC).run { _ in () }
}

We can see that we have the same view controller as before, but with a navigation bar. In order to hook this up to the next view controller, I’m going to extract this navigationController part, and put it into a variable called flow. This will describe the main flow of our app. If we look at the type, we can see that this is a navigationController of Organization.

My custom operator is called push, and it pushes another view controller on the stack. On the left hand side, we have a navigationController, and on the right hand side, we write this new reposScreen function that we made. So if we run this, it works. Yeah!
```swift func app() -> UIViewController { let configuration: CellConfiguration = standardCell( { organization in organization.login })

let orgsVC = resourceTableViewController(organizations(), configuration,
		navigationItem: defaultNavigationItem)

let reposScreen: Organization -> Screen<Repository> = { org in
	return resourceTableViewController(org.reposResource, standardCell { repo in
        (repo.name)
	}, navigationItem: defaultNavigationItem)
}

let flow = navigationController(orgsVC) >>> reposScreen

return flow.run { _ in () } } ```

More views! (10:23)

So far we have not made a single UIViewController subclass. All we have done it call some functions, hook up our screens, and, like magic, it works! We can repeat this trick to show issues, which will be a similar process as before. Let’s call it issuesScreen. This will be a function that is given a repository, and will return a screen that allows the user to select an issue. We call resourceTableViewController again, and the resource will be the issues of the repository, so we can just say repo.issuesResource. The configuration is going to display the issue name, so it will be a standardCell. Given an issue, it will display the issue’s title and use the defaultNavigationItem.

let issuesScreen: Repository -> Screen<Issue> = { repo in
    return resourceTableViewController(repo.issuesResource, standardCell { issue in
        (issue.title)
    }, navigationItem: defaultNavigationItem)
}

This builds, but we still need to hook it up. By now, we know how to hook up another view controller. We just type this operator again, add issuesScreen, and run it. The build succeeds. So, what’s going to happen? If we select the repo, an organization, we still get the repo. If we select the organization “BananaKit”, and navigate to my favorite repo of this organization, we see the issues. This is only five more lines of code, and we have a whole flow.

func app() -> UIViewController {
	...

	let flow = navigationController(orgsVC) >>> reposScreen >>> issuesScreen

	return flow.run { _ in () }
}

Include Existing Code (12:26)

We can make all these TableViews, but of course, we already all have a lot of existing code. How this works with existing code is a very valid question. The answer is not very magical, it’s actually very simple to use your existing view controllers with this model. I’ll show you how using this pre-made small Storyboard that has a login view controller. It’s a simple view controller in a Storyboard that has a TableView with a username and password, connected with IBOutlets, all as a simple UITableViewController subclass. Perhaps the only fancy thing is the completion handler: if the login button gets tapped, its IBAction gets executed, and it tries to get the text from the username and password; then, if it has both, it will call our completion handler.

We want to wrap this somehow to create a screen from it. We create a small function, loginViewController, to return a screen that has LoginInfo when it completes. How do we go about this? Well, the first step is to load the view controller from the Storyboard. The next step is to return a screen, which takes a function, which, again, takes a callback. Then, a screen needs to return a view controller. We need to pass this callback to the view controller, and that’s just as simple as saying vc.completion = callback. We can run it, and it builds.

func loginViewController() -> Screen<LoginInfo> {

    return Screen { callback in
        var vc = UIStoryboard(name: "Storyboard", bundle: nil).instantiateViewControllerWithIdentifier("LoginViewController") as! LoginViewController
        vc.completion = callback
        return vc
    }

}

This is ready to use. What we could do, for example, is put code at the end of our navigation hierarchy. Let’s open the AppDelegate. loginScreen would be a function that, given an issue, shows a screen to log in. So, given an issue, we just return the loginViewController that we just wrapped. Then, we add it to the back of our flow and try to run it. What’s going to happen? We can log in after we try to click on an issue in the repo of the organization.

func app() -> UIViewController {
	...

	let loginScreen: Issue -> Screen<LoginInfo> = { _ in
		return loginViewController()
	}

	let flow = navigationController(orgsVC) >>> reposScreen >>> issuesScreen >>> loginScreen

	return flow.run { _ in () }
}

Of course, this is very nonsensical, but it shows how easily you can wrap your own existing view controllers, and do something with them. We can make this a little bit nicer, by taking the loginScreen out and putting it at the front. In order to do this, I want to display the loginViewController as my root view controller, then display the organization screen. We have orgsVC, which has the type Screen<Organization>, and what we want is a function. It gets a little complicated, but we can just say that orgsScreen is a function that, when given LoginInfo, will display a Screen that allows the user to select an organization. We pass in LoginInfo as a parameter, and return the resourceTableViewController. We end up with a login, and if the user presses login, it shows their repos. Hooray!

func app() -> UIViewController {

	let orgsScreen: LoginInfo -> Screen<Organization> = { loginInfo in
		return resourceTableViewController(organizations(), configuration, navigationItem: defaultNavigationItem)
	}

	// more screens
	...

	let flow = navigationController(loginViewController()) >>> orgsScreen >>> reposScreen >>> issuesScreen

	return flow.run { _ in () }
}

Conclusion (18:37)

We end up with a view controller that is completely decoupled, which is very cool! Our view controllers do not know anything about the data model. We just plug some data into the view controller and we can use it. Also, our view controllers definitely do not know about any of the other view controllers. In fact, this resourceTableViewController is a very simple wrap around something called asyncedTableViewController, and you could easily make it work with something like Core Data. If you want to swap out your entire model stack, you can do just that, without having to change all your view controllers around.

I think this is a very powerful technique. As I said, it’s just something I’m playing around with, so I’m not really sure if it will actually work for real things, but so far, it seems like it will. I hope to get back to an iOS project so that I can test it in production.

Thank you!


You can follow Chris’ experiments on GitHub



Chris Eidhof

Chris Eidhof

Chris Eidhof is the author of many iOS and OS X applications, including Deckset and Scenery. He has also written extensively on the subject, from his personal blog to objc.io to a variety of books. He formerly ran UIKonf, and still runs frequently.