스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Structure your app for SwiftUI previews
When you use SwiftUI previews during development, you can quickly create apps that are more flexible and maintainable. Discover ways to improve the preview experience by making small tweaks to your project. Find out how to preview multiple files at once, how to manage data flow for previews, and how to use sample data while previewing. We'll also give you strategies for defining view inputs to make them more previewable and testable. To get the most out of this session, you should have some familiarity with SwiftUI. For an introduction to interacting with SwiftUI previews in Xcode, check out "Visually Edit SwiftUI Views" from WWDC20.
리소스
관련 비디오
WWDC20
-
다운로드
Hello and welcome to WWDC.
Hi. I'm Kevin, and I work on Xcode Previews. Xcode Previews helps you write SwiftUI code, edit your views in multiple configurations at once, and quickly test all of your changes. But previews benefit your app as a whole. By structuring your app to make it more previewable, you make your app more understandable, more testable and more maintainable. In this video, I want to show you four ways you can structure your app to get the most out of Previews. First, we'll look at how to preview multiple files at once, so that you can edit views in the larger context.
Secondly, we'll look at the relationship between previews and the SwiftUI app life cycle, so that you can get both great performance for your previews while also defining explicit data dependencies.
Third, speaking of data, we'll look at where to define sample data, so that you can have a comprehensive set of examples during development without affecting the size of your deployed app. And finally, we'll look at a handful of techniques for making your views themselves more previewable. We have a lot to cover in this video, so let's dive in. I'll be using the same app for all the demos in this video. It's an app for creating collages of the photos I've taken. You can create a collage, pick a layout, pick some photos, add your friends, and, like every great photo app, even add some effects. So first, let's look at how to preview multiple files at once.
We'll start in the view for the thumbnail for picking a layout for our collage. I have this gray rectangle here, and it looks great on this white background. But how does it look in context? For that, I'll switch over to the selector for picking the layout.
Now I have two previews here. One is showing the selector on a white background, and the second is showing it on top of an image. This is meant to show what it looks like when the user pinches-to-zoom the collage and content slides underneath that selector. As you can see, that thumbnail kind of disappears when there's content behind it. So I want to make that color stick out a bit more. But as I edit that color, I don't want to lose the context of where I'm using it.
I can accomplish this by pinning the preview by clicking this button in the bottom bar. Now, when I switch back to my Layout Thumbnail view, Xcode is showing me both the previews for the view that I just pinned and the previews from the file you're working in. And Xcode adds dividers to show where each set of previews comes from.
And now I can edit this color. So let's change this to 70%. It looks a little better, but 80% looks even better. But how does this look in Dark Mode? I'll duplicate the preview by clicking the "plus" button.
I'll select the preview, open the inspector, pick a dark appearance...
And now when I see this preview, that gray looks a little out of place. I'd like to customize this gray for Dark Mode. The recommended way to do this is to use an asset in the Asset catalog.
So I'm gonna select my color. And I've already set up a color, so I'm gonna open the library, go to my colors, and pick our grid placeholder.
Now, nothing's changed because I haven't customized this color yet for Dark Mode. So let's go do that. But when I do that, I want to be able to edit that color, again, in the context of where I'm using it in this thumbnail. So I'm gonna unpin the previews from our layout selector and re-pin the previews for our layout thumbnail.
Let's go into the Asset catalog to edit this color. Here you can see that gray. I'll select it, and using the inspector, add a customization for Dark Mode.
You can select that customization. I'm gonna change this to 20%.
Now I'll save the Asset catalog.
Resume the preview. Xcode will rebuild the app and show me what this new color looks like in context. And that looks great.
I want to show you one other use-case for pinning, which is by bringing in additional previews that you might not need during the entire time you're developing.
Using the navigator, I'm gonna navigate over to another set of previews in this extra previews file.
This is showing me several different previews for our accessibility layouts. They all look the same right now because I actually haven't customized my view yet to react to the different accessibility sizes.
So let's unpin the preview and pin these different accessibility previews.
Then I can switch back to my thumbnail.
And now I can scale the size and the spacing of this thumbnail based on the size category. I've passed the size category in through the environment, and I've added an extension to the content size category to provide a scale that's appropriate for my app.
I can use that scale by selecting each place I need to use it and multiplying by the size category scale. And now it looks great across all these different size categories. And just like you would expect, I can continue to edit my preview and see what these changes look like across all those different sizes.
So that's three ways that we can leverage previewing pinning, whether it's making edits within a larger context, being able to edit non-Swift files, or being able to pull in additional previews that you need for a particular task. Next, we'll look at the relationship between previews and the SwiftUI app life cycle, which was introduced in iOS 14 and iPadOS 14.
All of our apps do some kind of initialization at startup. This might be loading a document from disk, connecting with CloudKit, or communicating with various devices. Whatever it is, the point is that often this work is expensive. What's great about previews is that we can edit small and leaf views in our project and jump right to them right in the canvas.
For these previews, we want to avoid doing that expensive initialization work.
Now, it's important to think about this in terms of defining explicit dependencies for our data. It's not so much about doing work for previews or not for previews. It's about defining when you want that work to happen.
SwiftUI app life cycle gives us just the tool to define this dependency. Let's take a look at how it works.
Here's the definition of my app type.
And here as a property, I've defined my network model. So every time my app gets created, it'll create my model.
Let's take a look at what my app is doing when I'm running under previews. And to do this, I'm gonna attach a debugger to our preview. I can do that by pressing and holding the "play" button and picking Debug Preview.
I can attach a debugger to any of my previews, and it's really great 'cause I can do two things: One, if I have any breakpoints set, I can hit those breakpoints and debug why my preview isn't showing what I expect.
Or, in the case of this demo, I'm gonna give you an example of how using the debug gauges will help you see what your app is doing. When I open the debug gauges, I can see that my app is doing a lot of work on startup, both on the CPU, and the network is pulling down a lot of data. This is because I'm initializing that data model.
For the preview that I'm looking at, I don't need to do that work. So I'm gonna take advantage of SwiftUI's StateObject property wrapper.
Let's pause our preview.
And let's add @StateObject to our property.
This will tell SwiftUI to only initialize our data model when the app is first created, and it allows SwiftUI to react to any changes in our data model. Also, for previews, models that are created using StateObject are not initialized. And this give us a great opportunity to do work only when we need to run our app.
So now, let's resume our preview.
Let's start debugging again.
And let's take a look at where our CPU is now.
That looks much better. We're doing no unnecessary work on the CPU or on the network.
So that's an example of using StateObject to only initialize our data when we actually need it. To learn more about StateObject, check out the video "App Essentials in SwiftUI." If we're not initializing our data models as part of our StateObjects, how do we get data into our previews? We'll look at this in two parts. First, let's look at where to define sample data for our previews.
Our collage editor is a photo app. So during development, I want to have a lot of examples to make sure that my effects and my layouts are working properly.
So I've defined an Asset catalog and added a bunch of images to it that I can reference in my previews.
But I don't want to ship all of these images in my final app.
So I can take advantage of Xcode's development assets for my target.
So let's switch over to our project editor, scroll to the bottom, and you'll see a section here called Development Assets.
A development asset is a path to a file or folder, and Xcode will only include those paths in the development version of your app.
If you're creating a SwiftUI app form an Xcode template, the app comes pre-configured with a development asset path for you. That's this one right here. But you can also easily add your own, like I've done here, and you can also add them to additional targets. So I'll switch over to my Mosaic Kit framework, click the "plus" button, type in "preview content," and find that additional preview content to only include for the development version of my app.
What's great about Development Assets is they apply not only to files like Asset catalogs, but also to code. Let's look at what's inside that preview content folder that we just added.
By using the navigator, we can look inside and see two Swift files. These Swift files contain code that I only need for development and debugging and testing my app, and I don't want to include these when my app is actually deployed. So that demo was mainly about defining where to define our preview assets and resources and code. But how about actually feeding that data into our previews? Well, for this, we're gonna make our views themselves previewable. And this is really where we're gonna see the payoff in structuring our app to make it more previewable.
I want to start with a big idea. What makes our apps unique is the user experience. But behind our unique user experience is a unique data model. I call this a rich data type. It could be something from Core Data or from CloudKit or something custom that we've built. But at the end of the day, our users are gonna interact with our apps using simple data types. This might a be a string in a text field or a Boolean in a Toggle.
And in between is all of our views. And those translate the rich data type into the simple data type. The question is, where in our view hierarchy should we do this translation? Now, as a general rule of thumb, the sooner that we do this translation from our rich type into our simpler data types, the more reusable, testable, and previewable our app is gonna be. The big incentive here is that we want to make it easy for us to add previews so that we can test our app in all of the configurations so they all look great.
We'll look at four examples of how to structure our views to make them more previewable. First, we'll look at at an example of passing immutable, simple data types into a view that doesn't need to change the values. Secondly, we'll look at an example of using SwiftUI bindings to pass simple data types into a view, an inspector, that does need to change those values. Third, we'll use generics so that we can pass an abstraction of our data into a view so that we can pass that on to other views. And finally, we'll look at an example a little different from the others and use the environment to pass inputs into our view. So first, let's look at an example of passing immutable, simple data types into our cell for adding a collaborator for our collage.
We'll start by looking at the data model behind our collaborator. It has some simple types, like this color and last contribution date, but most of the model is backed by the CKUserIdentity. So now, let's go look at the cell.
Our cell takes a collaborator as an input as an observed object. So, let's create a preview.
I'll create the cell, and then need to create a collaborator.
When I get to the point of filling in the CKUserIdentity, I know that I need to create a fetch operation on our CK database. But our data model for CloudKit was exactly what we turned off in one of the previous demos. For this view, I don't actually need the full model. So let's look at another way to do this. This brings up a really important idea, which is to look at the UI that you're building, and look at your model, and identify the minimum amount of data that you need to pass through your model into the view.
So let's look at another example of our CollaboratorCell.
In this cell, we're passing the name, the image, and the connection status as individual simple inputs.
So now, creating a preview for this is really easy. So, we'll create a CollaboratorCell... And this is gonna take that name-- we'll say Jane Doe-- an image-- for now we'll just pass an empty image-- and the status.
And we'll start with Jane on-line. And just like that, I have a preview for that cell. What's great is it's really easy to create multiple configurations of this cell.
So I'm gonna duplicate this preview a few times.
And one of these, I'll make Jane off-line.
And we'll give her an image. So, open up our Asset Catalog and pick an image.
And we'll also make sure the text is gonna properly resize as it gets larger.
So we'll give her a really long middle name.
And that's just how easy it is to create multiple configurations with these simple data types as inputs.
But sometimes we can make our data types too simple. This can happen when we want the system to format a value based on the locale that your user is using in the app. For example, with a date, or in this case, with a name. So instead of passing a string in for our name, we're gonna pass in PersonNameComponents.
And now we can tell SwiftUI to format the text for this name at exactly the right time. This is as easy as defining a formatter...
and then passing that formatter into our text.
Finally, let's update our previews to create name components instead of strings.
So I'll select each instance of my static string...
and replace it with a call to create the PersonNameComponents. Now, I can refresh my preview, and you can see how easy it is to create one of these PersonNameComponents and still give me all the configuration that I need.
Now we can also add in that example with the middle name. So we'll say "Really Long Middle." And just like that, all my previews look great, and I can see all these different configurations. So when you can, you wanna make the inputs to your views simple, immutable data types. But sometimes our viewers need to actually change values. So, second, let's look at an example of using SwiftUI bindings to pass in a simple data type that our viewer can change.
So in this inspector, I'm passing in the slot data for an image as an observed object. A slot just represents one of the photos on our collage. Let's take a look at that ImageSlot data type.
Now, this has some simple types, but really, the core of this is a backing ClockKit record. As we saw from our previous examples, in order for me to create a record, I'm gonna need to have that full ClockKit data model all pulled in. I wanna make it really easy to create previews, so I'm gonna apply that same idea that I did in the previous demo. I'm gonna look at the UI and pick out just the pieces that my UI actually needs to edit.
So let's switch to a different version of our inspector.
And in this case, we're passing a binding in for our SlotEffects. This is a really simple type that just has some floating-point values for each of our different filters. It's really easy to create previews.
So I'm gonna create an inspector and pass in some effects. And you can see here I'm using a constant binding, which just means that in this view, the binding can't be changed. But that's actually what we want to test. So let's add an example where we can actually change the value of this binding. To do that for previews, I'm gonna introduce an intermediate view that will store some state that I can pass as a binding into my view.
So let's define my view...
and let's define a new preview that creates that new view and passes in some effects. Those effects are stored as state, and our inspector passes them in as a binding.
And now, when we resume our preview, we can take this preview live...
and we can interact with it right here in the canvas. There's one more piece of UI I want to add here, which is a button that allows me to replace the photo. I could pass in the whole image slot data type into this view and do all the replacement logic here, but really, I want clients to decide how to do that. So instead, I'm gonna pass in a closure that will get called when the button is pressed.
So I'm gonna add a closure as an input, and let's add that button.
This is just a simple button that calls that replacePhotoHandler. And now let's go update our previews to provide a value for this callback.
For the first one, I'll just pass an empty closure. But for the second one, I wanna make sure that button is actually working. So for that, I'm actually gonna have my callback just change our effects, so I can see that it's working.
And now, when we resume the preview...
As we click our button, you can see that saturation is going down.
But what's great about previews is that I can interact with it not only right here on the canvas, but I can interact with it in multiple places by using on-device previews. So I have an iPhone XR here in Dark Mode, and I want to put that same preview on that device as well. I'll click the "device" button, click my iPhone XR...
and Xcode will now mirror this preview onto a device.
And just like before, I can fully interact with it right here on the device.
Now, in Xcode 12 and iOS 14 and iPadOS 14, there's a new experience for on-device previews. As you make changes, they're seamlessly reflected on to your devices. You'll notice that the first time you use on-device previews with Xcode 12 that an icon appears on your Home Screen called Xcode Previews. By tapping this icon, you can get back to the last preview you were looking at even if your phone is disconnected from Xcode. This makes it great for being able to make some changes, put them in a preview, and go show your colleagues. So that's a quick look at using bindings. So we can keep using simple data types for our inputs while also allowing our views to mutate them. But there are cases when we need to pass richer data types from one view to another. So third, let's look at an example of using generics to pass data into the main editor for our collage. So let me switch over first to show what the model for our collage looks like.
Like the other types that we have, it's backed by this CloudKit CKShare record. Now, as with the other examples that we've seen, I'm gonna need that full data model in order to create one of these collage model objects and pass it into a preview. We could use the approach we used in our previous demo, and use bindings to pass in the data. Let's try that.
so I'll switch over to the Editor for our collage.
And here you can see it's taking a binding in for the name and the layout, which are simple types, and for the slot data. So let's create a preview.
I'm gonna provide a name using a constant binding for now, and the layout using a constant binding. But when I get to the slot data, I noticed that the slot data is still using that rich data type that we used in a previous demo. This is gonna make it difficult for me to pass in data for each one of my previews. So even though I'm using bindings, I still can't pass in the data that I actually need.
Let's step back and use that same approach we've been using for our other demos, to think about the UI that we're building and the minimum amount of data that we need from our model to create that UI. And in this example, we're gonna define a protocol to create an abstraction of exactly what we need.
So let's look at this abstraction, which is called a CollageProtocol. This defines everything that a collage needs to have in order to be editable by our UI.
I have a name, a layout, and some slot data, but that slot data is also an abstraction. Each slot in our collage must have an imagePublisher. It's a publisher because that data could be asynchronously provided from the cloud, already on disk or in an error state. And it's also providing those effects.
Now that we have this collage protocol, we can define a Design Time version of our model that's really easy to create.
So I've done that over here in the preview content for our framework.
You can see that this Design Time collage has a name and a layout and some slots, and that's using this DesignTimeSlot.
The DesignTimeSlot has a publisher and some effects. So now let's go look at a version of our editor that allows passing in this Design Time version of our model.
So I'll switch over to the editor here. And this collage editor is generic over two things. First, it's generic over the collage type, which conforms to that collage protocol that we just saw. It's also generic over an ImagePickerType. This allows me to define a simpler UI for Design Time to allow picking a photo for that collage instead of having to go to the full photo library every time I want to test it.
So now you'll see how easy it is to create previews for our collage editor.
So just call our collage editor. That's gonna take two inputs. First, our collage.
Do a Design Time collage. And this, as we saw, takes a couple inputs. It takes a name, which we'll just call "My Collage." Takes a layout. We'll start with twoByTwo. And it takes some slot data. For now, we'll just pass in some empty data.
It also takes a closure for making this photo picker. I've created a Design Time version of this photo picker that just draws on images from that previews asset catalog that we saw earlier.
And just like that, I have a preview for my collage editor, and it's completely functional. But it's using that Design Time simpler data model.
And, because it's a view, I can also add view-specific modifiers. So let's give it a bit of padding.
And now I can test this collage editor right here in the canvas.
By clicking the "play" button...
I can double-tap on a slot and pick a photo or maybe pick a different layout.
Also, I can pass in examples of our Design Time slot data.
So I can pick an address-- The address is-- Think of it like a row in a column. And then I can pass in an example of our DesignTimeImageSlot. This is just taking one of the images from our asset catalog and passing in some empty effects.
And I can see what it looks like if it's in the second column or in the second row or maybe a different image, or I can even pass in some effects. So I want to see what the saturation looks like at 50%.
This looks great on iPhone, but how does it look on iPad? Like the last example, we're also gonna put this preview on-device.
I can select the On Device button and select "My iPad." And this'll let me see what this collage editor looks like on an iPad in landscape. Just like the version in my canvas, this is fully interactive. I double-tap, pick a photo, and I can even open the inspector and add some effects.
I was able to test all the functionality for this collage editor without ever creating my CloudKit-backed data model and just using the Design Time version of my data model.
Fourth, let's look at an example that's a little different from the others. When you can, you should define explicit data dependencies between your views by defining inputs on them. But sometimes it's convenient to pass data through the environment. Let's look at the relationship between previews and the environment by adding some previews for the view that shows the status of syncing with our cloud.
So let's switch over to our CloudSyncView.
Our CloudSyncStatusView takes the CloudSyncStatus as an input as an EnvironmentObject. In SwiftUI, when you have an EnvironmentObject, you need to provide a value for that object higher in the hierarchy. When you're creating previews for these views that take EnvironmentObjects as inputs, you also need to provide a value for each preview. Let's see how that works.
So I'll create a CloudSyncStatusView...
and then attach the EnvironmentObject modifier.
I'll create a CloudSyncStatus, and there's a preview of what my view looks like when our cloud is on-line. But just like with our other examples, it's so easy to add multiple configurations when you're using environment. So I'll duplicate my preview...
and this time, I'm gonna pass in what happens when our cloud is off-line.
So there's my icon for the cloud being off-line. Let's duplicate it again, and let's make sure we can correctly show the time of when it last synced. So we do lastSync, and let's try "a few hours ago." Finally, to make sure that we're covering all of our bases, let's look at when the cloud is on-line and having just recently synced.
So there's an example of using the environment to pass inputs to our views, and just like with other explicit inputs, it's really easy to create multiple configurations of our previews when we're using these simpler types.
For our last example, I want to bring together all the things we've been seeing in this video. So let's switch over to the root view of our app.
This root view, like our collage editor, is generic over two things: first, the CollageType, which is that CollageProtocol, and the ImagePickerType.
I've defined an intermediate view that has some state which is storing our collages which are the Design Time collages that we made earlier.
Then it's creating one of those root views and passing the collages in as a binding. There's also an action callback when the "plus" button is pressed that allows me to create one of these Design Time collages, and because it's so easy to create one of the Design Time collages, this closure is simple to fill in.
And finally, it's passing in the DesignTimePhotoPicker.
When I actually use my preview, then I'm attaching an EnvironmentObject with our CloudSyncStatus.
And now, I have a fully functional version of my application right here in the canvas. I click the "play" button. I can click the "plus" button to add a new collage, go in, pick a different layout, maybe pick a photo.
I can go back, and I can delete it by swiping to delete.
And I can also make changes live to this view. So let's pin the preview, and I'd like to make the thumbnail a little bit bigger here.
So to do that, I'm gonna switch over to our Collage List to the collage label that shows the thumbnail for our collage and the title.
Now I can go into this value and change it to something a bit bigger. Okay, maybe that was a little too big. Let's go back to 30. That looks great.
Now finally, this looks great in iPhone, but how does it look in an iPhone in dark appearance or on the iPad? So we can see multiple on-device previews at once. I'm gonna click my on-device preview button and select both devices, my iPhone and my iPad.
And now Xcode is putting fully functional versions of my app on all of these devices. In the canvas, I could, you know, again, add and swipe to delete. On my iPhone, I could go into the portraits here and maybe pick a different layout. Oh, that looks a lot better. Go back. On my iPad, I can select a collage, pick a photo, and open up the effects...
and test all of that functionality without ever running my app. And what's great is when I make edits, it reflects on all these devices at the same time. So I'll select that 30.
I'll change it to a 90. And now I can really make sure that I see that thumbnail. I hope you can see just how powerful previews can be when you're defining abstractions for your data and using multiple previews and multiple contexts to test all of your configurations without ever running your app.
So there's four examples of how to structure your views to make them more previewable.
Now, we've seen a lot in this video. First, we looked at how to preview multiple files at once so that you could edit a view in the larger context. Secondly, we looked at the relationship between previews and the SwiftUI app life cycle and using StateObject to only initialize data when you need it.
Third, we looked at where to define sample data and how to keep it out of our release product by using development assets. And finally, we looked at four ways that you can make your views themselves more previewable.
I want to leave you with one big idea.
The investments to make your app more previewable benefit your app as a whole. I hope you saw from this video that a previewable app is more understandable, more testable, and a more maintainable app. So let's go build some great apps. Thank you.
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.