Paul Stringer at Swift Summit San Francisco, 2016
I'm here today to talk about Storyboards Revisited. Thank you to Ida and Beren for inviting me to come and talk to you about this. I don't know if they put this on just after lunch because if there is any topic likely to divide opinion amongst our community it's probably this thing up here. Right? If conversation starts to drag a little bit, just bring this up. You'll get the juices flowing really quickly. Alright. Let's go back to basics.
What are storyboards? We all know what storyboards are, and there's a lot of great stuff written about best practices around this thing. We're not going to be able to go into all of them today. What we're not going to go into is some of the tooling things and some of the pain that you might have. There are some things we can control, and there are some things we can't control so we're going to try and focus on the stuff that we can control.
And one of those things I want to focus on in this talk is about capturing the flow of your overall user interface because that is something which I think storyboards do incredibly well.
Before we get into that though, I want to just introduce a concept or an idea which I'm taking from a book which I read a couple years back. They really just made these two things make sense to me. The concept is called meta-programming. It's in a book called ‘The Pragmatic Programmer’ that you may or may not have read. In that, in tip 32 or 37 in chapter 27, I think. It says this, "Put your abstractions in code, and put your details in metadata." So what's the relevance? What can you do with that? Well, they go on to say, "You may be able to implement several different projects using the same application engine but using different metadata." And what this is, this idea, storyboards are a realization of this concept, and this is kind of where it comes from. So I just wanted to put that in this background to sort of help frame and help guide us as we go on this talk about how things work.
So, Storyboards. They do some things really, really well. Less boilerplate, less of the boring code to write, less of it to maintain, takes care of that for you. An amazingly powerful tool for creating and previewing your layouts as we've gotten more complex and more devices and more sizes. It's really good to have tools that help us with that. It allows us to very quickly visualize the flow of our application and all this is being done in a place that’s as close to the domain as you can get. Where you can actually see it. Not abstracted away behind some generalized programming language, which isn't necessarily the best thing for visualizing your application. Alright.
I'd like to try and talk about this stuff. I've been working on an application, which we're going to use to demonstrate some of these things that we're going to talk about today. Now, this application helps you buy coffee in the morning, beat the queue with it. Still a bit of a work in progress. We've gotten a quite simple, pared back design for this so far. All you do- I love simple interfaces, so maybe this is going too far. Let's try this laser. Woo! There we go. Thank you.
So, you start off. You hit order, and up pops a list of available drinks. There maybe more on here, but we're going to start with these. You pick one of these. You get a summary of what you're buying. You get a chance to customize the drink. Then, when you're ready and you're ready to go, you can hit buy. Your order is sent. It'll be made for you, and all you got to do is pick it up and collect it. After that, you are done. I can't believe no one's done this. Alright. I think they have. I use it everyday, it's great. It's just a way to illustrate what we're going to talk about today.
So when I started developing this application, I started with a storyboard. Maybe a lot of us do that. It allowed me to visualize all those screens. Not in this kind of way where I had to launch the app to look at it, but I can see the entire flow of that application in one place. Right? I think that's quite convenient when you come to a new project or a team member comes into a project. This is very useful to be able to find your way around and figure out exactly where you need to go to make some changes that you need to do.
At this stage, though, we haven't really written any serious code, we've just got the framework of the application set up, so we show it to the team. One of the team members comes over and he says, "Before we get any further into this, this storyboard thing, I've read a lot of stuff on the internet about these things. Should we hang on a little bit before we go too far with this stuff? Is this going to be the right thing? I mean, I've read that this stuff wasn't really suited for complex applications or lots of screens, that it would quickly not scale to the problem and we should, just maybe, just not go down that pain or that path."
Some would even say, though, "Well, let's compromise and let's still use it for the visuals but don't use ... You know, don't make your storyboards too big. Just have one view controller per storyboard." It's like, "Hmm. That's an interesting idea." If you were to say that about a core data model, I think you'd give the guy a funny look. Let's have one core data model and one entity per core data model and have lots and lots of core data model files. That seems like we'd lose all the benefit of core data modeling, the ability to see all the relationships together. Put them all together. Understand how they all work together. That would be useful, and we would lose all that if we did that.
Why would experienced, talented developers sometimes say that? There's got to be a good reason, and I think there is. I think there are some good reasons why we should not be using storyboards. I think the problem, this is just my guess, but I think it's a lot to do with this. Segways, right? They're our problem. Not those Segways, but I couldn't resist, so there it is. It's these segues which we have a problem with. Okay? That kind of code smells funny, to most of us out there with a bit of experience, we'd be like, "This is not the kind of code we want to see in our applications". It's just already you can spot some glaring things that would be wrong with that and over time would be very hard to maintain. But this is kind of where storyboards sort of seem to suggest where we should go and how we should do things. So you see enough of this kind of code, and you go, “uh-huh let's not do any of this, I don't want to see any of this in our code base", fair enough.
Because the problems, there are kind of a couple, if you talk through them all, but effectively what it ends up doing is very, very tightly coupling your view controllers and all that segue code to your storyboards. Trying to keep those two thing synchronized is really quite difficult to do. We've created the opposite of what we tried to do in software engineering which is to create orthogonal designs. Where you can change one thing without having to change the other. What this gives you is the complete opposite, every change in a storyboard tends to mean you need to require to make some change in your code. This is a backwards and forwards, which, on top of everything else just makes you go, "We don't need this pain in our life, right, so let's just put them to one side. Nice idea for something simple but nothing for serious work that needs to get done."
But if we go back to the statement about meta-programming it goes on to say this, "Our goal is to think declaritively, specifying what is to be done, not how, and to create highly dynamic and adaptable programs. We do this by programming for the general case and we put the specifics somewhere else." Now the second part, that I've underlined, "put the specifics somewhere else", I think we've got that one nailed, we understand that thing. That's the storyboard, right? That's all the meta data about how it all looks and how it all flows in one place. I think it's the first bit that we haven't cracked yet, and it's possibly a more difficult problem that we need to solve, and that isn't solved for us.
So what would it mean to create highly dynamic and adaptable code, in relationship to segues and storyboards? I think those are just some things we would do to try to- we'd have to generalize all this stuff, try and get rid of all the switch statements, all the typing to do with identifiers, all that kind of stuff, and try and get all that code that's about that stuff outside our view controllers. Then we'd be able to make storyboards orthogonal to our code, which is the goal. So, let's dig a little bit deeper into the problem and how it arises.
This is your typical setup. You've got two view controllers, and they have to communicate with one another, because the entry point for the 'prepare for segue' method is the view controller. Now, maybe we can argue that wasn't the best design decision in the world, and it let us down this path, but I can't think of anywhere better they could have put it so, that's where it is and that's probably where it's gonna stay. But does that mean we have to abandon all this? No, because we tend to solve these problems- this is just software engineering, we do this all day long, so maybe we can apply some of that thinking to here as well.
What this creates is, typically, a very strong coupling to the next view controller. This would be typical code again, and we can see, just in this one, we've got three bits of information about the next view controller, that we need to pass all the dependencies and everything else that it wants. If we look at that as a diagram, this view controller, which really just wants to deal with it's own stuff, is now having to deal with somebody else's thing. Highly coupling it to that destination. You see how the problem quickly multiplies, you've got another view controller that also needs to go to that destination, he needs to do all the same stuff as well. So, the code- we tend to copy and paste, and we move it all around, and it just grows, the relationships get very hard to manage and deal with. This kind of stuff is enough to make you want to cry when you see it, right.
What are we going to do about this? How do we solve something like this? So we can continue to use storyboards and avoid some of those problems. Well somebody said- in fact it was David Wheeler who said this, "All problems in computer science can be solved by another level of indirection." Over the years I've just come to agree with this statement, even if it's not meant seriously. But it turned out somebody else added some other thing to this, there is a caveat, "except for the problem of too many levels of indirection", right? But in this situation, I think, that's not an issue. We have too little levels of indirection in between the storyboard code and our view controllers, so I'm gonna try and fix it by putting some indirection in there.
Instead of this, let's move those two to the side, and drop something in the middle. Now, for want of a better name, because I couldn't think of one, it's just called some coordinators thing, but we'll get more about what that thing is there. What that does is takes the responsibility away from the view controllers, and gathers all that up into a single place that deals with that stuff, so he's the one who knows about the view models and the entity objects, and your view controllers on the left hand side know nothing. They don't know about any other view controller except themselves, which is exactly the job of what a view controller's meant to do. Manage that life cycle of that thing but nothing else.
So, how are we going to get to that situation? What do we need to do to get there?
We need to create some kind of intermediary to co-ordinate these segues. We need to move responsibility for prepare out into this intermediary. We need to then figure out, for each destination that comes through the segue, what the right code is for it. That's normally where we get all the switch statements, and all that kind of stuff, all the forced casts. But we want to try and do this without all that stuff, because, remember, what we're trying to do is create an orthogonal design and that means things like that, forced down casts and the switch statements, they all smell of too much knowledge of how the thing works. We want to avoid those things.
Hmm. It kind of seems like that's a hard problem to solve, you've got to know stuff about this storyboard to be able to do this thing. So, I went away and scratched my head about this problem for quite a while before finding something else, but, where do you look for information about this? You go to the developer documentation, this won't tell you anything about how to solve a problem like this. You can go on the internet, this will just tell you, "don't use 'em", that's the best advice you're going to get. That doesn't seem very satisfactory.
Who would you call in a moment like this?
You call Crusty, right? This guy, if you don't know Crusty and haven't met him yet, go check out the videos at WWDC and search for protocol oriented programming, you'll meet this guy, find out what he's about. I call Crusty and I say, "Crusty, help me with this thing." Now, he's too busy, he doesn't really want to get involved in this UI stuff but I give him a sense of the problem and he mumbles something to me in passing. He says, "Double dispatch, pattern, visitor pattern", something like that, and I'm like, "What? ... Double what?" He's like, "Here, take this book." I'm like, "Books? That's an interesting idea. People read these?" Turns out he does.
So, I look at this thing, and I look at the index and I try to find something about double dispatch. What I find is this, buried inside some chapter about something called the visitor pattern. Now this is just one solution that I picked out, there could be many more out there. I'm sure there are, but this is just the one I'm going to use to illustrate the point that perhaps it's possible to create this highly flexible, cleanly architected system, even when you're still using storyboards. That's a description of it, I don't know what that means. I mean, I know bits of it, but this is the kind of situation where a diagram really, really helps, so let's get a good old sequence diagram up, and try to walk through this thing. Okay, I'm going to stand back here so I can see it too, because I forget.
The way this double dispatch thing works, which is really, really powerful, yet simple but, at the same time, really quite hard to get your head around it sometimes, is like this. In our segue, in the solution we're going to come to, this is going to happen. Our view controller receives 'prepare for segue', now instead of doing all the work himself, he's going to delegate that out to something called the visitor.
What the visitor does at that point, is he implements this double dispatch technique, which has been around a long, long time and it's in all programming languages you can use this. The way it works, at this point remember that the only thing we know about the destination is that it's a UI view controller. We've no idea what particular view controller it is, and that's normally where all the hard coded references to stuff happens. What he does is, he calls into this view controller, and hopefully it adopts his protocol, and he says, "Will you accept myself as a visitor?" If he does, he says, "Sure", and what he does is the second leg of the dispatch and he calls back, passing himself in as the exact type. At that point you know exactly what you're dealing with, you can choose the right code path, and do the right things on it that you want to, like passing data or dependencies.
Let's do that again but with view controller B, so another segue comes along, the same visitor. Again, at this point, we don't know what we're talking to, it's just a segue destination. We call into it again, he calls us back and he says, "Okay, it's me, I'm view controller B", you go, "Ah, view controller B, what you need is one of these things", give him one of those things. Really, really simple, but in that little dispatch thing that happened in the middle, let's go back, in this bit here, we took away all the ifs and all the elses, and everything else that we had to do to figure out what we were talking to and what we had to do. Just that little piece there. You could apply this, probably, to all other kinds of problems that you have, but it's useful in this one.
So let's go look at our app- oh no, right, shouldn't have gone back. Just in case you missed it let's do it another time. Go. You getting it yet? That just took a minute of my presentation I didn't need.
Okay, so we're going to go back to the application we're developing here, to try to illustrate how we're going to try and implement this with code, in our application, so you can see some of the magic behind the scenes of this thing.
Here's our application, if we remember it, what we do is we go from this screen where we select something. Select something ... there we go. When we do, it goes to the next screen, and it's passed some information to it about what was selected, there it's the drink, so there's communication happening between these two things. Let's try and refactor that to get some better code to do with this. What we want to do is get rid of that dependency there, we're going to break that and get rid of that.
Here's how we do it. We start by delegating all the responsibilities of segues to the visitor. We have to do this with a base class, which overrides the prepare for segue method once and for all for everybody, so this seems like a reasonable use case for inheritance, in this case, and I can't think of any other way to do it. In that method what we do is, we have this instance of the visitor, that can form some protocol, and we say, "Visitor, go prepare this segue. Go get on with it." Now, what is this visitor? We need one of those. The visitor, in that method that he got for prepare, he first of all checks, "Do you conform to some protocol which allows me to call this method?", and if you do, to do the first leg of the dispatch.
There we go ... and that protocol is there, you can see there. So then he says, "Oh if you do, okay." ... and then what we need to do is we need to add performer to this protocol to the view controllers that we want to deal with here. We are extending our order summary view controller, which is the thing that we're going to, and says "Yes, you accept visitors." What he does then is the second leg of the dispatch, so you know exactly what you're dealing with on the other side. As a result of all that, we just add one more extension using protocol extensions to do this, where we nicely extend that class there, or whatever it is struck probably, and we put all the code for preparing it in here, dealing with exactly this thing here that we know all about now. This visitor has now grouped together all this related functionality to do with preparing segues and at that ... and at that point the refactoring is complete, and we're done.
So, what has all this work bought us? We go back to the intent here, we looked at some stuff here, is this really necessary, is it really worth going down this path? Well, I think the best way to show this is with a demo of what you can do after some of these changes. What I'm going to do is- bear with us while we just need to jump out of here, change the display over so you can see what I'm doing, and I can see what I'm doing ... open display preferences, find my mouse, and mirror displays. Awesome, now I can see what's going on too. You can all see that? Excellent, cool. Alright, you can get that a bit bigger.
So, this is my storyboard, and we've had some changes come through. We've looked at using the app, we've had some user feedback, and it turns out there are a couple of things about it which the design guys think we should change. This kind of stuff happens all the time in your applications, right? Here's the flow of the application, and what we've decided we're going to do, is, we want to take this view controller here, which decides what size drink you want, and we want to push it into the flow in line over here, before you actually get to the summary of what you're buying. It turns out that people are missing that, and they're getting all the wrong sizes of drinks, and we're missing sales on ventes that we should be getting, so we're going to put that right into the early part of the process, so you've got to pick before you get to the checkout. That means I'm going to delete that segue there, because we're no longer going to go from here to this summary we've got of the information, so let's just try to delete that. Let's get this up so we can see what's going on, hide that thing, and hide that thing, oh, we'll need that.
Okay, and what I need to do is then bring this all the way over here. Remember the goal. Can we make changes to our storyboard without having to make changes to our code? We're making some ... quite invasive changes to this. What I need to do now is, I want this to now go to this as a 'set next' view controller, so, I'm going to create a segue from there to here, we're going to show that, all right. We already had one coming off the next one, this is called 'show next', I'm going to use the same identifier for this, because there are times when you still need to programmatically trigger some events off from your code, which is fine because it's nice to be able to drop into that when you need to. Then what we need to do, we can get rid of that segue now, we need to now segue from this one to here. Let's call this- let's give this the same name. Okay.
We've just gone in there and changed our application. Now, I said let's try and do this without any changes to the code at all, but there are- one place where we drop down into the code to be able to make a particular piece of behavior that we had in the previous version of the app, and that is where it was popping the view controller back to go to one previous, so I'm going to look for where I've got that line of code. Pop, there it is. This code is not in a view controller, it's in something which is dealing with all the communication of data, coming from all the different view controllers and funneling it all down into one single place. There's only one place to go and reason about this and figure it all out so you can understand it. Instead of this line here I'm going to steal this line here, so that when we've chosen our size of drink, which comes back through a delegate call back, we then perform the segue and go to the next screen when somebody taps. Instead of pushing back, which we were doing before.
Let's run it and see. Here it is.
All the same code apart from that one line we changed, now the flow goes something like this. Bring it over. Drinks, gonna peruse- I think right now I need an espresso, we'll go for small. Cool, espresso, small. Actually, let's change that order, let's go for just a drip, medium. Medium drip, okay.
It's all still working, still communicating, all the data's being passed around as it should do using dependency injection. There's no singletons here, it's not just reaching out to some global thing to get the data, it's all being passed through in all the clean, architected ways you would hope for. We can buy, we're done. Do it again. Okay? One line of code we had to change to do that, see if we can do anything more to it.
So, let's go back to our storyboard, and let's say again, we've got another change for the guys and they say, "Well, actually, what we'd like to do is, instead, put this one before." So, we want the size to come before we choose what drink it is. We need to go make another quite drastic change. We need to change the route view controller, at this navigation controller here ... and then we need to ... change this one here so that doesn't go to there but instead it goes to here. Now our flow looks a bit more like we've got this coming first, this is coming next, and then ... this should then go to this one. We've just shuffled everything around again. We need to make sure these new segues have that identifier, which is just the one thing that we've got left. Try and type these correctly, and run the app again. This time, fingers crossed, I think, actually, this works well, I'm going to treat myself to a large latte. Boom. Buy. Done.
That time, zero lines of code changes. Just to sum up what we've just done, before I get hauled off, okay. Summarize what happened here, we went for an orthogonal design, we tried to make these two things independent of each other. We still used storyboards and remained close to the domain that we're working in with flow. Meta-programming suggested we should move all these details outside of our code, but make our code more flexible so it can change independently. We found a classic design pattern, which gave us some great new dynamic flexibility to our type safe swift language, and now, changes in our storyboards don't require changes in our code, as much.
Just to finish up, I just want to leave you with one thought about this. Which is that some people will say storyboards are right for when you are doing very simple applications or prototyping, but I think the opposite is true. It's when we're building our most complex applications we need to rely on the powerful new tools that we have, to work with them to try and manage the complexity of the software that we're building these days. It's in the complex applications which we'd use these tools for what they're great for.
With that, that's the end of the talk, thank you so much for listening.