ViewBuilder Tutorial: Creating Reusable SwiftUI Views
Learn how to use ViewBuilder to create reusable SwiftUI views. Understand how to refactor views, including how to use type constraints and convenience initializers. By Renan Benatti Dias.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
ViewBuilder Tutorial: Creating Reusable SwiftUI Views
25 mins
- Getting Started
- Exploring RayGem’s Views
- Understanding the Problem
- Exploring Possible Solutions
- Using a Boolean to Add and Remove Views
- Using an Enum to Select the Kind of View
- Understanding ViewBuilder
- Using ViewBuilder Closures
- Creating GemList
- Refactoring the FavoriteGems View
- Refactoring AllGemsView
- Adding a Convenience Initializer to GemList
- Creating the Search Feature
- Building SearchView
- The Type Problem
- Updating GemList to Accept Other Collection Types
- Using GemList Inside the Search Feature
- Where to Go From Here?
SwiftUI, Apple’s latest UI framework, helps you quickly build simple and more complex views. But what about when different features in your app need nearly the same view? That’s where @ViewBuilder
, an essential underlying component of SwiftUI itself, can help.
Creating SwiftUI views that work across multiple features can be challenging because each feature has different use cases. You might end up copying code, modifying for each feature. The right way to solve this problem is by extracting repeatable code into reusable views.
In this tutorial, you’ll learn:
- How to identify what portion of a view can be abstracted and made reusable.
- How to refactoring repeating code.
- How to use closures and @ViewBuilder to build views.
- What generic types are and how to use type constraints to build a view.
You’ll learn all of this while building RayGem, an app that displays information about different gemstones. Rockhounds and Crystal Gem fans, you’ll love this one!
Getting Started
To get started, download the project materials by clicking Download Materials at the top or bottom of this tutorial. Then, open the starter project in Xcode.
Build and run. You’ll see:
RayGem lists a collection of gems, which are precious or semiprecious stones. Users can read interesting facts and save their favorites.
However, this project has a problem: it repeats code across multiple features.
You’ll fix this problem as you complete the tutorial. But before you start refactoring code and adding new stuff, it’s important to understand the features that are already there.
Exploring RayGem’s Views
The first view you’ll explore, AllGems, holds the app’s main list. Users can scroll, find all gems and tap one to open its detail view.
AllGems uses a simple List
view with a custom view for each row, GemRow. This custom view displays the gem’s image, name and color.
Next, you have the Favorites list. You’ll find the gems you have favorited by tapping the heart button at the top of the details view. When the list is empty, you’ll see a message explaining how to favorite gems in the view.
The Favorites list also uses a simple List
with the same custom GemRow view to display the rows.
Finally, the Search feature helps users find gems by name using a search bar in the navigation view.
Open the search tab, and you’ll find a placeholder view:
You’ll build this feature later in the tutorial.
Now that you understand the features, take a closer look at the problem.
Understanding the Problem
RayGem feels like a well-designed, feature-rich app. Each view displays a simple list that uses the same custom view for its rows, creating a consistent user experience throughout the app.
But, there’s a small problem.
Open AllGemsView.swift and look at the body
:
var body: some View {
List {
ForEach(gems) { gem in
NavigationLink(destination: DetailsView(gem: gem)) {
GemRow(gem: gem)
}
}
}
.navigationTitle("Gems")
}
Now, look at FavoriteGems.swift‘s body
:
var body: some View {
List {
if gems.isEmpty {
EmptyFavoriteMessage()
}
ForEach(gems) { gem in
NavigationLink(destination: DetailsView(gem: gem)) {
GemRow(gem: gem)
}
}
}
.navigationTitle("Favorites")
}
Both have the same code, a List
with a ForEach
to iterate over the gems. Both also use GemRow
for the row of the list.
The only difference is that Favorites shows a message if you don’t have favorite gems.
If you continued this way, the Search feature would also use the same list and custom row to display results.
RayGem has code repeating not only in two features, but would also have it in the new Search feature.
Why is this a problem?
Well, code duplication is a code smell, which means it indicates a deeper problem within the coding. It may cause inconsistency later if you update one of the lists but forget to make the same changes in the others.
Both lists should appear identical, changing only the data they present. By repeating the code that builds them, you’re obliged to update everywhere you repeat the same code whenever you have to add or change something.
That would mean updating AllGemsView, FavoriteGems and SearchView every time you want to change something in the List
view.
The source code would be larger, harder to maintain and take longer to build.
Exploring Possible Solutions
SwiftUI makes it easy to refactor and reuse code in many features. If both views only used List
to display gems, with no other view inside, it would be straightforward to extract the code into another view and use it in both features.
But, FavoriteGems.swift has a conditional view that shows in the list if there’s no favorite gem to show yet. The message view explains how to favorite a gem.
Before you start building a new view to extract this code, you’ll go through a couple of possible solutions to this problem.
Using a Boolean to Add and Remove Views
One way to solve this problem is by creating a new view for listing gems and passing a Boolean in the initializer to add or remove the message view.
struct GemList: View {
let gems: [Gem]
let showMessage: Bool
var body: some View {
// 1
List {
// 2
ForEach(gems) { gem in
NavigationLink(destination: DetailsView(gem: gem)) {
GemRow(gem: gem)
}
}
// 3
if showMessage {
EmptyFavoriteMessage()
}
}
.listStyle(.insetGrouped)
}
}
This new view extracts the repeated code from AllGemsView.swift and FavoriteGems.swift. It builds the list and uses a Boolean to add and remove a view at the bottom. Here’s how this code works in detail:
- You use a
List
as the root of the view. - Next, you build each row of the list using the array of Gems.
- Finally, you use the new
showMessage
Boolean to add or removeEmptyFavoriteMessage
That way, you’d always pass false
inside AllGemsView.swift, and true
if gems is empty inside FavoriteGems.swift:
struct FavoriteGems: View {
// Properties...
var body: some View {
GemList(gems: gems, showMessage: gems.isEmpty)
.navigationTitle("Favorites")
}
}
Here, you remove the repeated code from FavoriteGems.swift and use the new GemList
view to build the list, passing the gems and a Boolean to add or remove the view at the bottom.
This solution has a couple of problems:
- Adding a
Boolean
means that if you add or remove other views for other purposes, you have to add aBoolean
to each view. - You’re tied to the view already built into
GemList
. If you add other views for other features, you’d have to change the code inside GemList.swift.
That also means GemList.swift will grow as you add more features and they need to be hard-coded into the new view, inside the body
.