Conf42 Mobile 2022 - Online

Leveraging the power of State Machines in Swift

Video size:

Abstract

This talk teaches the attendee what state machines are, how to draw state diagrams, and then how to take those state diagrams and translate them into code.

It also touches upon how we can keep complexity manageable by creating state machines that can be composed together. This is a novel technique I have rarely seen discussed before, but it’s incredibly powerful.

Summary

  • Frank Corville is an iOS corporate trainer for a company called School of Swift. We deliver interactive workshops on all sorts of iOS topics. Most workshops are only half a day. Please reach out and tell me what you think of this talk.
  • In this presentation, we're going to talk about state machines in Swift. They're, of course, in our code, but also in our daily lives. We'll learn how to draw state diagrams, then we'll take those state diagrams and translate them into Swift. Why should you care?
  • The coordinator pattern was popularized by Sarosh Kanlu. It's my go to pattern for building anything in Uikid. But it has its shortcomings. How can you tell the order of the view controllers? How are you supposed to test this?
  • A state machine is a pattern to represent a finite number of states and enforce known transition between those states. Loading remote content is a great example of an implicit state machine that we have in our apps.
  • State enums are a natural fit for a state machine. These are the events that act on our state machine to cause the state to change. What should we do with an invalid state transition? There are several ways to fix this.
  • Let's create a class, and we're going to call it remote content state machine. This class needs to communicate with the outside world, the rest of our app. As the state changes are communicated through the state machine, the container controller switches between different view controllers. This allows for each of these child view controllers to be completely self contained.
  • In some use cases, state machines tend to grow in complexity over time. If we want our state machines to remain easy to work with, they need to be composable. We need a solution that allows us to build big state machines out of smaller state machines.
  • Presentation ends with some examples of state machines that you may have in your application. If you want the sample code and resources for this talk, go to bitly state machines comp 42. Here are a few additional resources for learning more about state machines.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
