Previewing UIViews and UIViewControllers With SwiftUI Preview
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
.