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?
Using an Enum to Select the Kind of View
You could solve the problem by creating an enum
for each view you add inside GemList.swift. Depending on the case you pass, you would then add or remove views from the body
.
This solution fixes the problem of passing many Boolean values into the initializer, but it has the same problems the other solution has. You’d have to add new cases to the enum
and more views inside GemList.swift every time a new feature requires a custom view inside the list, making it even bigger.
Fortunately, Swift has a handy feature called @ViewBuilder
.
Understanding ViewBuilder
@ViewBuilder
is a kind of result builder that’s specifically designed to help create child views. Result builders create functions that build a result from a sequence of elements. SwiftUI uses this in its own native views, controls and components. It also uses this in the body
to compose your views.
You can also use result builders to create your own Domain-Specific Language, or DSL. A DSL is like a miniature language-within-a-language used to solve problems within a particular area or domain. This topic, while undeniably fascinating, is outside the scope of this tutorial.
Using ViewBuilder Closures
When you use @ViewBuilder
, you can add a closure in the initializer that builds a view using it inside the body
. That way, instead of hard-coding views inside GemList.swift, you pass this view creation responsibility to the feature that will use it.
Many SwiftUI views already use @ViewBuilder
. Button
, for example, has an initializer, init(action:label:)
, that takes a @ViewBuilder
closure to build its label. Button
can use any view a developer wants as the label of the button.
VStack
and HStack
also use @ViewBuilder
to take all sorts of views as their contents.
Using @ViewBuilder
helps you add views while keeping GemList focused on building a list of gems.
You’ll use @ViewBuilder
in the initializer of a new view, GemList.swift, to take a closure for adding an explanatory message to the user when the Favorites list is empty.
Now that you know about @ViewBuilder
, it’s time to create GemList.swift.
Creating GemList
Now it’s time to add the GemList view to the project. Inside the Views group, create a new SwiftUI view and name it GemList.swift.
Begin by finding preview code:
struct GemList_Previews: PreviewProvider {
static var previews: some View {
GemList()
}
}
Comment out this code for now. Otherwise, your subsequent changes in this section will cause Xcode to complain. Not to worry; you’ll fix the previews later in this tutorial.
Next, find this line:
struct GemList: View {
And replace it with:
struct GemList<Content>: View where Content: View {
let gems: FetchedResults<Gem>
let messageView: Content
This code adds a generic type, Content
, to GemList and adds a constraint so it has to be View
. messageView
stores the view the closure builds in the initializer to use later inside the body
.
It also adds a property, gems
, for storing gems to display them in the list.
Next, add the following initializer just after the declaration of messageView:
init(_ gems: FetchedResults<Gem>, @ViewBuilder messageView: () -> Content) {
self.gems = gems
self.messageView = messageView()
}
This initializer takes a FetchedResults
of gems to populate the list. It also takes a closure marked with @ViewBuilder
to build the message view.
Then you store the view that this closure builds in messageView
.
Now, replace the contents of body
with:
// 1
List {
ForEach(gems) { gem in
// 2
NavigationLink(destination: DetailsView(gem: gem)) {
GemRow(gem: gem)
}
}
// 3
messageView
}
.listStyle(.insetGrouped)
Here’s a breakdown of the code above:
- You declare a
List
with aForEach
view for listing gems. - Here, you use a
NavigationLink
with the custom view,GemRow
, for each row of the list. - Finally, you add the
messageView
you stored from the closure, at the bottom of the list.
Use messageView
, inside the body
, like any other SwiftUI view. If you pass a Text
, Image
, Button
or any combination of views inside the closure, it’ll store in messageView
and add it to the bottom of the list.
The code to build the list, inside the body
of GemList.swift, is identical to what’s inside AllGemsView.swift and FavoriteGems.swift. By extracting this code to GemList.swift, you’re able to use this to replace the repeating code inside both these other views.
There’s no problem using this with FavoriteGems.swift because you can add a view in the messageView
closure. That’s where you’ll show the subview letting the user know how to favorite a gem if she’s not yet done so.
Now, roll up your sleeves. It’s time for some refactoring!
Refactoring the FavoriteGems View
Open FavoriteGems.swift and replace the contents of the body
with:
// 1
GemList(gems) {
// 2
if gems.isEmpty {
EmptyFavoriteMessage()
}
}
// 3
.navigationTitle("Favorites")
Here’s a code breakdown:
- First, you replace
List
with your new view,GemList
, passing the gems you favorited and a closure with a view. - Here, you check if gems is empty. If so, you add
EmptyFavoriteMessage
to display a message explaining how to favorite gems. - Finally, you use
navigationTitle(_:)
to changeGemList
with the title for theNavigationView
.
Build and run. Tap Favorites to see the message explaining how to favorite a gem.
Favorite some gems and open Favorites again to see them.
Success!
FavoriteGems looks the same, but you refactored the code to use the new GemList.swift view.
Time to refactor AllGemsView.swift to use GemList.swift, too.
Refactoring AllGemsView
Open AllGemsView.swift and replace the body
‘s contents with:
// 1
GemList(gems) {
// 2
EmptyView()
}
// 3
.navigationTitle("Gems")
Here’s the breakdown:
- You also replace the
List
, from AllGemsView, withGemList
, passing all gems and a closure. - Inside the closure, you add an
EmptyView
sinceAllGemsView
doesn’t show any message if the list is empty. - Finally, you change
GemList
with the title for theNavigationView
.
Build and run to see your results:
Nice job! You removed the repeating code from AllGemsView.swift and you’re also using GemList
.
Both features use the same view to list gems, removing repeated code and making both views shorter and cleaner.
But you can make AllGemsView.swift even neater!