One of the talks that Ayaka enjoyed watching is Boundaries by Gary Bernhardt. If you’ve seen a talk about functional programming in Swift, you’ve probably heard it being referenced. When she first watched the talk a few years ago, she understood the theory but wasn’t sure where exactly to apply the concepts. As she has been writing more Swift, she started to see that the concept of “Functional Core, Imperative Shell” applies not just to functional programming, but also to good engineering in general.
In her try! Swift talk, we’ll go over a couple of specific examples how these “boundaries” can help us write better, safer, and more future-proof Swift code. You don’t need to watch the Boundaries talk to understand this talk, but if you have time, definitely watch it because it’s a great talk.
Functional & Imperative (0:00)
Hi everyone, I’m Ayaka. One of the first talks that I watched after Swift was released is this talk by Gary Bernhardt called Boundaries. You might have heard about it from another talk which was like about functional programming.
In his talk, he presents a dichotomy of parts: the functional core and the imperative shell. The idea is that you can make the core of your app functional. It’s all input, output and no side effects, which is much easier to understand. But, not everything that we have to deal with is functional. Most of us have to deal with the UIKit on a daily basis, which causes all sorts of side effects. We also handle networking code which is inherently full of side effects and state.
The idea is that we can pull this out into a separate layer, an imperative shell. The other day, my team and I rewatched the talk titled Advanced iOS Application Architecture and Patterns from WWDC 2014. I really loved this quote from Andy Matuschak:
“All you really know, maybe, is that your taste is improving a lot faster than your ability.”
That’s where I was with this whole functional core and imperative shell idea. I had a taste for it and an intuition. However, I wasn’t sure how to use the ideas in practice. That was fine. To learn something, we first need to develop a taste for it. We also need to develop an intuition. Then, we can start applying it in practice when the time is right.
Boundaries in Practice (4:00)
That’s what I will discuss here. We’re going to talk about boundaries. As I have been writing more and more Swift, I start to see that the concept of functional core and imperative shell applies, not just to functional programming, but also to good engineering in general. Today, I’d like to show you some of these examples.
Networking Example (4:36)
The first one I want to talk about is the story of the immutable core and the network shell. The Venmo app has a news feed where you can see your friends’ stories. You can tap into any of them to see more details. Each story is modeled something like this.
struct Story {
let ID: String
let title: String
let message: String
let sender: User
let recipient: User
let date: NSDate
// ...
}
The story has an ID, title, message, sender, recipient, a date, and maybe a few other things.
The stories view controller class displays a list of stories. It has a list of stories, and just presents it in maybe a table view controller.
class StoriesViewController: UIViewController {
let stories: [Story]
// ...
}
When you tap into one of these stories, we show a story detail view controller, which you can initialize with a story.
class StoryDetailViewController: UIViewController {
init(story: Story)
}
So, this is what gets pushed on if you select something in the table.
class StoryDetailViewController: UIViewController {
private let titleView: StoryTitleView
private let senderView: AvatarView
private let recipientView:AvatarView
private let dateLabel: DateLabel
init(story: Story) {
titleView = StoryTitleView(story: story)
senderView = AvatarView(user: story.sender)
recipientView = AvatarView(user: story.recipient)
dateLabel = DateLabel(date: story.date)
}
}
It has a view for the title, a view for the sender, a view for the recipient, and a date label. When you initialize it with a story, it creates all these views, and sets it to these immutable lets.
I’m pretty happy with this. Everything is non-optional and immutable – as immutable as a view controller can be. I’d say it’s pretty functional of a core.
But of course, soon after, we have to add a new feature on top of this. We have to implement push notifications, and handle different URL schemes such as url_scheme://stories/12345
.
This is what the view controller looks like right now for the detail view.
class StoryDetailViewController: UIViewController {
init(story: Story)
}
The networking conundrum (7:10)
But, we have a slight problem here. We can’t use the story initializer like before, because when we’re coming from a push notification, we don’t have the story to initialize this view controller with. We only have the story ID.
The first thing that I tried was to add an initializer that takes a story ID.
class StoryDetailViewController: UIViewController {
init(story: Story)
init(storyID: String)
}
So this seems fine. And then, I started implementing it. The initializer with the story is still the same. The initializer with the story ID, how does this one work?
Unlike the case where we come from the stories view controller, which owns the list of stories, we don’t have a story to use to initialize all the properties. We can’t swap them out to nil, because they’re all non-optional. So, what are we going to do?
Well maybe we can load the story in the viewDidLoad()
. But then, we have to make everything mutable and optional. I guess we can initialize it, and make everything nil at first, and then in the viewDidLoad()
, load it.
class StoryDetailViewController: UIViewController {
let storyID: String
private var titleView: StoryTitleView?
private var senderView: AvatarView?
private var recipientView: AvatarView?
private var dateLabel: DateLabel?
init(story: Story) { /* same as before */ }
init(storyID: String) {
self.storyID = storyID
titleView = nil
senderView = nil
recipientView = nil
dateLabel = nil
}
// Load everything from API in viewDidLoad? 😰
}
This works. I don’t know about you, but this code doesn’t really suit my taste. Something just feels wrong.
Before, everything was non-optional and immutable. So, there’s only one way to configure this. Now, there are 2^4 – 16 ways to configure the properties. Actually, there are an infinite number of ways, since everything is mutable.
The solution (8:31)
I thought about it more, and this is what I came up with. We pull out the networking code into an outer layer, a parent view controller, called StoryContainerViewController
. You can initialize one of these with a story ID. And once you have the story ID in the viewDidLoad, we can use our API client and load it.
If it’s successful, we can create the story detail view controller, and add it as child view controller. If there’s an error, we can show some kind of an error.
class StoryContainerViewController: UIViewController {
let storyID: String
init(storyID: String) {
self.storyID = storyID
}
override func viewDidLoad() {
client.showStory(ID: storyID) { result in
switch result {
case .Success(let story):
let viewController = StoryDetailViewController(story: story)
self.addChildViewController(viewController)
self.view.addSubview(viewController.view)
viewController.view.frame = view.bounds
viewController.didMoveToParentViewController(self)
case .Error(let error):
// Show error
}
}
}
}
url_scheme://stories/12345
StoryContainerViewController(storyID: "12345")
Now, if we get a URL like this all we need to do is use the story container view controller, and it just works.
Protocol abstraction (9:08)
That handles the stories case. But, what if we need to handle other URLs? What if we had to show a user’s profile from a URL? Or, maybe we need to show a specific message from a URL. What if we wanted to make it a little bit more generic?
To do this, we define the new protocol: RemoteContentProviding
.
protocol RemoteContentProviding {
associatedtype Content
func fetchContent(completion: Result<Content, Error> -> Void)
func viewControllerForContent(content: Result<Content, Error>) -> UIViewController
}
This protocol has an associated type Content. And it specifies two things. First, how to fetch content. This is most likely an API request. And two, how to take that content, and convert it to a view controller to display. Then, we have a remote content container view controller, which is generic on type T, which is of remote content providing.
class RemoteContentContainerViewController<T: RemoteContentProviding>: UIViewController {
let provider: T
init(provider: T) {
self.remoteContentProvider = remoteContentProvider
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
provider.fetchContent { content in
let viewController = self.provider.viewControllerForContent(content)
self.addChildViewController(viewController)
self.view.addSubview(viewController.view)
viewController.view.frame = view.bounds
viewController.didMoveToParentViewController(self)
}
}
}
You can initialize one of these with a provider. In the viewDidLoad()
, we can use that provider to fetch the content, and then also use the provider to get the view controller to present, and add that as a child view controller. Using this, we can handle the stories case.
Let’s look at what the stories provider would look like.
struct StoryProvider: RemoteContentProviding {
let ID: String
func fetchContent(completion: Result<Story, Error> -> Void) {
client.showStory(ID: ID, completion: completion)
}
func viewControllerForContent(content: Result<Story, Error>) -> UIViewController {
switch content {
case .Success(let story): return StoryDetailViewController(story: story)
case .Error(_): return ErrorViewController(title: "Could not find story.")
}
}
}
In the fetched content, we use the client to show the story and get the story from the API. And then, in the view controller for content, in the success case like before, we get the story detail view controller with the story. If there’s an error, maybe we can use something like an error view controller.
Using this, now if we have to handle a URL scheme like this, we can create a story provider with the ID. Then, we can use the remote content container view controller, using that provider.
url_scheme://stories/12345
let provider = StoryProvider(ID: "12345")
RemoteContentContainerViewController(provider: provider)
We can do something very similar for the other two cases of messages and users. For user, we can use a user provider. For messages, we can use a message provider.
By pulling out the stateful networking code into a container view controller, we are able to prevent our detail view controller from turning into a monster where everything is mutable and optional.
Coordinators (12:00)
The next thing I want to share with you is the story of the independent cores and the connective shell. As a side project, I’ve been working on an app that helps me learn Dutch.
One thing that I’ve loved about this process is that I get to experiment with a lot of ideas that I’ve been interested in in this brand new codebase. One of those ideas is the coordinators design pattern.
I first heard about it last year at a talk given by Soroush at NSSpain. The main idea behind coordinators is that view controllers don’t know about other view controllers.
Let’s look at some code, starting with the app delegate.
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private lazy var applicationCoordinator: ApplicationCoordinator = {
return ApplicationCoordinator(window: self.window!)
}()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
window = UIWindow(frame: UIScreen.mainScreen().bounds)
applicationCoordinator.start()
return true
}
}
Can we first admire how small this AppDelegate is? The AppDelegate has a window as per usual. It also has an application coordinator that we initialize using the window. Next, all we do in the didFinishLaunchingWithOptions
is that we create the window, and call start on this application coordinator.
Before we take a look at the application coordinator, let’s take a look at the coordinator protocol.
protocol Coordinator {
func start()
}
I think there are different ways to define the coordinator protocol. The one I defined is very minimal. All it requires is a start function to kick things off.
Let’s look at what an implementation of a coordinator would look like.
class ApplicationCoordinator: Coordinator {
let window: UIWindow
let rootViewController = UITabBarController()
let wordsNavigationController = UINavigationController()
let phrasesNavigationController = UINavigationController()
let wordsCoordinator: WordsCoordinator
let phrasesCoordinator: phrasesCoordinator
init(window: UIWindow) {
self.window = window
let viewControllers = [wordsNavigationController, phrasesNavigationController]
self.rootViewController.setViewController(viewControllers, animated: false)
self.wordsCoordinator = WordsCoordinator(presenter: wordsNavigationController)
self.phrasesCoordinator = PhrasesCoordinator(presenter: phrasesNavigationController)
}
func start() {
window.rootViewController = rootViewController
wordsCoordinator.start()
phrasesCoordinator.start()
window.makeKeyAndVisible()
}
}
This is what the application coordinator looks like. It has a root view controller, which in my case, is a tab bar controller. Then, it has two navigation controllers; one for each tab, and two coordinators; one for each tab, which one of them is for words, and the other one is for phrases.
Next, in the initializer, we create the root view controller – that’s the tab bar – with the two navigation controllers. We initialize the two coordinators with the respective navigation controllers.
In the start function, we set the window’s root view controller, and we call start on the two coordinators. This is what the words coordinator looks like:
class WordsCoordinator: Coordinator {
let presenter: UINavigationController
private let listViewController: ListViewController<Word>
private let dataSource: WordsDataSource
init(presenter: UINavigationController) {
self.presenter = presenter
self.dataSource = WordsDataSource()
self.listViewController = ListViewController<Word>()
self.listViewController.title = "Words"
self.listViewController.items = dataSource.words
self.listViewController.configureCell = { cell, item in
cell.item = item
}
self.listViewController.didSelectItem = { item in
presenter.pushViewController(WordViewController(word: item), animated: true)
}
}
func start() {
presenter.pushViewController(listViewController, animated: true)
}
}
The words coordinator can be initialized using a navigation controller. We set the presenter. We create some sort of data source to load up the data. We also set the list view controller, which is what shows the list of words. And, maybe we set a title. We also set the items onto the list view controller so it can show things. Then, in the configure cell, we set the item in the cell.
So, for this list view controller, I read something on objc.io’s blog. They had a really cool blog post on generic table view controllers, or functional view controllers. To implement those, in the didSelectItem
call on the list view controller, we tell the presenter to push the view controller.
The idea here is that the list view controller isn’t pushing anything on its own. It’s asking the coordinator to present the view for it.
Within the start function, we call push view controller on the presenter with no animation. So, what this essentially does is it just sets the root view on the navigation stack. The phrases coordinator, it’s basically all the same thing. The only thing that changes is that the list view controller has phrases instead of words.
I totally recommend reading that blog post by objc.io if you’re curious about that part.
If you’re curious about coordinators, I highly recommend Soroush’s talk from NSSpain. It’s really awesome. It’s in Objective-C, but it’s still really really good. When you use coordinators, the backbone of your app becomes a tree of coordinators.
If we wanted to add a feature that adds a search functionality for words, we can create a new search coordinator, and have the words coordinator manage that. If we wanted to add logging in functionality, that’s also easy. We can add a layer right above the word coordinator and phrases coordinator, called dictionary coordinator. That basically manages the logged in side, which is the two tabs. We can have a separate log in coordinator that’s managed by the application coordinator.
If you want to add a sign up flow, we can add a sign up coordinator that’s managed by the application coordinator. As you can see, it’s really easy to add new features using coordinators. It’s pretty awesome to work with.
Writing Future-Proof Code (18:43)
Abstraction is one of the first concepts that we learn as software engineers. When I first learned about abstraction, I only thought about the interface. But, as I’ve started embracing Swift features, like value types, I’ve noticed that abstraction applied just to the interface is not enough.
In the first example with the story detail view, it wasn’t good enough to just add an initializer that uses the story ID. Sure, it was a simple interface. But, it introduced mutability into what would have otherwise been an immutable core.
That’s why we need to look a little bit further. I’m finding that it’s not good enough to just abstract things through the interface. We need to think about what parts of the app makes sense to be functional and immutable – in other words, solid.
Then, we need to step back and think about how each of those components interact with inherently imperative and stateful things like networking – things that are fluid.
Things that are fluid aren’t necessarily bad. In fact, they give our apps movement and life. The key, which I think is the most difficult part, is finding the boundary between what’s considered solid and what’s considered fluid. I think if we frame our thinking around this balance, we can write apps that are more resilient, robust and future proof.
I hope that you learned some new ideas today that you can start using immediately, or at least got a taste of what you might want to try in the future. Thank you.
Q&A (21:52)
Q: Do you find these ideas difficult to implement in a large codebase like Venmo’s?
Ayaka: For the coordinator’s pattern, I started using it in a brand new codebase. However, after I started using it in a brand new codebase, I also started using it in a Venmo codebase as well. So, I think the brand new codebase is where I experiment and try crazy ideas. If I like it, I try to introduce it into a production codebase, where I shouldn’t be experimenting with too crazy of ideas.
Q: You primarily focused on basic pushing and popping. How would you go about implementing popping an entire stack with this coordinator pattern?
Ayaka: I haven’t done that personally, so I would have to think of the specific case to give a more informed answer, but I suspect it’s possible.
Q: The coordinator pattern is similar to the wire frame part of the VIPER architecture pattern. How would this work with Xcode’s Storyboards?
Ayaka: I don’t use storyboards, because they inherently couple view controllers together.
Questioner: There is a library that uses storyboards but still manages to decouple the view controllers which is really cool.
Receive news and updates from Realm straight to your inbox