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.

5 (6) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. You declare a List with a ForEach view for listing gems.
  2. Here, you use a NavigationLink with the custom view, GemRow, for each row of the list.
  3. 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:

  1. First, you replace List with your new view, GemList, passing the gems you favorited and a closure with a view.
  2. Here, you check if gems is empty. If so, you add EmptyFavoriteMessage to display a message explaining how to favorite gems.
  3. Finally, you use navigationTitle(_:) to change GemList with the title for the NavigationView.

Build and run. Tap Favorites to see the message explaining how to favorite a gem.

Empty favorite gems list with a message explaining how to favorite a gem.

Favorite some gems and open Favorites again to see them.

List of favorite gems with 3 rows

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:

  1. You also replace the List, from AllGemsView, with GemList, passing all gems and a closure.
  2. Inside the closure, you add an EmptyView since AllGemsView doesn’t show any message if the list is empty.
  3. Finally, you change GemList with the title for the NavigationView.

Build and run to see your results:

A tab view with a list of gems

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!