# Making SwiftUI Previews Work For You

## Introduction

Canvas previews are an in-your-face feature of SwiftUI. When you create a new view, half of the boilerplate code is for the preview. A third of your Xcode real estate is taken up by the preview.

Despite the prominence of the feature, many developers simply delete the preview code from their views and rely on the simulator.

In past releases of Xcode (including the Xcode 13 betas), a reluctance to use previews was understandable. They'd fail for no apparent reason, and the error messages were beyond cryptic.

I've stuck with previews from the start, but at times, they've felt like more effort than they're worth. But, with Xcode 13, I think we should all be using them for all views. In particular, I've noticed:

- They're more reliable.
- The error messages finally make sense.
- Landscape mode is supported.

I consider previews a little like UI unit tests for your views. Like with unit tests, there's some extra upfront effort required, but you get a big payback in terms of productivity and quality.

In this article, I'm going to cover:

- What you can check in your previews (think light/dark mode, different devices, landscape mode, etc.) and how to do it.
- Reducing the amount of boilerplate code you need in your previews.
- Writing previews for stateful apps. (I'll be using Realm, but the same approach can be used with Core Data.)
- Troubleshooting your previews.

One feature I won't cover is using previews as a graphical way to edit views. One of the big draws of SwiftUI is writing everything in code rather than needing storyboards and XML files. Using a drag-and-drop view builder for SwiftUI doesn't appeal to me.

95% of the examples I use in this article are based on a BlackJack training app. You can find the final version in the repo.

> **Build better mobile apps with Atlas Device Sync**: Atlas Device Sync is a fully-managed mobile backend-as-a-service. Leverage out-of-the-box infrastructure, data synchronization capabilities, built-in network handling, and much more to quickly launch enterprise-grade mobile apps. Get started now by build: Deploy Sample for Free!

## Prerequisites

- Xcode 13+
- iOS 15+
- Realm-Cocoa 10.17.0+

Note: 

- I've used Xcode 13 and iOS 15, but most of the examples in this post will work with older versions.
- Previewing in landscape mode is new in Xcode 13.
- The `buttonStyle` modifier is only available in iOS 15.
- I used Realm-Cocoa 10.17.0, but earlier 10.X versions are likely to work. 

## Working with previews

Previews let you see what your view looks like without running it in a simulator or physical device. When you edit the code for your view, its preview updates in real time.

This section shows what aspects you can preview, and how it's done.

### A super-simple preview

When you create a new Xcode project or SwiftUI view, Xcode adds the code for the preview automatically. All you need to do is press the "Resume" button (or CMD-Alt-P).

The preview code always has the same structure, with the `View` that needs previewing (in this case, `ContentView`) within the `previews` `View`:

```swift
struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       ContentView()
   }
}
```

### Views that require parameters

Most of your views will require that the enclosing view pass in parameters. Your preview must do the same—you'll get a build error if you forget.

My `ResetButton` view requires that the caller provides two values—`label` and `resetType`:

```swift
struct ResetButton: View {
   var label: String
   var resetType: ResetType
   ...
}
```

The preview code needs to pass in those values, just like any embedding view:

```swift
struct ResetButton_Previews: PreviewProvider {
   static var previews: some View {
       ResetButton(label: "Reset All Matrices",
                   resetType: .all)
   }
}
```

### Views that require `Binding`s

In a chat app, I have a `LoginView` that updates the `username` binding that's past from the enclosing view:

```swift
struct LoginView: View {  
   @Binding var username: String
   ...
}
```

The simplest way to create a binding in your preview is to use the `constant` function:

```swift
struct LoginView_Previews: PreviewProvider {
   static var previews: some View {
       LoginView(username: .constant("Billy"))
   }
}
```

### `NavigationView`s

In your view hierarchy, you only add a `NavigationView` at a single level. That `NavigationView` then wraps all subviews.

When previewing those subviews, you may or may not care about the `NavigationView` functionality. For example, you'll only see titles and buttons in the top nav bar if your preview wraps the view in a `NavigationView`.

If I preview my `PracticeView` without adding a `NavigationView`, then I don't see the title:

To preview the title, my preview code needs to wrap `PracticeView` in a `NavigationView`:

```swift
struct PracticeView_Previews: PreviewProvider {
   static var previews: some View {
       NavigationView {
           PracticeView()
       }
   }
}
```

### Smaller views

Sometimes, you don't need to preview your view in the context of a full device screen. My `CardView` displays a single playing card. Previewing it in a full device screen just wastes desk space: 

We can add the `previewLayout` modifier to indicate that we only want to preview an area large enough for the view. It often makes sense to add some `padding` as well:

```swift
struct CardView_Previews: PreviewProvider {
   static var previews: some View {
       CardView(card: Card(suit: .heart))
           .previewLayout(.sizeThatFits)
           .padding()
   }
}
```

### Light and dark modes

It can be quite a shock when you finally get around to testing your app in dark mode. If you've not thought about light/dark mode when implementing each of your views, then the result can be ugly, or even unusable.

Previews to the rescue!

Returning to `CardView`, I can preview a card in dark mode using the `preferredColorScheme` view modifier:

```swift
struct CardView_Previews: PreviewProvider {
   static var previews: some View {
       CardView(card: Card(suit: .heart))
           .preferredColorScheme(.dark)
           .previewLayout(.sizeThatFits)
           .padding()
   }
}
```

That seems fine, but what if I previewed a spade instead?

That could be a problem.

Adding a white background to the view fixes it:

### Preview multiple view instances

Sometimes, previewing a single instance of your view doesn't paint the full picture. Just look at the surprise I got when enabling dark mode for my card view. Wouldn't it be better to simultaneously preview both hearts and spades in both dark and light modes?

You can create multiple previews for the same view using the `Group` view:

```swift
struct CardView_Previews: PreviewProvider {
   static var previews: some View {
       Group {
           CardView(card: Card(suit: .heart))
           CardView(card: Card(suit: .spade))
           CardView(card: Card(suit: .heart))
               .preferredColorScheme(.dark)
           CardView(card: Card(suit: .spade))
               .preferredColorScheme(.dark)
       }
       .previewLayout(.sizeThatFits)
       .padding()
   }
}
```

### Composing views in a preview

A preview of a single view in isolation might look fine, but what will they look like within a broader context?

Previewing a single `DecisionCell` view looks great:

```swift
struct DecisionCell_Previews: PreviewProvider {
   static var previews: some View {
       DecisionCell(
           decision: Decision(handValue: 6, dealerCardValue: .nine, action: .hit), myHandValue: 8, dealerCardValue: .five)
           .previewLayout(.sizeThatFits)
           .padding()
   }
}
```

But, the app will never display a single `DecisionCell`. They'll always be in a grid. Also, the text, background color, and border vary according to state. To create a more realistic preview, I created some sample data within the view and then composed multiple `DecisionCell`s using vertical and horizontal stacks:

```swift
struct DecisionCell_Previews: PreviewProvider {
   static var previews: some View {
       let decisions: Decision] = [
           Decision(handValue: 6, dealerCardValue: .nine, action: .split),
           Decision(handValue: 6, dealerCardValue: .nine, action: .stand),
           Decision(handValue: 6, dealerCardValue: .nine, action: .double),
           Decision(handValue: 6, dealerCardValue: .nine, action: .hit)
       ]
       return Group {
           VStack(spacing: 0) {
               ForEach(decisions) { decision in
                   HStack (spacing: 0) {
                       DecisionCell(decision: decision, myHandValue: 8, dealerCardValue: .three)
                       DecisionCell(decision: decision, myHandValue: 6, dealerCardValue: .three)
                       DecisionCell(decision: decision, myHandValue: 8, dealerCardValue: .nine)
                       DecisionCell(decision: decision, myHandValue: 6, dealerCardValue: .nine)
                   }
               }
           }
           VStack(spacing: 0) {
               ForEach(decisions) { decision in
                   HStack (spacing: 0) {
                       DecisionCell(decision: decision, myHandValue: 8, dealerCardValue: .three)
                       DecisionCell(decision: decision, myHandValue: 6, dealerCardValue: .three)
                       DecisionCell(decision: decision, myHandValue: 8, dealerCardValue: .nine)
                       DecisionCell(decision: decision, myHandValue: 6, dealerCardValue: .nine)
                   }
               }
           }
           .preferredColorScheme(.dark)
       }
       .previewLayout(.sizeThatFits)
       .padding()
   }
```

I could then see that the black border didn't work too well in dark mode:

![Dark border around selected cells is lost in front of the dark background

Switching the border color from `black` to `primary` quickly fixed the issue:

### Landscape mode

Previews default to portrait mode. Use the `previewInterfaceOrientation` modifier to preview in landscape mode instead:

```swift
struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       ContentView()
           .previewInterfaceOrientation(.landscapeRight)
   }
}
```

### Device type

Previews default to the simulator device that you've selected in Xcode. Chances are that you want your app to work well on multiple devices. Typically, I find that there's extra work needed to make an app I designed for the iPhone work well on an iPad.

The `previewDevice` modifier lets us specify the device type to use in the preview:

```swift
struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       ContentView()
           .previewDevice(PreviewDevice(rawValue: "iPad (9th generation)"))
   }
}
```

You can find the names of the available devices from Xcode's simulator menu, or from the terminal using `xcrun simctl list devices`.

### Pinning views

In the bottom-left corner of the preview area, there's a pin button. Pressing this "pins" the current preview so that it's still shown when you browse to the code for other views:

This is useful to observe how a parent view changes as you edit the code for the child view:

### Live previews

At the start of this article, I made a comparison between previews and unit testing. Live previews mean that you really can test your views in isolation (to be accurate, the view you're testing plus all of the views it embeds or links to).

Press the play button above the preview to enter live mode:

You can now interact with your view:

## Getting rid of excess boilerplate preview code

As you may have noticed, some of my previews now have more code than the actual views. This isn't necessarily a problem, but there's a lot of repeated boilerplate code used by multiple views. Not only that, but you'll be embedding the same boilerplate code into previews in other projects.

To streamline my preview code, I've created several view builders. They all follow the same pattern—receive a `View` and return a new `View` that's built from that `View`.

I start the name of each view builder with `_Preview` to make it easy to take advantage of Xcode's code completion feature.

### Light/dark mode

`_PreviewColorScheme` returns a `Group` of copies of the view. One is in light mode, the other dark:

```swift
struct _PreviewColorScheme: View {
   private let viewToPreview: Value

   init(_ viewToPreview: Value) {
       self.viewToPreview = viewToPreview
   }

   var body: some View {
       Group {
           viewToPreview
           viewToPreview.preferredColorScheme(.dark)
       }
   }
}
```

To use this view builder in a preview, simply pass in the `View` you're previewing:

```swift
struct CardView_Previews: PreviewProvider {
   static var previews: some View {
       _PreviewColorScheme(
           VStack {
               ForEach(Suit.allCases, id: \.rawValue) { suit in
                   CardView(card: Card(suit: suit))
               }
           }
               .padding()
               .previewLayout(.sizeThatFits)
       )
   }
}
```

### Orientation

`_PreviewOrientation` returns a `Group` containing the original `View` in portrait and landscape modes:

```swift
struct _PreviewOrientation: View {
   private let viewToPreview: Value

   init(_ viewToPreview: Value) {
       self.viewToPreview = viewToPreview
   }

   var body: some View {
       Group {
           viewToPreview
           viewToPreview.previewInterfaceOrientation(.landscapeRight)
       }
   }
}
```

To use this view builder in a preview, simply pass in the `View` you're previewing:

```swift
struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       _PreviewOrientation(
           ContentView()
       )
   }
}
```

### No device

`_PreviewNoDevice` returns a view built from adding the `previewLayout` modifier and adding `padding to the input view:

```swift
struct _PreviewNoDevice: View {
   private let viewToPreview: Value

   init(_ viewToPreview: Value) {
       self.viewToPreview = viewToPreview
   }

   var body: some View {
       Group {
           viewToPreview
               .previewLayout(.sizeThatFits)
               .padding()
       }
   }
}
```

To use this view builder in a preview, simply pass in the `View` you're previewing:

```swift
struct CardView_Previews: PreviewProvider {
   static var previews: some View {
       _PreviewNoDevice(
           CardView(card: Card())
       )
   }
}
```

### Multiple devices

`_PreviewDevices` returns a `Group` containing a copy of the `View` for each device type. You can modify `devices` in the code to include the devices you want to see previews for:

```swift
struct _PreviewDevices: View {
   let devices = 
       "iPhone 13 Pro Max",
       "iPhone 13 mini",
       "iPad (9th generation)"
   ]

   private let viewToPreview: Value

   init(_ viewToPreview: Value) {
       self.viewToPreview = viewToPreview
   }

   var body: some View {
       Group {
           ForEach(devices, id: \.self) { device in
               viewToPreview
                   .previewDevice(PreviewDevice(rawValue: device))
                   .previewDisplayName(device)
           }
       }
   }
}
```

I'd be cautious about adding too many devices as it will make any previews using this view builder slow down and consume resources.

To use this view builder in a preview, simply pass in the `View` you're previewing:

```swift
struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       _PreviewDevices(
           ContentView()
       )
   }
}
```

![The same view previewed on 3 different device types

### Combining multiple view builders

Each view builder receives a view and returns a new view. That means that you can compose the functions by passing the results of one view builder to another. In the extreme case, you can use up to three on the same view preview:

```swift
struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       _PreviewOrientation(
           _PreviewColorScheme(
               _PreviewDevices(ContentView())
           )
       )
   }
}
```

This produces 12 views to cover all permutations of orientation, appearance, and device.

For each view, you should consider which modifiers add value. For the `CardView`, it makes sense to use `_PreviewNoDevice` and `_PreviewColorSchem`e, but previewing on different devices and orientations wouldn't add any value.

## Previewing stateful views (Realm)

Often, a SwiftUI view will fetch state from a database such as Realm or Core Data. For that to work, there needs to be data in that database.

Previews are effectively running on embedded iOS simulators. That helps explain how they are both slower and more powerful than you might expect from a "preview" feature. That also means that each preview also contains a Realm database (assuming that you're using the Realm-Cocoa SDK). The preview can store data in that database, and the view can access that data.

In the BlackJack training app, the action to take for each player/dealer hand combination is stored in Realm. For example, `DefaultDecisionView` uses `@ObservedResults` to access data from Realm:

```swift
struct DefaultDecisionView: View {
   @ObservedResults(Decisions.self,
                    filter: NSPredicate(format: "isSoft == NO AND isSplit == NO")) var decisions
```

To ensure that there's data for the previewed view to find, the preview checks whether the Realm database already contains data (`Decisions.areDecisionsPopulated`). If not, then it adds the required data (`Decisions.bootstrapDecisions()`):

```swift
struct DefaultDecisionView_Previews: PreviewProvider {
   static var previews: some View {
       if !Decisions.areDecisionsPopulated {
           Decisions.bootstrapDecisions()
       }
       return _PreviewOrientation(
           _PreviewColorScheme(
               Group {
                   NavigationView {
                       DefaultDecisionView(myHandValue: 6, dealerCardValue: .nine)
                   }
                   NavigationView {
                       DefaultDecisionView(myHandValue: 6, dealerCardValue: .nine, editable: true)
                   }
               }
                   .navigationViewStyle(StackNavigationViewStyle())
           )
       )
   }
}
```

`DefaultDecisionView` is embedded in `DecisionMatrixView` and so the preview for `DecisionMatrixView` must also conditionally populate the Realm data. In turn, `DecisionMatrixView` is embedded in `PracticeView`, and `PracticeView` in `ContentView`—and so, they too need to bootstrap the Realm data so that it's available further down the view hierarchy.

This is the implementation of the bootstrap functions:

```swift
extension Decisions {
   static var areDecisionsPopulated: Bool {
       do {
           let realm = try Realm()
           let decisionObjects = realm.objects(Decisions.self)
           return decisionObjects.count >= 3
       } catch {
           print("Error, couldn't read decision objects from Realm: \(error.localizedDescription)")
           return false
       }
   }

