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.
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 pre-viewable, you make your app more understandable, more testable, and more maintainable.
In this video, I want to show you four ways that 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 lifecycle, 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 pre-viewable. 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 a 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 little bit more. But as I edit that color I don't want to lose the context of where I'm actually 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 of those sets of previews comes from. And now I can edit this color. So let's change this to 70 percent. Looks a little better, but 80 percent 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 going to select my color, and I've already set up a color, so I'm going to 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 and again, in the context of where I'm using it in this thumbnail. So I'm going to unpin the previews from our layout selector and repin the previews for our layout thumbnail.
So let's go into the asset catalog to edit this color. So you can see that gray, I'll select it, and using the inspector at a customization for dark mode.
I can select that customization and I'm going to change this to 20 percent. Now I'll say the asset catalog. Resume the preview. Xcode will rebuild the app and show me what this new color looks like in context. 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 going to navigate over to another set of previews in this extra previews file. Now this is showing me several different previews for our accessibility layouts. Now 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 just selecting each place I need to use it, and multiplying by the size category scale. And now it looks great across all these different sized 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 preview pending; whether it's making edits within a larger context, being able to edit non-Swift files, or being able to spawn additional previews that you need to have for a particular task.
Next, we'll look at the relationship between previews and the SwiftUI app lifecycle, which was introduced in iOS 14 and iPadOS 14.
All of our apps do some kind of initialization of 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. Now what's great about previews is that we can edit small and leaf views and our project, and jump right to them right in the canvas.
Now 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 lifecycle gives us just the tool to define this dependency. Let's take a look at how it works. So 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 will create my model. So let's take a look at what my app is doing when I'm running under previews. And to do this, I'm going to 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 because 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 going to 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. Now for the preview that I'm looking at, I don't need to do that work. And so I'm going to take advantage of SwiftUI's state object property rapper.
Let's pause our preview. And let's add @state object to our property.
Now 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. Now also for previews, models that are created using state object are not initialized. And this gives 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 state object to only initialize our data when we actually need it. To learn more about state object, check out the video app Essentials in SwiftUI.
So if we're not initializing our data models as part of our state objects, 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. And 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 have to find 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 from 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 the additional preview content to only include for the development version of my app.
Now what's great about devolvement assets is they apply not only to files like asset catalogs but also to code. Let's look at what's inside that previous 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 finding 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 pre-viewable. And this is really where we're going to see the payoff in structuring our app to make it more pre-viewable.
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 CoreData 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 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. And 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 simple data types, the more reusable, testable and pre-viewable our app is going to be. Now 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 pre-viewable. First, we'll look 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.
So 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 CK user identity. 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.
Now when I get to the point of filling in the CK user identity 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 collaborator cell.
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 collaborator cell. And this is going to take that name, say Jane Doe.
An image, for now, we'll just pass an empty image. And the status. We'll start with Jane online. 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 going to duplicate this preview a few times. And one of these I'll make Jane offline, and we'll give her an image. So open up our asset catalog, pick an image, and we'll also make sure that 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 an edit 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 going to pass in person named components. 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 texts. Finally, let's update our previews to create named components instead of strings. So I'll select each instance of my static string, and replace it with a call to create the person named components.
Now I can refresh my preview and you can see really how easy it is to create one of these person named components and still give me all the configuration that I need.
So now we can also add an 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 want to 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 on a simple data type that our view can change.
So in this inspector I'm passing in the slot data for an image as an observed object. A slot, this represents one of the photos on our collage. Let's take a look at that image slot datatype.
Now this has some simple types but really the core of this is a backing CloudKit record.
Now as we saw from our previous examples, in order for me to create one of these records I'm going to need to have that full CloudKit data model all pulled in. I want to make it really easy to create previews, so I'm going to apply that same idea that I did in the previous demo. I'm going to 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 slot effects. Now 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 going to create inspector and I'm going to 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. Now to do that for previews I'm going to 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.
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 that I want to add here which is a button that allows me to replace the photo I could pass in the whole image slot datatype 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 going to pass an enclosure that will get called when the button is pressed.
So I'm going to add a closure as an input and let's add that button.
This is just a simple button that calls that replace photo handler. And now let's go update our previews to provide a value for this callback.
For the first one, I'll just pass that empty closure. But the second one, I want to make sure that that button is actually working. So for that I'm actually gonna have my callback just change our effect so I can see that that'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 10R here in dark mode and I want to put the same preview on that device as well.
I'll click the device button, click my iPhone 10R, 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 onto your devices. You'll notice that the first time you use on device previous with Xcode 12 that an icon appears in your home screen called Xcode Previews. By tapping this icon you can get back to the last preview that 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 where 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 CK share record.
Now as with the other examples that we've seen, I'm going to need that full data model in order to create one of these collage model objects and pass it into a preview. Well we could use the approach we use in our previous demo, and use bindings to pass in the data. Well 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. So 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 on the data that I actually need. So let's step back and use that same approach that we've been using for 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 going to define a protocol to create an abstraction of exactly what we need. So let's look at this abstraction which is called a collage protocol. And 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 image publisher. 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 design time slot. The design time slot does 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 image picker type. 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 see how easy it is to create previews for our collage editor.
So let's call our collage editor. That's going to take two inputs: first, our collage.
Do a design time collage, and this as we saw pics a couple inputs, takes a name which we'll call My Collage, takes a layout, we'll start with two by two.
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 the photo picker that just draws on images from that previous 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 little 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 take 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 design time image slot. So 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 if I want to see what the saturation looks like at 50 percent. Now this looks great on iPhone, but how does it look on iPad. Well with like the last example we're also going to put this preview on device. I can select the on device button and select my iPad. And this will let me see what this collage editor looks like on an iPad in landscape. Now just like the version of my canvas, this is fully interactive. And double tap, pick a photo, and I can even open the inspector and add some effects.
So I was able to test all of the functionality for this collage editor without ever creating my CloudKit back to 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 I'll switch over to our Cloud sync view.
Our Cloud sync status view takes the cloud sync status as an input as an environment object. Now in SwiftUI when you have an environment object you need to provide a value for that object higher in the hierarchy.
When you're creating previews for these views that take environment objects as inputs, you also need to provide a value for each preview. Let's see how that works.
So I'll create a Cloud sync status view, and then attach the environment object modifier.
I'll create a Cloud sync status and there's a preview of what my view looks like when our Cloud is online. 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 going to pass in what happens when our Cloud is offline. So there is my icon for the Cloud being offline.
Let's duplicate it again and let's make sure that we can correctly show the time of when at last synced. So do you last synced and let's try, oh, a few hours ago.
And finally just to make sure we're covering all of our bases, let's look at when the Cloud is online 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 a last example, I want to bring together all the things that we've been seeing in this video. So let's switch over to the root view of our app.
Now this root view, like our collage editor, is generic over two things.
First, the collage type which is that collage protocol and the image picker type.
Now 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 reviews and passing the collages in as a binding.
There's also an action call back 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 design time photo picker. When I actually use my preview, then I'm attaching an environment object with our Cloud sync status. 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 going to 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 bit 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 going to click my on device preview button and I'm gonna select both devices my iPhone and my iPad. And now Xcode is putting fully functional versions of my app on all 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. 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 of 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. Hope you can see just how powerful previews can be when you're defining abstractions for your data and using multiple previews in 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 pre-viewable.
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 lifecycle and using state object 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 released product by using development assets. And finally, we looked at four ways that you can make your views themselves more pre-viewable.
I want to leave you with one big idea. The investments to make your app more pre-viewable benefit your app as a whole. I hope you saw from this video that a pre-viewable app is more understandable, more testable, and more maintainable app.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.