스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Protocol and Value Oriented Programming in UIKit Apps
Building on last year's Protocol-Oriented Programming and Building Better Apps with Value Types sessions, this year's session will highlight tips and tricks for building better Swift apps. See how you can incorporate these design approaches into a real MVC-based Cocoa Touch app, especially in the view and controller layers, where you might not have thought of using these techniques before.
리소스
관련 비디오
WWDC16
-
다운로드
Good afternoon, everyone, and welcome. I'm Alex, and Jacob and I are going to talk to you today about how you can use value types and protocols to make your app better. And we're going to focus today on something called local reasoning. Local reasoning means that when you look at the code, right in front of you, you don't have to think about how the rest of your code interacts with that one function.
You may have had this feeling before and that's just a name for that feeling.
For example, maybe when you just joined a new team and you have tons of code to look at but very little context, can you understand what's going on in that single function? And so the ability to do that is really important because it makes it easier to maintain, easier to write, and easier to test, and easier to write the code the first time. And so we're going to talk about how local reasoning can improve our UIKit apps in the context of model view controller design paradigm, which is what Cocoa uses, where the model stores your data, the view presents that data, and the controller coordinates between the two. And so we need a real app to put our answers to the test.
Unfortunately, we actually had a problem at Apple that we needed to solve. So I'm going to tell you a little secret about how we plan WWDC every year.
Engineers have dreams and that's what they're dreaming about for what they're going to present. And so somehow we need to take those dreams and record them, but we noticed a lot of engineers don't actually remember their dreams by the time they get into work. They forget them.
So Jacob and I developed this awesome app to do just that and the app is called Lucid Dreams. I want to show you some of the dreams that people had over the past year. Some people dream of unicorns.
This is serious . Strangely, some people were also wrapped up in work stress issues and even more wanted to just get out of the office and ride their bike. And some engineers even dreams of Crusty. And so that was the inspiration for our application. But since we're going to be using the app throughout our session, I want to give you a quick demo so you can get a feel for what this application does.
And so if we launch the app, you'll notice it takes us right to the list of dreams that we've had.
And if you tap one of the dreams, you can edit it. And so you can see we have a preview at the top. We can scroll down and we're going to add some effects, like laser focus and rain. And so if we scroll back up, you can see the preview of a dream of a unicorn with laser focus, rain, and fire breathing. And so when we're done editing the dream, we can just go back to the list of our dreams. And so that's the app.
And we wanted to think really, really hard about how we could make our code just awesome. And so we watched these fantastic Swift talks from last year.
These sessions focus on the benefits of value types and protocol-oriented programming. And these ideas were so powerful that we wanted to take advantage of them because they can help improve local reasoning in our application.
And so we wrote our app with this different way of thinking.
Now some of these ideas might feel foreign to you and that's okay. When we were initially trying these techniques out, they felt foreign to us too. So don't worry. Just stick with us.
And so we're going to quickly go over the benefits of value types in the model layer. This was already covered last year, so it's going to be a quick recap. Then we're going to focus on how we can use value types in the viewing controller layers since that's where we think most people don't think they can take advantage of value types, even though there're some really tangible benefits. And along the way we're going to show you how using value types and protocols made our code testable.
And as you just saw this awesome app, I know you want to test it. We've shipped this as a sample app so you can go download it yourself, see the code, and log your own dreams.
All right, so now it's time to talk about the model layer.
So what is a dream? A dream is our model type that represents a dream entry in our application.
A dream as a description, a creature, and a set of effects, as you saw in the UI earlier on.
Now I want to show you a version of the dream type that we used last year in the first version of our app. We started with a dream type being a class.
Now classes have reference semantics, meaning that references to the same instance share their storage and that sharing is implicit.
So why is that a problem? Well, let's say someone tries to modify dream2's description.
If we only care about dream1, we may be surprised that the variable's value changed from underneath our control. And this really hurts local reasoning. And we had exactly this kind of bug in our own application, even though our test passed. But why was that? And so this diagram shows the relationships of the first version of our application. Some of these relationships can be explicit and implicit, some of them can be one-way or two-way, and some of them can even be dynamic or static. And so these relationships can get very, very complicated.
So what happens when we try and test just the dream type on its own? Well, even if you create a dream that stands by itself, this doesn't reflect the reality in the app because there are many more dependencies that actually exist.
And so that's not good.
And so we can solve this by making our dream type a struct which has value semantics.
This means each variable has independent storage. So changing the value in one doesn't change the value in the other. And so if we modify dream2's description, we only change dream2's description, not dream1's.
And so this guarantees us that dreams aren't involved in the complicated relationships that we saw earlier. And so this really improves our ability to reason locally because no code can change the value we're using from underneath our control. Now using value types -- So we just saw how we can take advantage of value types in the model layer. And using value types in the model layer is actually pretty uncontroversial.
But wouldn't you want to take advantage of the same benefits that we just saw in other parts of our application? And that might actually raise a few eyebrows. So I want to share with you a quote that I recently saw on the Internet and that said, "Use values only for simple model types." That doesn't seem encouraging.
But do we believe everything that we read on the Internet? And the answer is no, if you didn't know . And so for the remainder of the talk, we're going to focus on how you can use value types for more than just simply model data in your app, and at the same time we'll prove the Internet wrong.
There we go.
And with that, I'd like to hand it over to Jacob to talk about the view layer. Thanks, Alex. I'm really excited to tell you about how we use protocol-oriented programming together with views.
So we spent a lot of time working on our app's table view cells.
We had a specific design for their layout that we wanted to implement to make sure that the unicorn the people dreamed about showed up exactly right.
Now when we started our app, we wrote these layouts as abstract subclasses of UITableViewCell. For example, this simple layout we called DecoratingLayoutCell, it just shows a small decoration on the left and a larger content area on the right.
Then, we made a concrete subclass of the layout cell that added our specific logic, like showing a dream. And we did this separation because we wanted to be able to reuse our layouts in different places. But as we worked on the app more, we found that this wasn't working very well. It helped us reuse our layout in different cells, but it was hard to use outside of a table view.
For example, we had a detail view that showed more information about a dream but we couldn't reuse our layout cell there.
So we wanted to find a better way to structure this where we could use our layouts together with table view cells but also in plain UI views.
And we also want to add SpriteKit to our app to show those cool particle effects and we want to be able to use our layouts with those SpriteKit nodes too. So those are our goals and we use what we learned about Swift to achieve this.
Now although I'll be talking about layout in detail, I want you to keep in mind that these techniques can be used all across your app. All right, let's get started.
Now this is what our layout cell looked like before. It has two views that it lays out, but there's really no need for this layout logic to be trapped inside of a cell. It's just some math and geometry to figure out the right set of frames.
So let's change this from a cell to just being a plain struct.
It will still have our two views and we can put all of that layout logic into a single method that can be called in to, to lay them out. Now just with that small change, we now have a really isolated piece of code that knows how to do our layout and nothing else.
Then, we can update our dream cell to use this new struct to lay out its children.
And what's great is that we can now use this in our UIView subclass as well.
Now that this layout logic is decoupled from table view cells, we can use it in any UIView. And there's another great benefit with this. Now that our layout can be used in isolation, it's really easy for us to run a unit test for it. We can just create some views, add them to our layout, and then lay them out in a known rect.
Then, we just have to verify that the resulting frames are what we expected. Our test doesn't have to create a table view or wait for the right view layout callbacks to happen. It can just tell our layout to work and then verify the output.
And this is part of a general benefit that we have now.
Our new layout struct is really small and focused. This change has made it much easier to reason locally about this code.
So if we want to understand our tests to our layout, we just have to understand that small struct in isolation. We don't have to think about what set of view capabilities it might use or override. Okay, now let's go back to our DecoratingLayout code.
So right now, this still only knows how to lay out views. But as I said earlier, we want to use this to support SpriteKit as well. So we don't want to have to duplicate this code, but SKNode is not a subclass of UIView. So there's no common superclass that we can use here. So how can we combine these together into a single layout? Well since the only thing that our layout ever does with these children is to set their frames, that's the only functionality that we need them to have. And we can represent that requirement with a protocol.
So we'll make the protocol and it'll just have a single frame property for now. This isn't very flushed out yet, but we'll improve it in a little bit. Then, we use this protocol as the type of our children instead of making them views. And finally, we can use retroactive modeling to make UIView and SKNode conform to our new protocol.
And now we have a layout that works with both of these types, and this is one of the great things about relying on protocols instead of superclasses for polymorphism. We can use this, add this functionality to unrelated types to use both of them. Now, our layouts no longer have any dependencies on UIKit as well. And so another thing that we could do is bring this same system to AppKit and support laying out NS views just as easily. I think that's pretty cool. So we're really close now, but there's something here that we can improve.
When we're using a DecoratingLayout in a view, we want to be able to add all of its content as subviews. And similarly, when we're using it in a SpriteKit scene, we want to be able to add our content as child nodes.
But right now, content and decoration can be any type that has a frame. And that means that for example we could have content that was a UIView and decoration that was an SKNode. But instead, we want our layout to just have a set of only UI views or only SK nodes as its children. And that way we'll be able to add them to their appropriate parent.
Now Swift has a great way of expressing that with generics.
So we can update our layout to be a generic type with a type parameter called child.
Then, we can make the content and decoration properties use that as their type. And this gives us exactly what we want. We can enforce that they're the same concrete type.
And so we can have DecoratingLayout with just UI views or one that just contains SK nodes. So generics are a great tool that let us have a lot more control over the types in our code.
Another great benefit of generics is that the compiler has more information about what your code is doing. So it can optimize more.
And you can learn about this in a lot more detail in the Understanding Swift Performance talk. It's a great talk for learning how Swift works and how to write fast Swift code. Okay, we now have a great implementation of our DecoratingLayout. But our app also includes a lot of other layouts, like this fancy cascading one. And this layout is very similar to the DecoratingLayout that we just looked at. They both show a large area on the right with a detailed decoration on the left. And we don't want to copy and paste our code to create this new layout because that would miss a great opportunity to create a shared abstraction that both of these can use.
So how can we share this code instead? Well, one tool you've probably all used before to share code is inheritance. But with inheritance, you have both your code, and please don't try to read this code, but you also have to consider what your superclass might be doing and what your subclasses might want to change or override. So instead of just thinking about the code that you're working with, your mind has to pull together a large amount of code that's spread across your app. And this is just the tip of the iceberg. A lot of the time you also inherit from a framework class, like UIView or view controller and there's orders of magnitude more code there.
So inheritance is another place where you really sacrifice the ability to use local reasoning.
But we can share code in a much better way by using composition.
Composition is a simple idea that's just combining smaller pieces together to build larger pieces. But when composing, you can understand those independent pieces in isolation.
And you can also enforce encapsulation without worrying about subclasses or superclasses poking holes in your abstractions.
But composition isn't new either.
You've probably used in the past with Objective-C or other languages.
And one way that we could've made this layout before would be to compose views together. So you could've written a UIView that does this cascading layout behavior and another UIView that does our decorating layout behavior.
Then you could've added both of those as subviews in your table view cell.
But there's a big problem with this.
Class instances are very expensive. When you make another object, you have an extra heap allocation and this is even worse with views. There's a lot of work that's needed to support a view to allow to do things like drawing and event handling. And because of this, we try very hard to minimize the number of views that we use.
So making a view that does no drawing and only acts as a layout abstraction is very wasteful. And that's why doing composition with views doesn't work very well.
But with Swift, we have a much better way to do composition and that's with value types. Structs are very lightweight, so we can use them together without paying the high heavy cost that we have with classes and views.
And structs are also better because of value semantics.
With value types you have much better encapsulation so you can use these pieces together for composition without having to worry about someone else modifying the copy that you're using.
So let's apply this to our layouts.
Well, we can write the cascading part of our layout like this, with an array of children that it lays out.
Then, we want to compose this layout with our DecoratingLayout to get the final effect. But there's one more small thing we have to change here.
These layouts only expect to have children that are UI views or SK nodes. So let's generalize this so that we can use layouts and compose them together. Well the layout protocol that we're using for our children requires a frame property.
But we never need to call the getter for that property. We're only ever setting new values for it. And we don't actually care if our children have a frame or not. We really just want to be able to tell our child to lay itself out in a given rect. So let's change this to a method that reflects that. When we decide a rect for one of our children, we'll tell that child to lay out in that rect. UIView and SKNode can still conform to our protocol. When they're asked to layout in a rect, they'll just use it to set their frame.
But now we can make our layouts conform to this protocol as well. They already know how to do layout. When they're given a frame, they just divide up that rect and give it to their children. Now we also need to make one small change to DecoratingLayout to allow it to have more flexibility in the types of its children. And we'll look at that in detail a little bit later.
So now we can build our fancy layout by composing together a CascadingLayout together with a Decorating Layout.
The composition lets us build advanced layouts like this in a very declarative way and there's even more examples of this in the sample code.
So when you're working on your own app, the next time you need to reuse code or customize some behavior, trying using Composition. It's a great tool.
So earlier I mentioned that we wanted to be able to add the contents of our layouts to either a superview or a SpriteKit scene. And an important part of that is adding those contents in the right order. For example, our CascadingLayout wants its children to be ordered so that they line up on top of each other, like this.
So let's expand our protocol to be able to support that as well.
We'll add a property to our layout protocol to be able to return its contents.
And our combining layouts will return all of their contents in the correct order from this. And then leaf views and nodes can just return themselves.
But once again, if we make the type of the contents just be our protocol, this would allow for mixed environments of UI views and SK nodes as the contents. And since we're adding these children to a parent, we only want to allow a homogenous collection of just UI views or just SK nodes. So to enforce this, we can add an associated type to our protocol. An associated type is like a type placeholder. The conforming type chooses the concrete type that it wants to use.
So our protocol's associated type is for what we'll be putting in the array of contents that our layout has. This allows us to write something that just knows how to lay out views and specify that its content type is UIView.
And similarly, we can write layouts that only have SK nodes as their contents.
And this type safety is really great. But just like before, we don't want to have to write a separate layout for views and nodes.
But with the generic version of our layouts, we can still have a single layout that supports both.
And for our content type, we'll just use whatever the content of our child is. And this means that we can make a DecoratingLayout that works with just UI views and one that works with just SK nodes. Both are strongly typed so that we can pull out their contents and know exactly what they are and they can still share all of the layout logic.
So associated types are a great way that you make your protocols even more powerful.
So now that we have our improved layout protocol, we can also revisit the types of our DecoratingLayout's children.
Now here they're required to be the same. And this works great if they're both UI views but not if we want to have a CascadingLayout together with a UIView, like we talked about earlier for composition.
What we really want here is for all of the contents to have the same type.
So let's update our layouts to reflect that. We can change our struct to have two different generic type parameters, one for each of its children.
Then, we can also add a generic constraint to require that those two types have the same kind of contents.
And this lets us express the exact restrictions that we want. Our children's content must be the same type. So this is our finished protocol, which represents our layout abstraction, and this is a much better protocol that what we had earlier. This now has real meaning. It's a complete set of operations that represent what it means to be a part of the layout process.
You can also see our sample app to learn all of the details about how all this works, including how we also used it to do layout for rendering images on a background thread. And one last place that we can also take advantage of our new layout protocol is in our unit tests. So we can write a struct that has a frame property and conforms to our layout protocol and then we can change our unit test to use this instead of UI views as the children in our layout.
Now our layouts will just be setting frames on these simple structs.
And this means that our test is completely isolated from UIView and only relies on the logic in our own layout and test structs. So we're unit testing our layout without using the GUI. I think Crusty would be proud.
So those are some examples of how you can use types and protocols in ways you might not have expected in the view layer of your app. And we also saw some great general Swift techniques that you can apply anywhere. First, we looked at how you can improve local reasoning by using value types.
Then, we saw how you can use generic types to get better type safety and flexible code. And we also saw how composition of values is a great tool for customizing and building up complex behaviors.
Now I'd like to bring Alex back to tell you what we did with controllers in our app. Thanks, Jacob.
Now I'd like to focus on how we can use value types in the controller layer. And we're going to talk about this in the context of our app's Undo functionality.
So we implemented Undo for our list of dreams and that worked really great, but we noticed that we had a small bug where Undo doesn't work for our favorite creature feature.
Now to reproduce this, we can tap the Favorite Creature row. Right now we have our pink unicorn set as our favorite creature but let's change that to be the dragon. And to finish the change, we can just tap the Done button at the top right. All right, so we've modified the favorite creature but the problem is that if we shake to Undo, nothing happens.
And so that's a bug. So let's take a look at our code and see why that actually happened. So we have two model properties on our view controller right now: one for our dreams and one for our favorite creature. This is a pretty typical arrangement in the UIKit app, especially as they grow larger and your feature set grows. And so again we started off with just our dreams functionality and so we implemented Undo for that and it worked, which was great. But after we added the Favorite Creature functionality, we didn't have that Undo code. And so the bug was just that we forgot that code.
And so to fix this, we could've added another code path that implemented Undo for our favorite creature. That sounds like a maintenance nightmare, because now every time we add another model property, we have to add another code path to implement Undo. And so that doesn't seem good. So we don't want that. And so after that we took a step back and wanted to figure out a solution that would better as we add more model properties. And so the solution is to compose these model properties into a single value, our model struct. And our undo logic is going to work solely in terms of that one type. Note it's really important that our model still have value semantics and that's because it's composed of two other values.
And so this approach is really great because we now have only a single code path for our two model properties, and if we add another model property, we still just have one code path. And so this is really great.
And so we can accomplish this in code by moving our two model properties on our view controller into our new model struct.
And from there, all we have to do is add a new model property to the view controller. So that's how we're going to structure it, but now we need to implement the Undo code. So how do we do that? Well, I want to show you first the way that it's commonly done and we'll see why it's a little buggy. And so on the left, we have our view controller's current model value. And on the right, we have our operations and our undo stack.
Now in the original version of our app, we thought of the undo stack as a sequence of small steps. Each step was responsible for modifying first the model and then the view to match. For example, in the first undo step, we're going to remove the dream that the user just added and then we're going to delete the row in that tableView. And we can continue with the next undo step. And so in this undo step, we're going to change the model to be back to a pink unicorn. And so this approach of mutating individual model properties and updating our view independently is really easy to get wrong and that's because you need to match the change in the model to the change in the view, precisely.
And so failing to do so leads to a lot of inconsistencies between your model and your view, and so you end up with bugs like this.
And I'm sure all of you have run into this. I run into this all the time.
And it's really hard to debug. But why is it hard to debug? Well, let's take another look at our Undo stack that we started with. Where do these undoable changes actually come from? Well, each undoable change comes from our view controller and each of those undoable changes matters for order. And as we add features to our app, these opportunities for mistakes spread.
And so there isn't one place in our code where we can reason about the correspondence between our model and our view updates. And so this is bad because it's really complicated. So let's think about a simpler way to handle undo. What if instead of recording small changes, each entry in the stack is just a whole new value, a whole new model and so now performing an undo on the model is really simple. Just replace the current model with the one on the stack and that way we don't even need to worry about order. And then we just replace the value. And so now that we have that sorted out with our model, we need to figure out how we're going to update our UI. And so in our view controller, whenever a model changes, we're going to call this modelDidChange method.
Now, I recommend you go download this sample and look at this method for more information about how it works. But in that method, we need to find the differences between the old and the new model values and update our UI to match.
So for example, we can check to see if the old model's favorite creature is different than the new model's favorite creature.
And if it is, we're going to update our table view section that contains the Favorite Creature row.
And there's a more flushed out implementation in the project, like I just mentioned, so I recommend you check out more of the UI that we update in there. And finally, we can just register the undo logic where we reset our model to the old value. Now this is great because now we only need one or we're only registering the undo in a single place. So what were the benefits here? Well, as we saw, we now have a single code path for updating our UI, and operations are order independent, which was not the case before.
This really helped us reason locally about our code, about our UI update code. We also saw how values compose very well with other values. If we have two values as properties of a single value, that value still has value semantics.
Okay, so we just talked about how to use value types in the controller layer with our model properties. And now I want to do the same with our controller's UI state properties. And so you've seen this screen before. It's just our list of dreams. But this view controller has many, many different states and so I'd like to show you the state diagram for our view controller as it relates to a very cool feature that we have, the ability to share dreams with friends.
So let's take a look. So this is our basic state diagram. We're going to start off in the viewing state. Now we can tap the Share button at the top right, which is going to take us to the selecting state.
And then we can select dreams to share. And then we can tap the Done button to go back to the sharing state.
Then when we're done, we just go back to the viewing state. And so that all worked well. But let's go back to the selecting state again real quick.
You'll notice that we can stop sharing midway through by tapping the Cancel button at the top left. And so this moved us back into the viewing state. And you can see that our navigation bar looks correct since it shows the Share button again.
But there's actually a subtle UI bug in our app because of an inconsistent state.
The UI on the left side of the table view is still visible to allow users to select dreams to share and this is wrong. So when we went back to debug this code, we saw that some of our state properties weren't fully cleared out during a state change.
And so in this case, even though we moved to the viewing state, we forgot to clear out some of the properties of the selecting state.
So let's take another look at our state diagram to see if we can come up with a fix. Now each state here has a corresponding property. And those properties are properties on our view controller. And the number of state properties in our view controller can easily explode as your app's feature set grows. And so it's important that in this case our properties are mutually exclusive. And so when we're viewing, we not sharing. And when we're sharing, we not selecting.
But the way we've written it, when you set one property, you need to clear out all the other properties and this is really error prone. And so how can we solve this problem? Well, enums are actually perfect for mutually exclusive values. And so we turn all of our UI state properties into cases on an enum value.
And from there, we can just add a state property to our view controller. So by using an enum, we can make sure that our states are mutually exclusive. And that's great because now the invalid state bug that we had before isn't even possible and it's enforced by the type system. And so this approach also means that our state changes all at once, without any possibility of any intermediate states. So we don't to coordinate flipping properties with implicit timing dependencies. And as a bonus, having your state all in one places makes it easier to launch your app in exactly the same state as the user left it. So I really recommend you checkout and download the project again to see how we implemented state restoration in our application. Okay, so we covered a lot today, and we started off with the goal of improving local reasoning in our application by introducing value types and protocols into our model view controller based application. So how'd we do? Well, we started off by making our model have value semantics, by making the dream type a struct. And this makes it easier for us to locally reason about our code because there's no implicit sharing of our dream variables.
Then Jacob showed you how we built small components, like DecoratingLayout and CascadingLayout.
These small components took advantage of protocols in generics to make sure that the generic components were reusable with views, SpriteKit nodes, and image rendering. And all of this led to better local reasoning since each of these types were small, testable, and isolated value types.
And then we saw how we can take advantage of composing model properties on a view controller into a single type.
This made it easier to implement undo with a single code path, even as our model type grows to have more properties.
And this approach also gave us one code path for updating our UI, making it easier to understand the UI logic of our view controller in isolation. And finally, we saw how we can turn our mutually exclusive state properties into an enum value on our view controller.
Now this reduced the potential for our UI to be an in inconsistent state.
And so those were the value types that we discussed today. But if you go and download the sample project, you'll see that there are actually many more in the project. And so pretty much our whole application is built with value types except for where we have a controller or view object. Now these are required to be reference types by UIKit but we've still moved most of our functionality into value types. And so we spoke about a lot today and I want you to go home with just a few things in your mind to take home. So the first is customization through composition instead of inheritance.
The next time you're at your desk and you're drawing a class diagram to solve some problem, I want you to think how you can use composition instead of inheritance to solve that problem, so you get the benefits of value types that we talked about today. And the second technique is to use protocols for generic reusable code.
You can make small reusable components that are easy to locally reason about and easy to test. And so I highly recommend you check out how we've done that in the sample with generic types instead of having class hierarchies. We also showed you how to take advantage of value semantics. The important thing to remember here is that if you have a value composed of other properties that are values, the larger value also has value semantics. And finally, we talked about local reasoning.
Now, local reasoning is actually a very general technique that's not specific to UI programming, it's not specific to mobile development, and it's not specific to Swift. This is a really important aspect of all programming language, all programming languages.
So when you get back to your desk and you start coding, I want you to think, regardless of the language, does that code, how well does that code support local reasoning.
Now it's no accident that Swift emphasizes value types so much because they're a hugely important aspect for you to be able to locally reason about your code. And that's it. So you can find the sample code and more relevant resources here. I highly recommend you do that. We have some related sessions that we mentioned throughout the talk and I recommend you watch them on video. And thank you for a great WWDC. [ Applause ]
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.