   static func bootstrapDecisions() {
       do {
           let realm = try Realm()
           let defaultDecisions = Decisions()
           let softDecisions = Decisions()
           let splitDecisions = Decisions()

           defaultDecisions.bootstrap(defaults: defaultDefaultDecisions, handType: .normal)
           softDecisions.bootstrap(defaults: defaultSoftDecisions, handType: .soft)
           splitDecisions.bootstrap(defaults: defaultSplitDecisions, handType: .split)
           try realm.write {
               realm.delete(realm.objects(Decision.self))
               realm.delete(realm.objects(Decisions.self))
               realm.delete(realm.objects(Decision.self))
               realm.delete(realm.objects(Decisions.self))
               realm.add(defaultDecisions)
               realm.add(softDecisions)
               realm.add(splitDecisions)
           }
       } catch {
           print("Error, couldn't read decision objects from Realm: \(error.localizedDescription)")
       }
   }
}
```

### Partitioned, synced realms

The BlackJack training app uses a standalone Realm database. But what happens if the app is using Realm Sync?

One option could be to have the SwiftUI preview sync data with your backend Realm service. I think that's a bit too complex, and it breaks my paradigm of treating previews like unit tests for views.

I've found that the simplest solution is to make the view aware of whether it's been created by a preview or by a running app. I'll explain how that works.

`AuthorView` from the RChat app fetches data from Realm:

```swift
struct AuthorView: View {
   @ObservedResults(Chatster.self) var chatsters
   ...
}
```

Its preview code bootstraps the embedded realm:

```swift
struct AuthorView_Previews: PreviewProvider {
   static var previews: some View {
       Realm.bootstrap()

       return AppearancePreviews(AuthorView(userName: "rod@contoso.com"))
           .previewLayout(.sizeThatFits)
           .padding()
   }
}
```

The app adds bootstrap as an extension to Realm:

```swift
extension Realm: Samplable {
   static func bootstrap() {
       do {
           let realm = try Realm()
           try realm.write {
               realm.deleteAll()
               realm.add(Chatster.samples)
               realm.add(User(User.sample))
               realm.add(ChatMessage.samples)
           }
       } catch {
           print("Failed to bootstrap the default realm")
       }
   }
}
```

A complication is that `AuthorView` is embedded in `ChatBubbleView`. For the app to work, `ChatBubbleView` must pass the synced realm configuration to `AuthorView`:

```swift
AuthorView(userName: authorName)
   .environment(\.realmConfiguration,
                   app.currentUser!.configuration(
                   partitionValue: "all-users=all-the-users"))
```

**But**, when previewing `ChatBubbleView`, we want `AuthorView` to use the preview's local, embedded realm (not to be dependent on a Realm back-end app). That means that `ChatBubbleView` must check whether or not it's running as part of a preview:

```swift
struct ChatBubbleView: View {
   ...
   var isPreview = false
   ...
   var body: some View {
       ...
       if isPreview {
           AuthorView(userName: authorName)
       } else {
           AuthorView(userName: authorName)
               .environment(\.realmConfiguration,
                   app.currentUser!.configuration(
                       partitionValue: "all-users=all-the-users"))
       }
       ...
   }
}
```

The preview is then responsible for bootstrapping the local realm and flagging to `ChatBubbleView` that it's a preview:

```swift
struct ChatBubbleView_Previews: PreviewProvider {
   static var previews: some View {
       Realm.bootstrap()
       return ChatBubbleView(
           chatMessage: .sample,
           authorName: "jane",
           isPreview: true)
   }
}
```

## Troubleshooting your previews

As mentioned at the beginning of this article, the error messages for failed previews are actually useful in Xcode 13.

That's the good news. 

The bad news is that you still can't use breakpoints or print to the console.

One mitigation is that the `previews` static var in your preview is a `View`. That means that you can replace the `body` of your `ContentView` with your `previews` code. You can then run the app in a simulator and add breakpoints or print to the console. It feels odd to use this approach, but I haven't found a better option yet.

## Conclusion

I've had a mixed relationship with SwiftUI previews.

When they work, they're a great tool, making it quicker to write your views. Previews allow you to unit test your views. Previews help you avoid issues when your app is running in dark or landscape mode or on different devices.

But, they require effort to build. Prior to Xcode 13, it would be tough to justify that effort because of reliability issues.

I believe that Xcode 13 is the tipping point where the efficiency and quality gains far outweigh the effort of writing preview code. That's why I've written this article now.

In this article, you've seen a number of tips to make previews as useful as possible. I've provided four view builders that you can copy directly into your SwiftUI projects, letting you build the best previews with the minimum of code. Finally, you've seen how you can write previews for views that work with data held in a database such as Realm or Core Data.

Please provide feedback and ask any questions in the Realm Community Forum.