Swift Enums & State Machines
Christina Lee at Swift Summit in San Francisco, 2016
Christina: I would love to make a joke about saving the best for last, but I'm just happy that I'm not dead, so; yay flu season. I'm here.
Before we dive into the presentation, I'm going to be using sample code, and I just want you to see what the UI looks like, because without that the sample code is probably not going to make much sense. This sample app has a photo, and you can make tags, you can like tags, you can long-press, and you can delete them if you wanted to, and there are multiple egresses for making tags. Just keep this picture in mind as we go through this presentation.
All right. With that said, hello everyone. My name is Christina Lee. I am an engineer at Pinterest, and I'm here to talk to you today about Swift enums and how you can leverage them to build simple state machines.
A little bit of background. I did do work in this area before; specifically, I did it on Android. The reason I've been working in this area is because I feel like we have so much to learn from Web. Web iterates incredibly fast, and I think that we, as native developers, I'm a native developer, has a lot to learn from that. In particular, I'm very fond of Redux and Cycle. I think that they do a lot of really good things around state management. We wanted to bring these into some of our own apps, and you may ask: why? I have entire presentations dedicated to exactly why, but suffice to say that if you find me trustworthy and you feel like believing me today, these three things are top of the list: They're easy to reason about, they're easy to debug, and they're really, really easy to extend, which any of you who have used React on Web probably already know.
This sounds great, like Hooray, let's just go all in on that, but, of course, the downside is that when we wrote this the first time, not in Swift, this was in Kotlin, it was really complicated. I was giving presentations on this, and if you go back and watch any of them, you'll hear over and over that I say one of the cons of this approach is that it's really, really hard to onboard your team because it wasn't easy to use. We had really good results nonetheless despite it being complicated, and when we decided to rewrite this in Swift, we realized we had a second chance to do it better and to simplify it. So we experimented a little bit. We tried to make it lighter weight. We tried not to rely on outside files as much, and, spoiler alert, you've already seen the title to this talk, it turns out all I needed was Swift's very powerful enums and some reactive streams to pass them around in. With those two things, I had everything at my disposal.
A little side journey into enums. As most of you are aware, Swift enums are particularly powerful. Like most enums, you get the warnings, the compilation errors if they're not exhaustive, which is very useful, but you can also do things like write funcs for them, and you also get to define associated values. I can't overstate how important this is, because many languages don't let you use variable associated values. This particular property of Swift enums is what allows you to use them to make state machines.
Another thing about Swift enums is that they act a lot like classes, and here are just a few ways that they mirror class-like behavior. You have computed props, you can do things like conform to protocols, have instance methods, etc., etc., but, of course, we're not using classes for a very good reason, and that's because enums give you a type-safe way to coalesce your ideas. If things are related, this gives you a typed way to say that they're related. This is not mind-bending. Everybody knows this fact about enums, but it's very, very important when you're doing state machines, because states need to be related. Also, the compilation warnings make sure that you don't forget anything in a state machine, which is obviously very important. You don't want anything falling through the cracks. There's built-in casing logic, which seems minor, but having switch statements for enums will definitely change your life. If you try to do this without it, your if - else statements are going to get out of control.
All right. Swift enums are great. We've established that, but what you're really here for is how we can use them to make state machines, so let's dive into that. First, I love this area. I spend my whole life in it, but if you are not one who sits on the couch on Saturday and thinks about state machines, we might want to just go over the basics.
What do we need for a state machine? It doesn't take Einstein to know that the first thing we need is probably some state. What's important about this, though, is not that we have state, but that the state can exhaustively define all aspects of our app. When you define the state object, you need to be able to render every part of your app from that. It needs to be exhaustive. I just brought up the concept of rendering, and this is really important, because if we have state we need some way to take that state and tell the UI how to render itself. Then, last but not least, we need some way to go from one state to another state. If we have a single state app, nobody's going to use it. That's the most boring app of all time.
Yes, I know. I would try it, but I think that you believe me. With these three things, we now have a state machine. With that, I'll go into how you can build this in Swift. The first thing, I'm using simplified code from a real production app, and what we're doing here is we're defining a TagModel, and it has a single field in it, which is a driver of states. Now if you don't know anything about reactive streams, that's fine. You don't really need to know about drivers. Just replace the word observable here, or even just stream. The reason we use drivers is that they have nice properties around error handling and being called on the main thread, which for various reasons is good for UI.
We have some stream of states, and what does that state look like? For that app that I showed you at the very beginning, we only need these three fields. If we have these three fields, we can render any different UI that we need. Here we have some enum, which is the tag view controller mode; we have a photo ID, because, of course, if we're reporting something back to the server we need to have some unique identifier; and then we have a dictionary of tags which are currently being displayed.
Okay. Diving deeper into what that VC mode is, we have four enum cases here. The last two are not very interesting. You have none and you have deleting - those are kind of self-explanatory. The first two are where Swift enums really shine, because, of course, you can see that they have payloads to them, and they don't have the same payload. For instance, when we're creating a tag what we care about is the text of that tag and the location at which we created it. If we're tagging and mentioning somebody, this works the same as you would for Twitter or Facebook. If you start @ mentioning someone it filters through your contacts and shows you an ever-decreasing list until you get to the one you want. Here, we not only care about the text and location, we also care about the search query. Together, these four states are all the different states that our VC can transition through, and they cover all the cases for our app.
Side note, I'm glossing over a lot of enums that hide under this. For instance, you'll see here that we have a tagViewLocation, that's an enum. In this one we have tagViewData - it contains an enum. I'm staying at this top level just to make sure that it fits in this presentation, but this is all in a Playground that's on GitHub so you can go poke through the rest of these as well.
That brings us to the second step. We need to have some mapping from the state that we've defined to our UI. A lot of people will say that this is a view model. I like to stick with mapping because I think it scares people a little less. All right. We have this code. This our view controller. We have a viewWillAppear. What we're doing is we have the model that we just talked about. This model has a stream of states. Okay, that's it. One line. Then that last line there is bind, which should be self-explanatory, but we'll get into it later.
The middle line is where we're at right now. What we're doing in that middle line is we're taking the model and we're converting it to a view model. We're saying tagViewModel.make from this model. Again, the reason we need to do this is because the state exhaustively defines all the different ways our app can present itself, but it doesn't define how that happens, and that's what we need in this step. Here, you'll see that we have exhaustively listed out all of the different properties that it is possible to change in the UI of that sample app that I showed you. It actually doesn't look like much, and it isn't. It's only about 10 fields, but between all of these, between the text and the text box, the is hidden property on the background view, some other is hiddens and tag creation containers, all of these sum up to all of the different UI states for our app. You can see that they're very simple types, string, boolean, etc.
What does this mapping look like in practice? Here's one that I pulled out. This is for the tagTableQuery, which is when you're @ mentioning someone. This is the search filter for your contact. If you start typing CHR it's going to bring up Christina, Christine, etc. In this case, what we're doing is we're taking the current state of the app, which you can see on that top line, model.map state. Then we're doing a simple switch on our enum cases. Here the state.mode, if it's equal to .taggingAndMentioning, then we're going to go ahead and return the search query as the tagTableQuery. For any other case in this enum, we're not @ mentioning someone, so we don't care about it, so we return an empty string. We do this for each one of these that I've shown here. We take the state and we map it to what this individual property should be for that state.
This brings us to the third step. State changes. I call it the third step, but really there are kind of two parts of this. One, we need to know when the state should change, and then, two, we need to know how it should change, and those are very separate things. On the part one of this, ‘knowing when to change’, I alighted some of these fields earlier, but this is actually the complete code for our model. We have that same driver state at the top, and now we've added intents for both our custom tag views, because you can do things like tap on them to like them, but also the VC as a whole because if you long press on the VC it pops up a tag creation view. We need to have intents from both of those in order to react to them appropriately. Here, this is the complete code. We can pass these intents in the init, and then we're doing something with them. That's how we know when. Any time the UI is tapped, it will fire that event on an intent and we'll be able to listen to it here.
All right. That brings it to ‘how we listen to it’. This top line of code, not very interesting. We're using some sensible defaults, so an empty dec, setting it to nil, setting it to none. Okay, boring. What's really interesting is the last two lines here, and that's because we're getting an intents from the UI, which is saying, "Someone tapped on a tag, someone tapped on the VC, help, let me do something." What we're doing is we're turning that in to actionable data via reducers. This line here is where the magic is happening, and it's really important to look at the signature here. We have some Observable, which, again, is just a stream, and what it's passing around is functions. You can see that the signature here is; it takes a state and it returns a state. So these are all functions.
Why do we need these? Because a tap doesn't tell us anything about how a state should change. It just tells us that someone tapped on a tag. That doesn't tell us anything about state. We're getting UI events, but what we really want to know is what our state should be. This is the role that reducers play. They fill in that gap of linking what a UI event is to what a new state should be. This is why it's so important that the function signature is state arrow state, because as you can guess, the first state that will be here is our current state, and then the state that we return is going to be the updated one.
What does that look like in practice? Here's one of the reducers that we've defined in this code base, and it's the tagRemovedFromVCReducer. This is when you tap on the X, the tag goes poof, it's no longer on the photo. You can see that the function signature is here, we're taking in the tagId of the one that we want to delete, and we're returning some function. What does that function look like? It's kind of a lot of code on screen, but really what it's doing is it's checking whether the tag currently exists in our tag dictionary. If it does, it creates a copy of a new dictionary minus that tag, and then it recreates a state object. This is very important because a state object should always be immutable. You should never mutate the state object that's coming in. It needs to be immutable. The key here is that we're copying over every field that hasn't changed, and we're only adding in the new dictionary, which is the only field that's changed. Of course, if we try to delete a tag that doesn't exist, that's nonsensical, theoretically it will never happen, but we return some sensible default value, which is just the current state.
This is what a reducer looks like. This brings us to this point here, where we've defined enums that cover all different cases of our state, we've looked at how the view model can map those enum cases to the UI, and then we've taken a look at how the model uses reducers to update the enum cases as they transition from one to the other. But there's this missing step, which is what kicks off this entire state machine.
That's step 2 in this code here. If you look at it here, this is all of it. This is everything that's running the state machine. It's doing two main things. At the top we have an observable, and what it's doing is it's taking all of the changes that come in from the view controller and it's taking all of the changes that are coming in from the tags themselves, and it's just squashing them into a single stream. Of course, we don't care where the changes are coming from. We just want the changes. The first part, all it's doing is putting all of our changes in one stream. That's it.
The really interesting part, the part that makes this entire thing turn, is the scan. If you haven't used a scan function before, what it does is you start with an initial state, the one that we defined earlier, and then for every one of these functions that's admitted by the stream, we can apply it to the state and we're expected to return another state. Here what that looks like is this line. Remember that reducers are simply functions that take state and then return state. Of course, we can call the reducer with our current state and that's going to give us the updated state. This is what's driving the state machine. This one line. It's that simple. Just like that, you have a state machine. In these three steps and that one line. This drives the state machine.
Of course, I've skipped over the fact that you're binding. The reason I've skipped that over is because even though I could have thrown it in where I was doing the mappings and the view model, I wanted to save it for last because I think this is where the payload is. When you do this, your view controller becomes simple. All you need to do is take all those drivers that you've defined and bind them. Here, you can see that we have this line where we say, "Okay, give me the background hidden driver, and then bind it to the background view’s is hidden property." Shocking. We also have the text and we bind it to the text view. Again, really shocking. Our view controller becomes very simple. I've added a little bit of more fancy code, that text with resize in there, just to show that you can make custom bindables if you want to, but you don't need to. Our view controller implements this method, and all this method does is pipes the output to the actual UI components.
In total, this is what we end up with. We have the enums that define the states, we have the model that uses the reducers, we have the view model that tells you how the enums map to your UI, and then that very last step that we just added, the view controller- it's only job in this whole thing is to bind that data to the UI component.
There are three takeaways that I took from writing this in Swift. One is that all of the heavy lifting for your state machine is done in reducers. The reason this is really, really cool is because reducers are dead simple. The function signature gives it away. A reducer takes a state and it gives you another state based on an action. So anytime you write a reducer, the only thing you have to do is account for a single action on a given state. That's it. You're not going to be merging things coming from the server and from the database and all of this stuff all in one step. Those are going to get built up as you amit more changes, more reducers. In any one given reducer, you're making exactly one change.
Second, two of these four steps are completely wrote and simple. Which two? Well, the binding, obviously. You have some, like; background is hidden driver and you bind it to the background.isHidden property, so that's pretty easy, but also when you're mapping the state to the drivers this is just you looking at the spec that some designer gave you. This is pretty easy, too. When you're tagging, what should the background color be? Just look at the spec. It's really not complicated. All of the heavy lifting is done in that reducing code, and the code is easy and granular.
Last, this gives you almost all of the benefits of Redux and Cycle, but you can see I've given you all of the code here, and you can go poke around in the repo. There's not much more than that. With a few extra methods, you're getting the benefits of these really cutting edge web technologies on any Swift writing platform, whether that be server or native code. That's really, really cool. Now, that said, I have so much more that I want to say about this, but it won't fit in this talk, so if any of you are experimenting with this or want to talk to me about it, please do. You can find me on the internet here, but otherwise, thank you so much for listening to my talk, and I hope you have a good rest of your day.
Q1: How do you typically handle animations with state machines?
Christina: The answer is that there's no codified solution, so everybody kind of roles their own. There are two ways that you can do this. You can chase or you can proactively do it in code. This app, what it does is it doesn't actually animate. It sets the location properties instead of relying on built-in animations. I think that the general consensus is that you should just animate a property and then chase that animation without additive animations. For instance, start an animation off. If your state machine admits a new state that tells you to animate in a different direction, have an easing curve between the two and then go in that direction so it kind of sways back and forth as state changes. That's really hard to implement, which is why it's one of the problems that are kind of outstanding and not totally solved, but with this app we found that we weren't dropping any frames if we just did the animations by hand by updating state location.