Adding Custom Views to the Library in Xcode 12

What do you prefer: creating user interfaces visually or in code?

This question has split the Apple developer community for years. Could this year’s WWDC finally mark the end of that?

Last year, Apple made a huge step forward in UI development with the introduction of SwiftUI: For the first time, the visual approach and the coding approach spoke the same language. It doesn’t matter if you add a button in code or by dragging it to the design canvas – both representations are always in sync.

This made it possible for a developer who prefers the visual approach and another one who prefers the code approach to work on the same project without any conflict. Yet, one important thing was still missing: The ability to simply drag and drop your own views onto the canvas.

With WWDC 2020, Apple has closed this gap – the feature is finally here! So let’s dive right in and see how to add your custom views in SwiftUI to the Xcode library!

Here’s our sample project that we’re going to refer to:

Step 1: Create Custom Views in SwiftUI

In order to add custom views to the library, we first need a project. In this example, we’re going to create a simple app showcasing all our team members in a neat list.

TeamList

We start with creating a new custom view called Row which we will later use for displaying an indiviual team member. It consists of a profile image, a title (for the team member’s name) and a subtitle (for the job description).

struct Row: View {
    var title: String
    var subtitle: String
    var image: Image

    var body: some View {
        HStack(spacing: 8) {
            image
                .resizable()
                .frame(width: 75, height: 75, alignment: .center)
            VStack(alignment: .leading, spacing: 4) {
                Text(title)
                Text(subtitle)
                    .opacity(0.5)
            }
            .font(.headline)
            Spacer()
        }
        .padding(8)
    }
}

This view simply adds several (sub)views and lays them out the way we want. But what’s still missing is the “card look”: the rounded rectangle frame around it and the subtle shadow.

To keep our Row view highly reusable and customizable, we don’t add the code for that to the view itself. Instead, we create a view modifier .card() for that which we can apply not only to a row, but to other views as well.

struct Card: ViewModifier {
    let cornerRadius: CGFloat

    func body(content: Content) -> some View {
        content
            .background(Color(.systemBackground))
            .cornerRadius(cornerRadius)
            .shadow(color: Color.primary.opacity(0.2), radius: 4)
    }
}

extension View {
    func card(cornerRadius: CGFloat = 8) -> some View {
        modifier(Card(cornerRadius: cornerRadius))
    }
}

With these components, we can now build a TeamMemberCell view, which combines the two things together:

struct TeamMemberCell: View {
    let teamMember: TeamMember

    var body: some View {
        Row(title: teamMember.name, subtitle: teamMember.position, image: teamMember.image)
            .card(cornerRadius: 8)
    }
}

Finally, we create a TeamList which creates a TeamMemberCell for each of our team members and arranges them in a vertical, scrollable stack. You can see how we did that in our sample project. We’ll skip that part in this article.

Step 2: Add Items to the Xcode View Library

All we’ve done so far isn’t new and could be accomplished with Xcode 11 as well. Now comes the cool part: Instead of composing the TeamMemberCell in code, we want to be able to assemble it with drag & drop in the design canvas. Before we can do that, we first need to let Xcode know about the custom views and modifiers we want to add to the view library.

For that, the new API LibraryContentProvider was introduced in Xcode 12. The following example adds the Row view and the .card() modifier to the library, alongside another custom view AppButton and a custom .loading modifier. (Feel free to check out the code for the latter two in our sample project.)

struct ViewProvider: LibraryContentProvider {

    @LibraryContentBuilder var views: [LibraryItem] {
        LibraryItem(Row(title: "", subtitle: "", image: Image("")))
        LibraryItem(AppButton(Text(""), action: {}))
    }

    func modifiers<V: View>(base: V) -> [LibraryItem] {
        [
            LibraryItem(base.card(cornerRadius: 8)),
            LibraryItem(base.loading(false))
        ]
    }

}

As you can see, we need to make a type conform to the LibraryContentProvider protocol to add new items to the Xcode library. However, we can’t just make our views and modifiers conform to that protocol (which we first assumed). Instead, we need to create a separate structure (which we called ViewProvider) that implements this protocol. In its view property, we return all our custom views that we want to add to the library, wrapped in a LibraryItem. Similarly, we return all view modifiers that we want to have in the library from the modifiers function.

Note: For the views property, we use the LibraryContentBuilder function builder similar to how ViewBuilder can be used to add multiple views as children of a VStack, HStack or ZStack. That’s why there are no array brackets around the library items.

In our ViewBuilder example, we chose the easiest way to create each LibraryItem by using the initializer with only a single parameter. That works, but we can also customize how that library item appears in Xcode by providing more context. Let’s take a closer look at the LibraryItem initializer:

LibraryItem(
    Row(title: "", subtitle: "", image: Image("")), // the code to create the view
    visible: true,                                  // whether it's visible in the Xcode library
    title: "My Awesome Team Member Row",            // the custom name shown in the library
    category: .control,                             // a category to find you custom views faster
    matchingSignature: ""                           // the signature for code completion
)

First of all, we can define what code snippet should be added to the SwiftUI code when using the LibraryItem. We can further specify whether the item should only be visible for code completion or also appear when using the design canvas. Since the default value for the visible attribute is true, we usually omit this parameter. It is also possible to define a custom title, as well as a category (including .effect, .layout, .control and the default .other). The category will be used for sorting the views and modifiers and also change their color in the Xcode library. A matching signature can also be added to determine which shortcut should trigger the code completion for the given modifier or view.

Step 3: Compose Views with Xcode 12

After building the project once, Row, AppButton, .card and .loading are available in the Xcode library, alongside all the native views! We can now use them to visually compose our TeamMemberCell.

With the design canvas open, you can add any of our custom views and modifiers to it by clicking the [+] button and simply dragging the respective item over to the canvas.

Ain’t that cool?! 😎

Note: We noticed that the library items sometimes don’t show up in the Xcode library immediately. This is probably because Xcode 12 is still in Beta and will hopefully be resolved soon. If it doesn’t work for you right from the beginning, don’t give up! Quit and reopen Xcode, clean, build, change a little bit of code – and we promise it’ll work eventually. 😉

Conclusion

By providing the same tooling for custom views and modifiers as provided for native components in addition to many other changes to the whole toolchain, SwiftUI finally feels like a first-class citizen when it comes to UI development on iOS. It was a long way to go, but in the year 2020, visual user interface development with Xcode is feature-equivalent to creating SwiftUI views in code.

Share via
Copy link
Powered by Social Snap

Get notified when our next article is born!

(no spam, just one app-development-related article
per month)