Xcode Previews for View Controllers

One of the features of SwiftUI that gets a lot of attention is the Xcode support for previewing your layouts without the need to use the simulator. Let’s look at how to add support to your UIKit views controllers and views so they also work with Xcode previews.

Prerequisites

Let’s get the bad news out of the way. As well as needing Xcode 11 you need to be on macOS Catalina and have a minimum deployment target of iOS 13. It’s probably a good idea to also limit the preview support to a debug build so you don’t ship unnecessary code in the release build of an App.

Previewing A View Controller

To make your view controllers work with Xcode previews:

  1. Make them representable in SwiftUI’s layout system by conforming them to UIViewControllerRepesentable.
  2. Add an Xcode preview provider that conforms to PreviewProvider.

Let’s try this with the terms view controller I created when Getting Started with Combine:

User interface with two switches (both off), a text field (empty) and a submit button (disabled)

UIViewControllerRepresentable

As a first step we need to conform the view controller to the UIViewControllerRepesentable protocol to provide a view that works with the SwiftUI layout system. After importing SwiftUI, we need two methods to make and update the view controller:

#if DEBUG
import SwiftUI

extension TermsViewController: UIViewControllerRepresentable {
  func makeUIViewController(context: Context) -> TermsViewController {
  }

  func updateUIViewController(_ uiViewController: TermsViewController,
    context: Context) {
  }
}
#endif

Since I only want to preview the view controller we don’t need to implement the update method. Use the make method to create an instance of the view controller, providing any configuration or model data. In this case I’m using a storyboard:

func makeUIViewController(context: Context) -> TermsViewController {
  let storyboard = UIStoryboard(name: "Main", bundle: nil)
  guard let viewController =  storyboard.instantiateViewController(
    identifier: "TermsViewController") as? TermsViewController else {
      fatalError("Cannot load from storyboard")
  }
  // Configure the view controller here
  return viewController
}

PreviewProvider

To make our view controller show up in Xcode previews we need a preview provider. Create a new type that conforms to the PreviewProvider protocol. The static previews property returns the SwiftUI view that Xcode will show in the preview canvas:

// TermsViewControllerPreviews.swift
#if DEBUG
import SwiftUI
struct TermsViewControllerPreviews: PreviewProvider {
  static var previews: some View {
    TermsViewController()    
  }
}
#endif

Returning a TermsViewController will preview the instance we created in makeUIViewController in the preview pane:

Xcode preview of the view controller

If you don’t see the preview canvas you can show it using the editor options in the top-right corner. You may need to resume the preview (⌥⌘P). Use the pin tool to keep the canvas visible if you switch back to the view controller. Click the live preview button in the canvas to run and interact with the view (it can take a while to start):

Live preview

Note: I’m using Swift but this also works if you’ve written your view controller in Objective-C.

Change Preview Environment

Finally we can view our layout with different environments. The easiest way is to create a group view:

struct TermsViewControllerPreviews: PreviewProvider {
  static var previews: some View {
    Group {
      // Create views here
    }
  }
}

Let’s pick small, medium and large device sizes:

let devices = [
  "iPhone SE",
  "iPhone 11",
  "iPad Pro (11-inch) (2nd generation)"
]

Then inside the Group block we loop over the device names creating previews for each preview device:

ForEach(devices, id: \.self) { name in
  TermsViewController()
  .previewDevice(PreviewDevice(rawValue: name))
  .previewDisplayName(name)
}

Note that we also set the preview display name that Xcode shows below each preview in the canvas:

Xcode previews for three devices

We can do the same for different dynamic type sizes:

let sizeCategories: [ContentSizeCategory] = [
  .extraSmall,
  .extraExtraExtraLarge,
  .accessibilityExtraExtraExtraLarge
]

Then in the Group block we override the environment:

ForEach(sizeCategories, id: \.self) { size in
  TermsViewController()
  .environment(\.sizeCategory, size)
  .previewDisplayName("\(size)")
}

Xcode preview of three dynamic type sizes

One more example, previewing light and dark modes by overriding the SwiftUI ColorScheme:

ForEach(ColorScheme.allCases, id: \.self) { scheme in
  TermsViewController()
  .environment(\.colorScheme, scheme)
  .previewDisplayName("\(scheme)")
}

Xcode preview - dark mode

Previewing A View

The steps to preview a view are similar except you adopt the UIViewRepresentable protocol. For example, I have a rating view that a user can tap to rate something:

Rating View showing five stars

It takes a few lines of code to make it representable using a convenience initializer to set the initial rating:

extension RatingView: UIViewRepresentable {
  func makeUIView(context: Context) -> RatingView {
    RatingView(rating: 3)
  }

  func updateUIView(_ uiView: RatingView, context: Context) {
  }
}

Creating the preview provider:

import SwiftUI
struct RatingViewPreview: PreviewProvider {
  static var previews: some View {
    Group {
      RatingView()
        .previewLayout(.sizeThatFits)
        .previewDisplayName("Fitting size")
    }
  }
}

The default preview layout shows the view at the device size. You can change this to show the view at a fixed size or the fitting size using .previewLayout:

.previewLayout(.device) // default
.previewLayout(.fixed(width: 320, height: 320))
.previewLayout(.sizeThatFits)

My rating view has an intrinsic size it wants to be so I’m using the fitting size. I found this only works if you specify the content hugging priority when creating the view. Otherwise the preview stretches to the device size:

func makeUIView(context: Context) -> RatingView {
  let view = RatingView(rating: 3)
  view.setContentHuggingPriority(.defaultHigh, for: .vertical)
  view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  return view
}

Preview at fitting size

Get the Code

You can find the updated Xcode project in my GitHub repository:

Read More