Igor Skvortsov/Guide - Using Preview for UIKit Views and ViewControllers

Created Tue, 08 Jul 2025 00:00:00 +0000 Modified Tue, 08 Jul 2025 22:46:25 +0000

A simple way to preview your UIKit views without running the whole app.

Illustration

We recently started modularizing our app at work. One of the first steps was moving most of our reusable UI components — buttons, input field combinations, loading indicators — into a separate Swift Package. That package is now included in our main app as a dependency.

And let me tell you — it was the right move.

Not only does this improve build times, simplify dependencies, and encourage better separation of concerns, but it also makes testing and reusing UI components across different modules a breeze. It’s much easier to isolate, maintain, and reason about UI logic when it’s decoupled from the rest of the app.

But there’s a small catch.

Since the Swift Package has no app target and doesn’t get compiled into an actual host runtime (no @main, no SceneDelegate, nothing), there’s no way to “run” views inside it unless they’re bridged into an executable target.

Now, our project has been increasingly adopting SwiftUI, and with that came a wonderful luxury: SwiftUI previews. Being able to instantly visualize and interact with UI in Xcode Previews has been a game-changer.

So I thought — why not do the same with UIKit?

Turns out, you can. Thanks to #Preview and a little bit of boilerplate, it’s entirely possible to make your UIView and UIViewController components previewable — and the development speed boost is very real.

Let’s start with a UIView

Let’s say you’ve got a custom button:

public final class MyFancyButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .systemBlue
        setTitle("Tap Me", for: .normal)
        layer.cornerRadius = 8
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

To preview this in SwiftUI:

#Preview {
    UIViewPreview {
        MyFancyButton()
    }
    .frame(width: 65, height: 34)
    .previewLayout(.sizeThatFits)
}

With the help of a tiny helper:

struct UIViewPreview<View: UIView>: UIViewRepresentable {
    private let viewBuilder: () -> View

    init(_ builder: @escaping () -> View) {
        self.viewBuilder = builder
    }

    func makeUIView(context: Context) -> View {
         viewBuilder()
    }

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

Boom — instant visual feedback inside Xcode. Screenshot

What about UIViewController?

No problem. Here’s a reusable wrapper:

struct ViewControllerPreview<ViewController: UIViewController>: UIViewControllerRepresentable {
    let builder: () -> ViewController

    init(_ builder: @escaping () -> ViewController) {
        self.builder = builder
    }

    func makeUIViewController(context: Context) -> ViewController {
        builder()
    }

    func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}

Note: we’re not using @autoclosure here — you’ll often want to pass actual closures that construct your controller with custom data.

And then:

#Preview {
    ViewControllerPreview {
        MyCustomViewController()
    }
}

This trick works beautifully for prototyping. You can inject mock data, test layout edge cases, preview different themes — all without having to run the full app.

Gotchas

  • Don’t expect Previews to support animations or real-time interactions — UIKit just sits there like a mannequin.
  • If your preview crashes — check if your view/controller requires data. Previews won’t magically inject dependencies.
  • Xcode previews sometimes hang for no reason. Close and reopen the canvas. Or restart Xcode. Or do a rain dance. No one knows why.

Final thoughts

Using #Preview for UIKit isn’t just a cute hack — it’s a very real productivity tool. Especially if you’re refactoring legacy code or slowly transitioning to SwiftUI. You don’t need to go full SwiftUI overnight to benefit from previews.

Stay tuned — or don’t. I won’t judge.