Add custom views and modifiers to the Xcode Library
The Xcode Library is an easy way for you to discover available SwiftUI views and drag and drop them to the Xcode Previews canvas, enabling rich visual editing of your app. We'll show you how to extend the content of the Xcode Library with your own views and modifiers, optimizing for reusability and discoverability within your app or Swift packages.
For more on Xcode Previews, check out "Structure your app for SwiftUI previews", and "Visually edit SwiftUI views".
My name is Anton, and in this session we're going to take some time to talk about your SwiftUI views and modifiers inside Xcode.
Since the very beginning, Xcode Previews was built to put your content front and center.
For example, your views are previewable without doing any extra work, and most views and modifiers are inspectable right out of the box as well.
In Xcode 12, we are taking a step further by letting you add your SwiftUI views and modifiers to the Xcode library.
Let's take a step back and talk about some benefits of the Xcode library.
As your code base grows, it can become hard for the users of your code to know exactly what the key parts of your API are.
The library is a place where developers look to discover available visual content, and by placing your most important and most reusable views there, you make them easier to find.
But discoverability is just the beginning.
Since each asset in the library comes in the compilable form, it can serve as an excellent way of educating the users of your code about how any particular view or modifier was intended to be used with a clear example that can often serve as an excellent starting point.
And finally, every asset in the library can be dragged and dropped into the canvas, and inserting views and modifiers from the library allows Xcode previews to continue rendering, enabling rich visual editing for your content.
So now, let's look at what you need to do to expose your content in the library.
And this is where the library takes a page from Xcode preview's playbook.
Since the view or the modifier you're adding to the library is defined in Swift code, the most natural place to define how to expose it in the library is also in Swift code, right next to the view or the modifier itself.
The big advantage of doing that is that the compiler can help us ensure that the library items remain functional as your API changes.
To extend the Xcode library, we need to declare a new type that conforms to the LibraryContentProvider protocol.
This protocol has two requirements: a views property, which you can use to extend the view's Xcode library; and the modifiers function, which you can use to extend the modifiers Xcode library.
There are some differences between the two which we will discuss in a moment.
But for now, notice that both of them return an array of LibraryItems.
Let's look at how to create a LibraryItem.
The minimum amount of data that Xcode needs to create a LibraryItem is a completion that will get inserted when the user picks that item from the library.
But you can also optionally specify additional information such as that library item's visibility, a more descriptive title for the item, or its category.
There's a lot to unpack here.
And the best way to discover this API is with a demo.
So let's take a look.
So I am here in the Fruta app that my team has been working on for a while.
This is an app that allows the user to explore different kinds of smoothies.
And in particular, I'm looking at a SmoothieRowView which is a view designed to showcase a particular smoothie with information such as its title, its ingredients, its image, and so on.
This view is used in a couple of places in our app, and so I'd like to add it to the library to make the reuse easier, and I'm going to do that now.
I'm going to scroll to the bottom of this file and add a new type that conforms to the LibraryContentProvider protocol.
This type can be named anything but for the purposes of this demo, I'm going to call it LibraryContent.
Since what I'm adding is a view, I'm going to add a views property.
And now I need to add a LibraryItem.
To finish completing the LibraryItem, I need to provide a completion that represents my view.
Since what I'm adding is a SmoothieRowView, I'm just going to instantiate a SmoothieRowView here.
I'm going to provide some default data here for my smoothie.
I'm going to pick out of the list of preexisting ones.
And I like lemonberry, so I'm just going to pick lemonberry here.
And that's really all we need to create a LibraryItem for this view.
I'm ready to use it.
I'm going to switch to the SmoothieList.
As the name implies, this is just a view that takes an array of smoothies and shows them in a navigable list.
It's empty right now, as you can see from the preview.
I'm going to populate it with my SmoothieRowView.
To do that, I'm going to click on the library icon in the toolbar, which is a little plus, and I'm going to search for smoothie.
As you can see, Xcode already discovered my LibraryItem and populated the library with it.
It even gave it a category, Fruta, which matches the name of my project.
This makes local items from my project easier to find.
It also gave it a rich title, Smoothie Row View, which is based on the completion I provided.
So now I can just drag this library item right into my code and my preview will update automatically to reflect the change that I just made.
Notice that all of the rows in this list right now are saying "Lemonberry," and it's because Xcode inserted exactly what I specified with a lemonberry placeholder.
But there's a slight difference here.
The argument for the SmoothieRowView here, lemonberry, that I provided, is tokenized, and that's because LibraryItems are meant to be a starting point that can be customized based on the insertion context for the view that you're inserting.
So I'm going to select the lemonberry token here, and I'm going to replace it with the data from the list, in this case, just smoothie.
So the preview updates to show me the content of the list with the different smoothies provided from its preview.
That's exactly what I want.
So now let's talk about customization options for these LibraryItems.
And for that, I'm going to go back to the LibraryContent struct that I'm working on.
First, let's talk about the category.
Now the project category is very useful for projects that are small and add a small number of items, such as this demo.
But for a larger project that adds a lot of items, this category will become pretty unwieldy pretty quickly.
If you've ever used SwiftUI's Xcode library, you notice that SwiftUI deals with that by adding categories that correspond to functionality such as controls, layout, effects, and so on.
We can do the same for our LibraryItems as well.
I'm going to go back to my code, and I'm going to add an argument here to specify the category.
And I'm going to pick control because this view is most like control; it provides data and allows me to interact with it.
Now if I bring up the library with a Command-Shift-L shortcut and search for smoothie, you will see that the category of this LibraryItem was updated.
It's still prepended with Fruta, so it's still easy for me to find the local ones.
But now it's clearly indicated that my view is a control.
The icon for the LibraryItem changed as well, to indicate that it's a control with the color blue.
So views don't have to correspond to LibraryItems one-to-one.
It is perfectly reasonable to create multiple LibraryItems representing the same view in different configurations.
In this particular case, SmoothieRowView has another argument that lets me specify whether I want to see local popularity of a particular smoothie.
I'm going to add a LibraryItem for that configuration as well.
I'm going to go back to my views property and add another LibraryItem.
And it also creates a SmoothieRowView, but now it also uses showNearbyPopularity flag and sets its default to true.
If I bring up my library again, I can see that my LibraryItem got added just like I would expect, but there's a problem.
It's really hard for me to tell which LibraryItem represents which instantiation.
I can fix that by adding another argument to this LibraryItem instantiation that specifies its title.
In this case, I'm going to say, "Smoothie Row View With Popularity." And while I'm here, I can also fix its category to match the other SmoothieRowView as well.
Now if I bring up the library, I can see that my LibraryItem got updated, and it's much easier for me to tell which one is which.
So that's a quick intro in how to add a view to the Xcode library.
But what about modifiers? Turns out the idea is very similar but has a little bit of a detail.
Let me set the stage here for a second.
As I audited my code, I found out that a lot of times when we use an image, we also use these three modifiers in a row: resizable, aspectRatio, and frame.
And the result of these modifiers is to resize an image to a particular size while keeping its aspect ratio.
We use it enough that I actually separated this functionality into its own modifier.
I created an extension on Image, and I created a function called resizedtoFill, which takes the width and height.
And this function just takes the image and applies the three modifiers to it and returns the result.
I'd like to add resizedToFill to the modifiers library.
To do that, I'm going to implement the second requirement of the LibraryContentProvider protocol, the modifiers function.
And this is very similar to the views property, but it requires a base argument.
When figuring out what the completion for the LibraryItem should be, Xcode needs a way to be able to tell which part of the completion is the modifier and which part is the thing it modifies.
And the base is the way for us to communicate that.
So since my modifier is declared on Image, I'm going to set this type to Image.
And then I'm going to add my LibraryItem.
Its completion is base with resizedToFill called on it.
And I just need to populate this with sample data.
And just like that, my modifier item is ready to be used.
I'm going to scroll back to my image and delete the three modifiers I'm trying to replace.
I'm going to bring up the library again, but this time I'm going to switch to the modifiers tab and search for resized.
And here's my new modifier.
I can use it right now by hitting Return, and what gets inserted is exactly what I would expect.
Xcode stripped the base part and just inserted the modifier itself and tokenized the arguments so I can customize them here with size, and size, like it was before.
And just like that, I used my modifier from the library.
You may have noticed that at no point during this demo, we had to build or run our project to populate the library.
This is because Xcode can harvest the library definitions by simply scanning your source for LibraryContentProviders and parsing their declarations.
There are several advantages to this.
First, it means that if your project is not in a runnable state, you can still contribute content to the library which is really handy when you're in the middle of that UI rework.
Your project doesn't run, but you still want the content of the library.
It also means that there's no additional build configuration required to enable this feature.
And since LibraryContentProvider code is never actually executed, the compiler will strip it when your project is built for distribution.
So this approach works really well for building a library of content for your workspace or project.
But since Xcode scans all of the source files in your workspace for library content, including any dependencies you have, it actually works really well with Swift packages as well.
In our project, we actually have a dependency on the Nutrition Facts Swift package, which provides facilities for visualizing nutritional information.
I'm looking for a view that will allow me to display caloric information of a smoothie in my row view.
I can use the library to explore the content of Nutrition Facts package for a view I want.
I'm going to bring up the library again, but this time I'm going to switch back to the views tab and scroll down until I find the Nutrition Facts category.
This is a category that Xcode created for the content of the Nutrition Facts Swift package.
I can see that there's a CalorieCountView here which sounds like exactly what I want.
I'm going to hit Return to insert it, and my preview will update to reflect the change that I just made.
This basically looks exactly like what I want, so all I need to do now is to populate this view with the data from my model.
Fortunately, I already have nutrition facts, so I can just type smoothie.nutritionFact here and have my view updated.
And just like that, I was able to discover and use content from a Swift package without ever opening its source code.
To summarize, in this session we've learned how to extend the Xcode library by creating a type that conforms to the LibraryContentProvider protocol, implementing one or both of its requirements, and returning instances of LibraryItem that correspond to the individual items.