Swift developers tend to favor a declarative programming style, but UIKit requires the opposite. UIKit mostly works on delegates and callback blocks that provide data to the framework and influence what is rendered on the screen. Forcing our Swift code into this architecture often results in code that is difficult to understand and maintain. What if we could turn this inside out? What if the view layer was simply a mapping of our data layer, and did not require delegates or callbacks?
Introduction (0:00)
My name is Benjamin Encz. I work at PlanGrid in San Francisco as a software engineer. For the last half year or so I’ve been active in the open-source community, working on ReSwift, a tool that allows you to manage state in your iOS applications.
What I’m talking about today is my love-hate relationship with View Programming. I got into view programming working on client-side applications because it was exciting for me to build something really fast, get it into users’ hands, get quick feedback, and to build something that is visual and that people can interact with.
But after getting my first professional job, I realized that working on UI code is, even on a professional level, a poor experience. The code is often low-quality, it’s not really fun to write, and it’s a lot harder than you would expect when you are not familiar with the intrinsic keys of how UI frameworks actually work. I also realized there are some real difficulties in view programming.
I want to address some approaches that we see outside of the iOS ecosystem today that might ease that pain. Before diving into my talk, I want to give you a little bit more context.
Bridging the gap: Swift and UIKit (1:32)
I’ve been working at PlanGrid for about seven months now. And as part of that, we’ve been doing a big refactor project. We’ve been rewriting major parts of our app, the synchronization code and the database code. We started at the business layer by replacing our Objective-C code, which was four or five years old, with Swift code because it was a good chance to do both things at once. After working on this business layer for a while, I got to the first view that I wanted to refactor - to write it in Swift, and make it fit into our patterns.
For our business logic, we adopted all the Swift paradigms. We had declarative code, used generics everywhere, and we had very functional code. Then I realized that this paradigm doesn’t fit together very well with the view program that we’re currently doing on iOS. A lot of this talk is really going to be about bridging this gap between the best practices in Swift and the best practices in UIKit.
I have more or less five different sections. I want to start out with talking about some of the issues in UI programming, just to put it into context. Sometimes we get so used to it that we forget that a lot of UI programming we do is pretty painful. Then I’ll talk about imperative versus declarative UIs.
The imperative/declarative discussion has come up a lot since the release of Swift. I think most of you will be somewhat aware of the discussion around that, and I want to talk about what it means under the UI layer. We’re going to take a glance at the state of UI programming in UIKit and where it fits into these two paradigms. I’ll also share some practical examples.
One thing that we’re using in production today at PlanGrid is a custom wrapper around some of the UIKit APIs.
Messy UI Code (3:21)
So let’s dive into complaining about UI programming. Who has seen this error message before?
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 2 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved in, 0 moved out).’
This is a very simple example of something that can go wrong when programming UIs, and some of the complexities that are involved. What happened here is that I tried to delete a cell, but I invoked two methods in the wrong order. What I should have done is first update my underlying data models, so that the table view data source is updated. As a second step, I should have called the table view and just told it that a cell had been removed.
If you do that in an incorrect order, you get this very long error message. Most iOS developers run into this error at some point in time. It’s easy to fix, but it’s a simple example of some of the complexities that we have to deal with when working with UIKit.
We have to understand in which order we have to call certain methods. We have to understand the mutation interfaces for all the different UI components. And it turns out there are some frameworks out there that make that a lot easier.
Interactive UIs with Imperative Code (4:29)
In more general terms, if you want to create interactive UIs with imperative code, first you need to build an initial UI. You can do that in the interface builder, or you can do it in code, and that step is pretty declarative and easy to follow.
First, you set up some constraints. Then you set up an initial layout and get your result on screen. Up until this point, everything is fine. Most of our iOS apps are unfortunately interactive, which is good for our users, but bad for developers. Some will update the app state, either for network requests or for interacting with the UI. Then we have to update the UI to actually reflect the state change.
This is where a lot of complex code comes in, figuring out how do we go from this initial state to the updated state. What are all the UI components we have to update? How do we have to update them and in what sequence? This code is, on average, very hard to test because it requires the actual UI to be in place and then to mutate it, and then to check if the UI updated. You can do it with UI tests or snapshot tests, but it’s pretty complicated.
As your UI grows, so do the many different ways to update UI components, and the code gets really complicated. It has a lot of different execution paths that you can’t really follow anymore in a large codebase. That’s how we end up at the point where we all have this feeling that yes, UI code is not really great, but it’s a part of the misery that you have to go through if you want to build a great product.
Just like UIKit, it’s kind of broken code, but it’s UI code so we have to live with it.
Imperative vs Declarative View Code (6:09)
That brings us to the comparison of the two paradigms, imperative versus declarative programming and what it means on the view layer.
As an example of declarative view code, I have to go outside of UIKit because UIKit APIs are all imperative. However, there’s a good example on the web, in the form of React, which Facebook uses as a web frame root to build UIs. And their approach is purely a declarative one.
The idea behind React is you have a declarative view layer, and that means instead of describing the initial state of your view and state updates separately, you have one uniform description of what your UI looks like. That uniform description is based on a state. All of that UI programming happens within one place in each app for each view component, and that is the render function. The render function gets some state that you want to draw on the screen as an input, and based on that it produces an output of what should be displayed on the screen.
The very simple example here, that doesn’t really require JavaScript or HTML knowledge, is to display a timer on the screen where we say how many seconds have elapsed since the program has started.
render: function() {
return(
<div>Seconds Elapsed: {this.state.secondsElapsed}</div>
);
Imagine implementing that in UIKit. You would set up a timer source. Next, you would create a label and put it on the screen. Then you would have some callback, and every time the timer updates, it would get called. You get a reference to the label, and then you update the label text. You have two code paths. One for the initial setup, something for you to load, or in Storyboard, and you have a second code path inside of the callbacks where you mutate the existing view.
React takes a different approach where you only have one render function for the initial rendering and any subsequent updates. The render function is the only place that will describe what the UI looks like. So whenever the timer fires, the render function will be called, and it will emit a new description of the view tree that will contain the new state.
Now the trick here is that under the covers, what React does for us is find UI components that are already on-screen. In the case of React, it’s the HTML DOM, and it updates these elements instead of redrawing the entire screen. So it’s doing a lot of complex optimizations behind the scenes so that we don’t have to mutate the existing UI components. We can pretend that we’re redrawing the screen on every single render update, which makes our code a lot easier.
What is also really cool is that as I mentioned, this is a render function, which means it has an input and output. The input is a state, and the output is the view tree, and the view tree is not actually DIV elements that you can render on-screen, even though it looks like it here. These DIV elements are just descriptions of UI components. So it’s very easy to run tests against these functions. You can just pass in a state and you get back a description of the view layer.
If you want to visualize these two approaches side by side - what imperative and declarative view code looks like - you might come up with a diagram like this. On the left-hand side, you can see what UIKit does. It’s imperative view code, which means we have different kinds of events that happen, and these call into our code, and then we have different code paths. We make decisions based on state.
So for example, when we get network requests, we might check if the response is valid or not. Then we have this triangle, which indicates a branch, and then at some point, one of these branches ends in updating the UI that is here, referenced in purple.
So we have different code paths, each of them references the UI and updates it in some certain way. In a declarative approach, we have no code paths because we don’t have any state outside of the render function. We only have an input to the render function and an output of what the view should look like. There’s no intermediate state, and there are no different execution paths. So it makes it, on average, a lot easier to reason about this code, and also a lot easier to test.
If you want to imagine understanding a very complex UI with the declarative approach, you can imagine having one render function, where you can see exactly how each view component depends on the state. There is a very clear mapping between them. Whereas on the imperative side you would just have a bunch of code that is referencing a view, and you have to build a mental model of which code paths happen in which sequence and which state impacts UI and in which way.
To go into a little bit more detail, the imperative approach - on average - is really hard to reason about as your UI gets very complex. It’s also hard to test because, in the very simple example of the timer, you have to test the side effect of when the timer fires, your method gets called, then it references the UI, and then it updates the UI element. That is very difficult to test because you can’t isolate the individual components.
Another thing that imperative UI programming does is it defers complexity to the user of the framework. So we saw the two examples. The one is the table view UIKit API where we have to understand in which order we want to update a model and which order we have to update a view. We also have to understand how to update each of the individual UI components.
For example, a label updates differently than a collection view, which updates differently than the table view. A developer has to understand all of that. And it also encourages coupling. You can easily couple business logic with your view code when you write only simple imperative code in order to update both the view and to call into network requests or a database.
So it’s very easy to have one callback that doesn’t only update a view, but also has some side effects that affect your business logic or your state. That makes your code, on average, also more complex. That also means that a view code can have arbitrary side effects. In a large codebase, it can be very difficult to understand which code is just updating the view and which code is having some side effects like network requests.
The Declarative Approach (11:42)
The declarative approach solves some of these problems. Firstly, we have a very simple and isolated concern, and that is mapping the state to a view. So we get the input, it’s a state, and we map it to a certain view tree. This function does nothing besides that. There are no side effects, and it can entangle itself with our business logic.
It’s also easy to test. We can provide an input, and we can assert an output. So if we want to test the timer view and make sure that it prints the label correctly, we pass in the amount of seconds that have passed, and we assert on the view tree and assert that it has one development that has a label with a certain value.
This also defers complexity to the system - to the framework - instead of to the user of the framework because we just described what the final view should look like, and we don’t care at all what happens under the covers, to actually update the existing UI officially, to reflect this new state. We get the luxury of just describing what we want to see on-screen and a framework. The well-tested framework ensures that that state update works correctly. This makes it a lot easier for us as developers.
So where does UIKit fit into this? You can write declarative view code with UIKit if you have static views. If you set up constraints, you define them once, and you don’t have to lay out the views manually. It’s a declarative approach to describing what a view should look like. But for any kind of dynamic content, it gets very complicated, and APIs are definitely not declarative. You have to understand which views you have to talk to, and which methods to call.
As a concrete example, I want to dive into the UITableView API, because that’s the first one that I wrapped when writing this custom wrapper that we use at PlanGrid today. And one of the things I didn’t dive into yet is that this declarative view approach also solves a problem of redundancy. We have a very data driven app that’s an enterprise software product, which means a lot of our views are table view, and we probably have more than 2,000 implementations that all look very similar, and that all produce redundant and not tested code.
Let’s dive into one example of the UITableView
API that is a bit problematic. In this example, what we’re trying to do is figure out if a particular row inside of a table view can be highlighted or not. There are a couple of problems in this piece of code.
let userSection = 1
let storySection = 2
let settingSection = 3
func tableView (tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let tableViewCell: UITableViewCell
switch indexPath.section {
case userSection:
tableViewCell = tableView.dequeueReusableCellWithIdentifier("user")!
// ...
case storySection:
tableViewCell = tableView.dequeueReusableCellWithIdentifier("story")!
// ...
case settingSection:
tableViewCell = tableView.dequeueReusableCellWithIdentifier("setting")!
// ...
default:
fatalError("No cell for indexPath: \(indexPath)")
}
return tableViewCell
}
override func tableView (tableView: UITableView, shouldHighlightRowAtIndexPath indexPath: NSIndexPath) -> Bool {
switch indexPath.row {
case UserAction.Status.rawValue:
return self.canChangeStatus || self.canClose
case UserAction.Assignee.rawValue,
UserAction.Phone.rawValue,
UserAction.Title.rawValue,
UserAction.Description.rawValue:
return self.canEdit
case UserAction.Shared.rawValue:
return self.canShare && ! self.allUsersShared
case UserAction.RemoveMaster.rawValue:
return self .canUnshare && ! self .allUsersNotShared
case UserAction.Archive.rawValue:
return self.canDelete
default:
return true
}
}
In this example, we have a piece of code that has to understand for each cell that is inside this table view, if it is highlightable or not. For that to happen it has to understand, what is the type of cell, and also the business rules for each of these individual cells. It’s kind of an inverted API where the table view has to have the understanding about all of its cells, and that also tightly couples the cells to this one table view.
One of the things I realized when looking at our codebase is how hard it is to reuse one cell in a different table view, because all the business logic for that cell is not in the cell itself, it’s actually in the implementation of the table view data source and table view delegates. It is spread out throughout the entire codebase. So that’s just one of the problems we faced. And especially when looking at our new Swift code, we got to the stage where we had one state object per screen, and that state object would perfectly describe what we wanted to render on-screen.
So a simple example would be a list of users. We’d have a nice Swift array with five structs that are users, and we want to render them on-screen. But to make that happen with UIKit, we would have to break the struct up into a description that UIKit can understand. So I realize that we take this declarative description that really fits the Swift paradigms, and we’re breaking it into a sequence of imperative statements so that UIKit can understand our intent. That was the point where I realized: maybe we can wrap this into an API that works better with Swift.
Turning UIKit Inside Out (15:40)
That kind of loops back to the title of this talk, Turning UIKit Inside Out. When you look at UIKit today and all the APIs we’re using, you see something that looks like this on the left. You have your app, and it has some data, but it doesn’t really have the chance to say “I want to render something on the screen,” and then pass a block of data to UIKit. Instead, we have APIs where UIKit defines a product call, which means it’s going to define how it’s going to communicate with our app, and then UIKit calls methods subsequently for us to provide data.
The table view data source and the collection view implementation are two classical examples where I, in some cases, already know exactly what I want to draw on the screen, but I have to wait until UIKit asks me to provide the data. That makes our code sub-optimal in some cases. But I wanted to see if we can get to a state that you can see on the right side, which is a lot more like React, where we have an app with some data in there, we have some states, and we want to render them on the screen. All we have to do to accomplish that is to pass the list of data that we want to render to a render function or a render component. From there, this component could take over the communication with UIKit. Inside our app, we could have a very simple API that we can use, and we can reduce a lot of the redundant code that we have in all these implementations of our protocols.
So the first approach was building our custom declarative UILayer on top of some of the UIKit components. Before we decided to create our own, I also looked at some of the existing solutions. And you might have heard of some of these.
I think React Native is probably the most popular one right now. React Native is bringing the idea of React to native platforms, currently iOS and Android. It does that by using native components, but building a JavaScript API on top of it. So you can write your view similar to the example that I showed you earlier, and it’ll actually render native UIKit components. But React Native is still pretty immature, although it’s moving very fast. It’s a pretty big dependency to take on, and to build a company upon. So we decided that it’s not something we can use within our app. However, if you’re building a small app, then it’s definitely worth trying out.
Another open-source project by Facebook is called ComponentKit. ComponentKit is a native API that I built for the News Feed on the iOS app. The downside of ComponentKit is, firstly, it only supports CollectionView based APIs. So it’s very powerful for building something very similar to Facebook’s News Feed, but it won’t work for a lot of other types of UI components. Secondly, it’s implemented in Objective-C++, which means it doesn’t work with our Swift code. That was the reason we couldn’t use it.
Another cool project is by Josh Abernathy, who works at GitHub, called Few.swift. He was also really excited about this idea of a declarative view layer and the program model that React brought to the web. He wanted to see how it works with Swift and UIKit and AppKit. I would recommend checking it out.
Do It Yourself (18:46)
So the conclusion was we don’t have a production ready solution. We have to build something ourselves if we want to support this. The goal that we have in mind is to get to a place where you can get a function or some component, which then takes in an app state and returns a description of the what the view should look like, because that fitted really well into our new architecture.
I want to show you a concrete implementation of wrapping the UITable to an API. You can see in the diagram that the flow starts on the left-hand side with the ViewController. The ViewController gets a reference to the state, which is highlighted in yellow. The state encapsulates everything that we want to draw on the screen. We already decided that on the business logic side that there is just a very simple Swift data structure. Now we have another component called a view model provider. This view model provider is equivalent to the render function in React. It takes in the state of what we want to draw on the screen and returns a description in terms of view models that describes what the view should look like.
In this concrete example, these view models are called table view model, section view model, and sale view model. Then, you can map the table view components to the UI. These descriptions are produced in this pure function, which we can now also test. These view models are passed through this API wrapper that I call the TableViewShim
. It implements the UITableViewDataSource and the UITableViewDelegate in terms of the data that it has received in this description in the view models. It has one implementation that works generically based on the view models that are passed in. This component takes over all the communication with the table view, so our view controller doesn’t interact with the table view directly anymore. It just says “render this list of objects on the screen.” So as a concrete example, very simplified, let’s say we want to render a list of users on the screen. Our state would be an array of users that have usernames.
The setup would look somewhat like this:
var users: [User] = [
User(username: "A"),
User(username: "B"),
User(username: "C"),
User(username: "D")
]
override func viewDidLoad() {
self.tableViewRenderer = TableViewShim(cellTypes: [
CellTypeDefinition(
nibFilename: "UserCell",
cellIdentifier: "UserCell"
)], tableView: tableView)
self.tableViewRenderer.tableViewModel = tableViewModelForUserList(
users,
deleteClosure: deleteUser
)
}
We have a view.load
method. And inside of there, we create a tableViewShim
object. And this is, once again, the API wrapper that implements all the difficult parts of UIKit for us. We initialize it with CellTypeDefinition
s so that it knows how to load the individual cells. And, in our case, these are Nib files and cell identifiers. Additionally, we passed in a reference to an existing table view. This wrapper doesn’t create a table view; it just implements the protocols for it, so we have to pass in a reference. Then, a second step is providing a table view model that describes which table view content this component should render. We do that by calling a function called tableViewModelForUserList
.
This is essentially the render function that we know of from React. We’re passing a state, and we’re expecting back a table view model, which is a description of what the table view looks like. Into this function, we’re passing in the list of users - the users array. We are also passing in the function called deleteUser
. This function will be invoked when someone swipes left on the cell and hits the delete button. So we need a way to capture these interactive events, and we’re doing that by passing in a function reference. So we pass in a list of users, and the closure that gets called when a cell is deleted. And from there on, this pure function calculates what a table view should look like. And here’s what this function looks like:
func tableViewModelForUserList(users: [User], deleteClosure: CommitEditingClosure) -> TableViewModel {
return TableViewModel(sections: [
TableViewSectionModel(cells:
users. map { viewModelForUser($ 0 , deleteClosure: deleteClosure) }
)
])
}
func viewModelForUser(user: User, deleteClosure: CommitEditingClosure) -> TableViewCellModel {
func applyViewModelToCell(cell: UITableViewCell, user: Any) {
guard let cell = cell as ? UserCell else { return }
guard let user = user as ? User else { return }
cell.nameLabel.text = user.username
}
return TableViewCellModel(
cellIdentifier: "UserCell" ,
applyViewModelToCell: applyViewModelToCell,
commitEditingClosure: deleteClosure,
customData: user ) }
)
}
Let’s first focus on the one on the top. This function is called tableViewModelForUserList
. It takes in the array of users, the delete closure, and returns the table view model. The first thing we do is instantiate the table view model struct, passing in an array of sections. For this example, we only have a single section. And for the section, we call map on the users array and call a separate function that produces the individual cell model for each of the users.
So what we’re producing here is a table view of one section and with one cell representing each user. Most table lists happen in the view model for user function. That’s again a pure function that takes in the state for one individual cell and returns the description for one individual cell. What we’re returning at the end of this function is a table view cell model that describes exactly what this table view cell should look like.
A part of that object is a cell identifier. The second part is the deleteClosure
function that should be called when this individual cell is deleted. The third part is the custom data that is used to display information in the cell. And the last part is a bridge between this model and the actual UIKit component - the actual UI table view cell. That is the applyViewModelToCell
function, which gets called when the UI table view cell is created on and displayed on the screen. We want to update it to reflect the latest state in our model. In this case, what we’re displaying is the username. So the only thing in this block that actually does any work is assigning the username to the name label text. With this description in place, we have a list of users on the screen without implementing UITableViewDataSource or UITableViewDelegate. We’ve just saved hundreds of lines of code in many of our view controllers.
Now, one thing I left out in this example is the interactive part. I showed you in view to load, we’re getting an initial list of states, and we’re rendering a state on the screen. But there’s another part where we want to delete a cell, update the UI, and update our state. There are two approaches to doing that. The first one is the generic, and what React does where we could say we’re just going to call this render function again. We’re going to produce a new view state, and we’re going to build some framework components that are going to figure out how to update the UI. I only had one or two days to work on this, so I decided that that would probably explode the scope of this initial implementation.
The Shortcut (24:52)
For the production API, we decided to take a little bit of a shortcut. That shortcut looks like this:
func deleteUser (indexPath: NSIndexPath) {
self.users.removeAtIndex(indexPath.row)
self.tableViewRenderer.newViewModelWithChangeset(
tableViewModelForUserList(
users,
deleteClosure: deleteUser
),
changeSet: .Delete(indexPath)
)
}
The deleteUser
function is called when a user is deleted. Instead of only updating the state and letting the framework figure everything out automatically, we’re adding two steps. The first one is updating a state by removing the user from the list. The second one is calculating a new view model by passing in the new list of users. But also, besides the new list of users and the new view models, we provide a hint to this table view component. We tell it what actually changed between the previous state and the current state.
If we had a full implementation, we wouldn’t have to do this. But it would mean that our wrapper component would be much more complicated to implement because we’d have to be diffing the previous and the current state. Here we’re basically telling the table view for this change state that a certain row has been deleted. This way it was much easier to adopt and put it into production.
For most of our table views, we got back from implementing UITableViewDataSource
and delegate from scratch on all of our view controllers, to actually having a very nice description that is concise and easily readable. So creating a table view would look something like this:
TableViewModel(
sections: [
TableViewSectionModel(
cells: users.map(cellModelForUser(actionHandler))
),
TableViewSectionModel(
cells: stories.map(storyModelForUser(actionHandler))
)
]
)
If we create a table view model, we create a table view section, and we create table view cells. We do that in a function that gets the state as an input. Another cool thing about that is now you can easily move cells from one table view to another because they describe the entire state as relevant to them.
I haven’t shown a full example, but we have very complex cells that make decisions about if they could be highlighted or not and if they can be deleted or not. And all the business logic is contained in one cell based on the underlying model state for that one cell. So we no longer have this problem that our view controller has all the code related to all the different types of table view cells, which means we can build and reuse components much easier. If you want to take a look at a full example, it’s on GitHub.
Demo (27:42)
It was really exciting building table views this way because I’ve been re-implementing them for years since I didn’t see a much better solution. I decided if we actually wanted to adopt this in our app as a pretty big dependency, then I should try to build it myself and get a better understanding of what goes into it. And I did that, and I would like to give you a little bit of a demo.
It’s not a very pretty UI, but it’s going to demonstrate two different things. Firstly, a very regular layout, and secondly, some state updates. So this here is the login screen. And if I try to login with an invalid username or an invalid password, then these text fields turn red and we see an error message that the username was invalid. If I choose Test
and Test as Credentials
and hit login again, then update it, we’ll see that we are no longer in error state. So it’s just a very simple example of a view transition. And all of this here that’s built on top of this experimental API is going to go into the code.
The second screen is a table view, and it is a little bit interactive. We can do some things like add items and delete cells. All of that by itself is not very exciting. But the idea of this is that it’s built on the React approach, and I’m going to show you the code in a second on what this means. So we have an interactive app for state updates and a declarative view programming API.
I’m going to first show you the login view. With this approach here, we don’t really have the need for view controllers. We can just use something called Component Containers. All that our controllers will ever do is implement a render function, like this one here where they take in the state for their component and where they return a description of what the views should look like. So for this component here, we have the login state that is actually driving what the view looks like. And here is an example of what you can store inside of the state. The login state, in this case, is username and password, and whether they are valid, which drives turning these text fields red or white. And you can imagine just throwing any kind of state for your view inside of this.
Going back to the render function, you can now see we have this one uniform function that describes what this view tree looks like for any arbitrary state. One example of how state changes are modeled in here is the background color for these text inputs. So we saw that it turns red if the username is invalid, and you can see that happening here on line 95 and 96. When I’m using the turning operator to switch over to state and say if the username is valid, then our color is going to be white. If the username is invalid, then the color is going to be red. So there’s just one of the examples of mapping the view state to the state of the component. When we actually want to set the state, I can go up to the login function to demonstrate that. Then all we do is update the state variables.
So inside of the login function, I have this very dumb login test that checks for these two constants. If these two constants are met then we’re just setting the username valid variable to be true and the password valid one to be true. This will invoke the render function again. The render function will run and will detect that these flags are set. It will now choose a different color for the text field. Then the framework itself will figure out how to update to a relevant UI component automatically. So our state updates are now just mutations of data, and our render function is just the description of state, or how it maps the views. This also carries over to the second view where we saw this table view list.
We’ll be creating a table view model that describes what a table view looks like. In this block here, it’s also again inside of a render function. Here we get in the user state, and the user state contains a list of users. We’ll be creating a table view model that has one section. And it has cells for each user, so we again are mapping over the list of users. For each of these users we’re calling cell model for user, which is just a utility function that is called from its render function, the basic rates of cells that represents an individual user.
Now the coolest part about this is how we can delete cells, and that’s happening up here in line 34 to 36. We have a delete row function, and all we have to do at this point is update our state. Get a reference through the user’s array and remove a user from this list. The state update will trigger the render function that will trigger the creation of a new table view model. And that table view model will have one cell less than we saw earlier. This will trigger an animated update of the table view automatically. So I don’t have to implement table view data source. I don’t have to update state manually. I just update a state itself, and the UI state updates automatically.
So that’s a very brief demo. It’s also on GitHub if you want to play around with it.
Implementation Details (32:30)
I mentioned “components”. Components is a term that Facebook has coined as part of React. Components are essentially just descriptions of views, and each component will map to one view. These mappings can be platform specific. One cool part about this is you can actually describe your view as a component trees, and you can imagine from Mac OS and iOS who have different views that match these individual components.
So instead of using a UI button, we use a button, and we’ll have different mappings for each platform. Another important part is that these renderers that I’ve written for table view models, collection views, and stack views, they understand how to update themselves with a new component state. So we have individual components inside of this framework that understand how certain state updates affect them.
For example, a table view knows that if a child’s component of it gets deleted, it has to call the cell delete row at the next path method. By doing that, we basically keep these animations that UIKit provides for us by using the correct APIs under the covers.
I also relied on a very cool library called Dwifft to implement this. The Dwifft library is the one that actually does the diffing. Basically, a library allows you to pass in two arrays, and it compares them and tells you if elements have been moved, inserted, or deleted. I can use that information within my library to drive the state updates.
Here’s an example of updating a table view and adding a cell. We start off with a table view of one section and one cell, and then we add a second cell to it. The first step that we have to do after that is perform “diffing”, to figure out what the difference is between these two descriptions of the UI. We find out that one cell has been added, so the differable identified is that we have to add cell number two to section number one to be able to update the UI to the latest view state that our function has described.
The second step is “reconciling”, a React term meaning we are bringing the UI up-to-date with what our model describes. So that means, in this case, we have to add a cell, which means we have to insert it into the UITableViewDataSource. We have to call the UI table view insert index path to actually animate this update. This code on the right-hand side, will be inside of different renderers that are mapping to each individual UIKit component. They understand how to update their state within UIKit APIs.
Conclusion (36:18)
So you can also find this on GitHub, it has a lot of triangles because it’s a weekend project, and it’s definitely not usable for production. It’s really just a proof of concept, and I wanted to play around and learn a little bit about how React works and see if we can use anything like that in the future.
In conclusion, I talked a lot about the idea of declarative view programming and the advantages of it. I wanted to recap that because I think it’s really important and has improved our code at PlanGrid a lot.
Firstly, the declarative view code that we write is really easy to reason about because it’s isolated in one function that does one thing, which is mapping the state to review description. That means it’s well encapsulated. If I want to understand how a view works, I have exactly one place to go. All the code that touches the view lives exactly there. I don’t have to scroll through my view controller and try to figure out if someone else might reference the view and update it.
It’s also testable, and we can easily write tests. You pass in a state, then make an assertion on the output of the description of the view tree, and you have almost 100% test coverage for your UI code as well, which is a pretty nice thing to have. And it also has removed a lot of redundancy. We had to implement TableViewDataSource
and delegate over and over. Now we have one component that encapsulates that.
One thing that is kind of difficult with declarative view code that I didn’t put in the slides yet is if you have very complex animations. Apple just now released some new, cool animation APIs. If you want to do really highly sophisticated animations, then this approach is not perfect. But for most apps, a lot of content is really mostly static and uses some very simple animations, and I think this approach works really well.
What does all of this mean? As I mentioned, I think a generic view layer that works declaratively just like React is very difficult to accomplish without Apple’s help. It doesn’t seem like Apple is going to move in that direction anytime soon. So I can’t really imagine that we’re going to see a Swift third-party library that’s going to be stable enough that you can use in production anytime soon.
What you can do is write wrappers around some of the components that you use in your app, just like we did with the UI table view. If you like this approach, I would really recommend trying it out and seeing how it impacts your code base and if you can get some of the benefits of this approach.
That’s all I have to say. Thank you.
Q & A (40:58)
Q: With this approach, how well do you think it works for a more complex table use? I think you were also touching on some of those points. For instance, if you have dynamic sectioning, things like drag and drop for your ordering, and still have pagination and placeholder cells for that kind of stuff.
Benji: I can only make an educated guess since I didn’t try it out. Reordering was one of the things I wanted to try out but didn’t get to. There are some examples out there that already do that automatically, but the coordination between animations and the state updates is really difficult. That’s something that the React core team on the browser, where they’ve been using it for years, are still trying to figure out what the right API is. If you have state updates that go hand in hand with very long running animations, that kind of stuff is difficult. But if your data model is the one that’s changing a lot, like adding sections, removing sections, these kinds of things are not too difficult. If you can implement it in terms of the delegate methods that we have today, then it should be fairly easy to adopt with this approach as well.
Receive news and updates from Realm straight to your inbox