Previewing UIViews and UIViewControllers With SwiftUI Preview

·

3 min read

Steps on how to do it

1. Create a new Cocoa Touch Class File template as a subclass of UIViewController using Swift language. Name that file as BaseUIViewController

2. Replace the boilerplate code with this example code snippet

import UIKit
import SwiftUI

class BaseUIViewController: UIViewController {

    lazy var button: UIButton = {
        let button = UIButton(type: .system)
        button.frame = CGRect(x: 50, y: 50, width: 200, height: 50)
        button.setTitle("Tap me!", for: .normal)
        button.addTarget(self, action: #selector(tappedMe), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setupView()
        self.setupNavigationBar()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // Reset Navigation Bar Appearance
        navigationController?.navigationBar.isHidden = false
    }

    func setupNavigationBar() {
        // Set the background color of the navigation bar
        self.navigationController?.navigationBar.barTintColor = .yellow
        self.navigationController?.navigationBar.isTranslucent = false // to reveal the navigationBar.barTintColor
        self.navigationItem.hidesBackButton = true

        // Create left and right bar button items
        let leftBarButton = UIBarButtonItem(
            title: "About Us",
            style: .plain,
            target: self,
            action: #selector(leftBarButtonAction)
        )

        let rightBarButton = UIBarButtonItem(
            title: "Call Us",
            style: .plain,
            target: self,
            action: #selector(rightBarButtonAction)
        )

        // Set the left and right bar button items
        self.navigationItem.leftBarButtonItem = leftBarButton
        self.navigationItem.rightBarButtonItem = rightBarButton

    }

    func setupView() {
        view.backgroundColor = .systemBackground
        view.addSubview(button)

        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    // Using customed UIHosingController
    @objc func showAllyCallUsView() {
        let viewModel = CallUsView.ViewModel()
        let view = CallUsViewWithNavigation(viewModel: viewModel)
        let controller = ThemedHostingController(rootView: view, statusBarStyle: .lightContent, isHideNavBar: true)
        controller.modalPresentationStyle = .formSheet

        viewModel.dismissAction = { [weak self] in
            self?.dismiss(animated: true)
        }

        self.present(controller, animated: true, completion: nil)
    }

    @objc func tappedMe() {
        print("Add logic for Tap Me Button here")
    }

    // Using normal UIHostingController
    @objc func leftBarButtonAction() {
        let swiftUIView = AboutUsScreen()
        let viewController = UIHostingController(rootView: swiftUIView)
        self.present(viewController, animated: true, completion: nil)
    }

    @objc func rightBarButtonAction() {
        self.showAllyCallUsView()
    }
}

3. Create another new file of the tyle Swift File template and call it PreviewView.

4. Replace the boilerplate code in the PreviewView file with this code snippet

import SwiftUI

@available(iOS 13, *)
struct PreviewView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> BaseUIViewController {
        BaseUIViewController()
    }

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

@available(iOS 13, *)
struct PreviewViewPreview: PreviewProvider {
    static var previews: some View {
        PreviewView()
    }
}

5. Now, we can see the content of BaseUIViewController on the XCode Canvas.

Limitation

In SwiftUI, when we use UIViewControllerRepresentable to wrap a UIKit view, it is treated as an independent entity and doesn't inherit a UINavigationController from the SwiftUI NavigationView. This is why you won't see the navigation bar items from your BaseUIViewController when you wrap it in a SwiftUI navigation flow.

To include the navigation bar from BaseUIViewController, you should create an instance of a UINavigationController inside of UIViewControllerRepresentable, set BaseUIViewController as the root view controller for the navigation controller, and return the navigation controller to the makeUIViewController method.

Make sure you specify the type of UIViewControllerRepresentable as UINavigationController.

Here is an example of how you can accomplish:

@available(iOS 13, *)
struct PreviewView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UINavigationController {

        typealias UIViewControllerType = UINavigationController

        let baseViewController = BaseUIViewController()
        let navigationController = UINavigationController(rootViewController: baseViewController)
        return navigationController
    }

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

This will provide your BaseUIViewController with a UINavigationController and should correctly show your navigation bar items. However, navigation behavior (like push and pop of multiple UIKit ViewControllers) within your BaseUIViewController will still be isolated from the SwiftUI navigation stack.

Regarding the issue of not seeing the navigation bar items inside BaseUIViewController, please make sure your BaseUIViewController is embedded inside a UINavigationController. This is required because UINavigationItem properties like leftBarButtonItem and rightBarButtonItem are actually properties of the UINavigationController, not the UIViewController.