Hi, welcome to my talk leveraging the power of state machines in Swift who am I? My name is Frank Corville and I'm based in trail, Canada. I've done iOS mobile development consulting for almost a decade, and now I'm an iOS corporate trainer for a company called School of Swift. School of Swift is an online organization that keeps iOS teams on the cutting edge. We deliver interactive workshops on all sorts of iOS topics, and most workshops are only half a day. This way you can ensure the continuing education of your team without jeopardizing their development schedule. Some of our workshop topics include getting started with voiceover, painless dynamic type Swift UI for UI kit developers, and getting started with async await. If that sounds interesting, check out schoolaswift.com and of course, please reach out and tell me what you think of this talk. You can find me on Twitter at frankacy, where my dms are always open. You can also email me at hello, frank courville.com. But keep in mind that being helpful on Twitter is my superpower. So if you want a quick reply, you know where to find me. All right, let's get to the good stuff. In this presentation, we're going to talk about state machines in Swift, what they are, how to build them, and why they're useful. We'll learn how to draw state diagrams, then we'll take those state diagrams and translate them into Swift, and we'll also see some advanced applications of state machines in our code. But perhaps most importantly, why should you care? What's the point of talking about state machines in the context of an iOS app? The reality is that state machines are everywhere. They're, of course, in our code, but also in our daily lives. Things like the turnstile you use to take public transit or the cruise control in your car are classic examples of state machines. And perhaps the best example, power of state machines is a traffic light, which we'll look at soon. But to drive the point home a bit further, in the context of iOS specifically, I want to talk about something seemingly unrelated. So just bear with me for a while. Let's talk about the coordinator pattern in Uikit. For those of you who are unfamiliar, coordinators are objects that control the flow in an app. This was popularized by Sarosh Kanlu, back when we were still all doing objective c. In a way, they're like a programmatic storyboard. They string together many view controllers and define the flows that a user can take through a series of screens. Now, as a freelance developer, I've worked on many different projects and I've seen this pattern again and again. It's my go to pattern for building anything in Uikid. However, it has its shortcomings. When you look at a coordinator class, there's always those questions that come to mind. How can you tell the order of the view controllers? There's nothing in the code that communicates those to developer. You kind of have to piece this together yourself. It's also unclear how each of these view controllers interacts with a coordinator. You often need to wade through multiple files in order to figure out what exactly is going on. And perhaps worst of all is, how are you supposed to test this? How do you ensure that your flow is working correctly and isn't going to be broken by future change? So with those questions in mind, let's look at state machines in swift so what is a state machine? Here's a good definition. It's a pattern to represent a finite number of states and enforce known transition between those states. There's a lot going on in this definition, so let's break it down. A finite number of states. We aren't trying to model the whole world, right? We have a constrained domain and we want to focus on our specific problem. Okay, now, known transitions. This means that we can assign names to them. We know which states can transition to which other states. Conversely, we also know which states can transition to which other states. We can make strong assumptions about our state machine and how it will behave. And finally, enforce. These transitions are enforced. There's no dynamic way of bypassing a certain transition. You can't, for example, reach into the state machine and set an arbitrary state yourself. If you want to move to a specific state, you must use the transitions that already exist. So let's look at an example of what we mean here. Let's look at the standard traffic light. We can define its states as red, yellow, and green. So here they are listed out in state diagrams. It's common to list out states using their names in circles. So that's what we did here. Now let's think about how we move between each of these states. Green can become yellow, yellow can become red, and red can become green. Again, we can represent these possible transitions by using arrows between the states. Now let's assume that inside our traffic light, we have a set of timers that determines when the light should change. Now we can use that to name our transition. So we have a state diagram from which we can glean the problem that we're dealing with. We can easily see, for instance, that our state machine transitions from green to yellow. When the green timer elapses. Cool. Finite number of states. Known transitions between those states. Exactly what we want. Now, this is fine and dandy, but you're probably not here to code traffic lights, right? You're here to build apps. Let's go through this exercise again with something a little more concrete. Loading remote content loading remote content is a great example of an implicit state machine that we have in our apps. In other words, we have code that acts like a state machine, but isn't formally defined that way. Let's see if we can break it down. When we navigate to a screen, we start in the loading state. We may encounter an error, which will push us to the error state. We can then hit a button to retry, and that moves us back to the loading state. Eventually, we receive data from the back end, but maybe it's empty. This brings us to an empty state. At this point, we could press a button to refresh our content and move back to the loading state again. And then finally we get a fresh set of data from the back end, moving us to the data state. Many of us have built something like this in one way or another in our apps without putting much thought to it. But this is those perfect example of an implicit state machine. Now, how do we take the state diagram and translate it into swift? In my experience, the most effective way to build a state machine is in two parts. The first part is to create a state definition enum. This will represent the state diagram we just made. It will hold the different states, the different transitions, and the rules that govern how your values change from one state to the next. The second part of the solution is to create a state machine wrapper class. This class will wrap the current state of your state machine, protecting it from the outside, and ensure that only predefined transitions are called on it. This is also the class that will communicate new states to the rest of the app building. The first part, a state definition enum, is pretty straightforward once you have your state diagram. So let's start with that. Since we're dealing with mutually exclusive, state enums are a natural fit. Here I have my remote content state definition enum, and we're going to start listing out the possible states. Loading data, error and empty. In addition to these cases, let's also define our different transitions. These are the events that act on our state machine to cause the state to change. In other words, they're the labels we added to our arrows in the state diagram. Once again, since they are known and mutually exclusive, let's use another enum. Here's an enum called event that defines four transitions, did receive error, did trigger reload, did receive empty data, and did receive data. You'll also notice that our event enum is inside our state definition enum. Since it should only really be used here, it makes sense to scope it as tightly as possible. This is also important if you end up with many state machine implementations in your app. Now those fun part we define our state transitions. It looks something like those we start with a mutating function called handle event. When it receives an event, it's going to change the current state into the next state. In the body of this method, we're going to switch over two things, the current value of those state, which is self, as well as the event we received. Now those is a lot of code, so let's focus in on a single case. Here you can see we're switching over a tuple of self, which is those current state and event. And in this case, when we're in the loading state, we receive the did receive error event, we change self to error, which means we are moved to the error state and we simply repeat this logic for each of the transitions until every transition on our state diagram is handled. Now, if you were to try to make this compile, you would get an error switch must be exhausted. This is a good thing. The swift compiler is on our side. Great. Here are a few ways to fix this. You could exhaustively list out every state and every transition. This, although wordy, is a good solution for a state machine that may grow over time. This approach will force you to consider every other state or transition in case you add a new one in the future. Again, this approach is very wordy, but those composed has your back, so that's a good thing. Alternatively, you can use simply a default label to handle all unused combinations, which is what we'll do here. Now this brings up another question. What should we do with an invalid state transition? Again, you have a few options. One option, the one that most often comes to mind, is to crash to fatal error. This can be useful in very strict applications power of state machines, but watch out race conditions in your code, especially revolving around UI, could cause your app to unnecessarily crash. So generally I don't recommend those approach. Another option is to simply ignore the attempted transition and log an error. You could, for example, log it to crashalytics or some other remote logging database to audit how your state machine is behaving in production. But at the very least you should log an error message to the console so that you can see it happening during development. This will alert you that you have unexpected behavior that requires your attention. So that wraps up our state definition enum. Let's move on to the second power of state machines wrapper class. Let's create a class, and we're going to call it remote content state machine. It's going to contain a private variable called state, and we're going to initialize it to the initial state that we want our state machine to be in. So in this case, that would be the loading state. Next, this class needs to communicate with the outside world, the rest of our app. Now, again, there's many ways that you could do this. You could, for example, use combine rx, swift, or even a simple closure. However, in our case, let's go with the approach that most of us are familiar with, a delegate protocol. Here I define a delegate called remote content state machine delegate, and it has a single method did change state. Next, I add a weak delegate property to my state machine, and I'll use a did set to ensure that I never forget to notify my delegate that the state machine has changed states. Next, we need a way for our app to trigger these state transitions. To do this, we can write public methods on our state machine class to allow this to happen. For example, here I have three methods on remote content state machine receive error, reload, and receive data. But what do these methods do? Receive error and reload will simply pass on the event to our state definition enum in order for it to attempt to do its transition. However, receive data is a bit different depending on the data it receives, it decides which event to send to our state definition enum. If it receives an empty array, it sends the did receive empty data event. Otherwise, it sends the did receive data event. Your state machine class can be a convenient place to put light business logic such as this. You could also inject more complex validators into your state machine class if necessary. And finally, it's good practice to add a start method power of state machines class. Those will allow you to control when the initial state is communicated to the rest of the app through the same mechanism that we defined earlier. In our case, it's through the delegate, and now we can integrate this solution into our app. To do this, I've built a remote content container controller. It's initialized with both a network controller and our state machine. As the state changes are communicated through the state machine, the container controller switches between different view controllers. A loading view controller, an error view controller, an empty view controller and a data view controller. This allows for each of these child view controllers to be completely self contained and reusable throughout our app in other contexts. In order to hook up our container controller to our remote content state machine, we're going to make it conform to the remote content state machine delegate that we made earlier. Then using a few judicious helper methods, we can simply switch over those state that we receive and ensure that our container view controller is displaying the correct child view controller. This is also a good spot for you to trigger side effects. For instance, when we receive the loading state, we send off that network request and that's it, we're done. So what have we accomplished? We've taken something that was traditionally done in a single giant view controller and split it up into a bunch of small parts that are easy to reason about. And that's especially true for our state machine. It is trivially easy to test and ensure its correctness. What's more, we've taken something implicit in loading remote content, that is, the different view controllers, and we've made them explicit. This is a huge win for any project that wants to stand the test of time. Great. So before we dive a bit deeper into state machines, it's story time again. So a few years ago I worked at a large multinational on their Internet of things app. We had about a dozen different devices to support, each with their own custom pairing flows and UI. In this case, we did have a state machine to handle, but it was like 5000 lines long and incredibly difficult to modify without introducing bugs. The kicker is that we were adding new devices practically every quarter, so it became unmanageable quite quickly. This got me thinking about two aspects that hadn't crossed my mind before. First of all, in some use cases, state machines tend to grow in complexity over time. You could imagine that today you create a feature that only has three different screens. But as your product evolves, or as Apple adds new device functionalities, the number of states increases to something that's not as manageable. However, complex state machines also often have substate machines inside them. That is to say, you can extract part power of state machines and make it its own. To me, this reveals that if we want our state machines to remain easy to work with, they need to be composable. We need a solution that allows us to bring to build big state machines out of smaller state machines. So let's take our remote content example a little bit further. I find that data and loaded are two sides of the same coin and I want to be able to refresh data once it's loaded as well to either see new data or to move back to the empty state if everything has been consumed, how can I model that as a child state machine? Let's create a new state definition and list these different states empty data and refresh. We'll go through the same exercise creating an event enum as well. Once again, we implement the handle event method to mutate the state of our child state machine. So far, everything is how you'd expect, but here is where things get interesting. Now, in our parent state machine, we can remove the empty and data states and replace it with a loaded state. It's going to have an associated value, which is the current state of our child state machine. Now, in the parent state definition, all we need to do is clean up the handle event implementation in order to forward events to the child state definition, and we're good to go. If you're interested in seeing the actual implementation of all of this, I encourage you to download the sample talk at the end of the talk. All right, so let's finish up this presentation with some examples of state machines that you may have in your application. I find that those task of discovering implicit state machines in our code is one that benefits from lots of examples. So here we go. Those first is an onboarding coordinator. Imagine you start in a login state, and then you move to an app preview, and then maybe you're prompted to enable location and push notifications. When you hit the back button in one of these last states, you don't necessarily want to go back the screen before. Here in our onboarding state machine, we would handle this. Next up is form validation. Imagine a form field in a form that would start off in an empty state. As you type, it moves to an entering data state. Once you're done entering data, it would move to a validating state where it checks to see if what you've entered is correct. Then you finally end up in either a validated state or can error state, which can both transition back to entering data. And finally, let's look at accepting push notifications. Those one is really great. Too often when we want users to accept push notifications, we'll show some sort of priming prompt to tell them the benefits of push notifications. If they accept, then we can move to show them the system prompt, but if they decline, we simply move to a declined state and the app can move on. However, if they accept the system prompt, then we move to a push accepted state and that's it for this presentation. If you want the sample code and resources for this talk, go to bitly state machines comp 42. The sample code is really great, by the way. There are three different projects that pull straight from the things we talked about during this talk. Also, here are a few additional resources for learning more about state machines. They don't all necessarily take the same approach, but it's interesting to have a more global view on what's possible and the different patterns that are available to us. And finally, my contact information. Again, if you enjoyed the talk, let me know on Twitter. And of course, don't be shy. But if you are shy, you can send me an email as well. Thank you so much and enjoy the rest of the conference.
...

Frank Courville

Chief Instructor & Content Director @ School of Swift

Frank Courville's LinkedIn account Frank Courville's twitter account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways