Say you have a modular iOS application which uses iOS frameworks for code separation. Say you also have a legacy UI made in Xib
files or Storyboards
, or views written in code. How to get all these types of UI working in SwiftUI previews in Xcode?
Let's make a sample application to experiment and to understand how Xib, Storyboards and views written in code can be used in SwiftUI previews.
The application consists with several frameworks:
- SharedLogic: Reusable non-UI code (DataSource, Enums, etc.)
- SharedUI: Reusable UI written from code (like reusable class
GradientView.swift
) - Settings: Application-specific component which represents dummy App settings.
- Dashboard: Application-specific component which represents Main screen of App.
- SwiftUI-Previews-in-Modular-app: Application itself (AppDelegate, Main window, etc.).
Here is a dependencies between components:
- SwiftUI-Previews-in-Modular-app depends on Dashboard, Settings, SharedUI and SharedLogic.
- Dashboard depends on Settings, SharedUI and SharedLogic.
- Settings depends on SharedUI and SharedLogic.
- SharedUI depends only on SharedLogic.
SharedUI
Lets start building preview for view GradientView.swift
made from code. Here is how GradientView
is looks like:
import Foundation import UIKit public class GradientView: UIView { private lazy var gradient = CAGradientLayer() public override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() } public override func layoutSubviews() { super.layoutSubviews() gradient.frame = bounds } } extension GradientView { private func setupUI() { let colors: [UIColor] = [.red, .orange, .yellow, .green, .cyan, .blue, .magenta] gradient.colors = colors.map { $0.cgColor } gradient.startPoint = CGPoint(x: 0, y: 0) gradient.endPoint = CGPoint(x: 1, y: 1) layer.insertSublayer(gradient, at: 0) } }
In order to preview this class in SwiftUI we need another type GradientView_UI
which conforms to UIViewRepresentable
.
import Foundation #if canImport(SwiftUI) && DEBUG import SwiftUI public struct GradientView_UI: UIViewRepresentable { let view = GradientView() public init() { } public func makeUIView(context: Context) -> GradientView { return view } public func updateUIView(_ uiView: GradientView, context: Context) { } } public struct GradientView_UI_Previews: PreviewProvider { static var views: [GradientView_UI] { let views = [ GradientView_UI(), GradientView_UI() ] return views } public static var previews: some View { ZStack { Color.gray VStack { ForEach(views.indices) { views[$0].frame(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 200) } } } .previewDevice("iPhone SE") } } #endif
Attempt to preview GradientView
fails due missed SharedLogic framework.
But SharedLogic framework was built and it exists in DerivedData
directory under /Build/Intermediates.noindex/Previews/SharedUI/Products/Debug-iphonesimulator/SharedLogic.framework
Build log also shows that both frameworks SharedUI and SharedLogic were built successfully.
The Library not loaded error seems because of not satisfiable, default, setting for run-time search path – @rpath.
To fix it we have to update Xcode build setting LD_RUNPATH_SEARCH_PATHS
by adding additional search path @loader_path/../
. With this we instructing runtime to search frameworks one level up relative to current framework.
// Before LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks @loader_path/Frameworks // After LD_RUNPATH_SEARCH_PATHS = @loader_path/../ @executable_path/Frameworks @loader_path/Frameworks
After applying changes to @rpath
, the SwiftUI preview for GradientView
start working.
Settings
In Settings framework we have SettingsCell
made as a Xib
-file and SettingsViewController
which is a subclass of UITableViewController
made from code. SettingsViewController
uses SettingsCell
. SettingsCell
uses GradientView
from SharedUI framework.
To preview cell and controller we creating two types SettingsCell_UI
and SettingsViewController_UI
. These types conforms to UIViewRepresentable
and UIViewControllerRepresentable
protocols respectively.
import SharedUI import Foundation #if canImport(SwiftUI) && DEBUG import SwiftUI struct SettingsCell_UI: UIViewRepresentable { let nib = UINib(nibName: "SettingsCell", bundle: Bundle(for: SettingsCell.self)) func makeUIView(context: Context) -> SettingsCell { let cell = nib.instantiate(withOwner: nil, options: [:]).compactMap { $0 as? SettingsCell }.first cell?.setting = "Lorem ipsum" cell?.gradientAlpha = 0.5 return cell! } func updateUIView(_ uiView: SettingsCell, context: Context) { } } public struct SettingsCell_UI_Previews: PreviewProvider { static var views: [SettingsCell_UI] { let views = [ SettingsCell_UI(), SettingsCell_UI() ] return views } public static var previews: some View { ZStack { Color.gray VStack { ForEach(views.indices) { views[$0].frame(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 240) } } } .previewDevice("iPhone SE") } } #endif
import Foundation #if canImport(SwiftUI) && DEBUG import SwiftUI public struct SettingsViewController_UI: UIViewControllerRepresentable { let vc = SettingsViewController() let nc = UINavigationController() public init() {} public func makeUIViewController(context: Context) -> UINavigationController { nc.setViewControllers([vc], animated: false) return nc } public func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { } } public struct SettingsViewController_UI_Previews: PreviewProvider { public static var previews: some View { SettingsViewController_UI().previewDevice("iPhone SE") } } #endif
Dashboard
Dashboard framework contains two view controllers made as a Storyboard
-file. One of controllers uses a UITableViewCell
also configured in same Storyboard
-file.
As before for previewing view controllers we creating additional types which conformed to UIViewControllerRepresentable
protocol.
Type DashboardViewController_UI
:
import Foundation #if canImport(SwiftUI) && DEBUG import SwiftUI public struct DashboardViewController_UI: UIViewControllerRepresentable { let bundle = Bundle(for: DashboardViewController.self) public init() {} public func makeUIViewController(context: Context) -> DashboardViewController { let vc = UIStoryboard(name: "Dashboard", bundle: bundle).instantiateInitialViewController() as! DashboardViewController return vc } public func updateUIViewController(_ uiViewController: DashboardViewController, context: Context) { } } public struct DashboardViewController_UI_Previews: PreviewProvider { public static var previews: some View { DashboardViewController_UI().previewDevice("iPhone SE") } } #endif
Type AphorismsViewController_UI
:
import Foundation #if canImport(SwiftUI) && DEBUG import SwiftUI struct AphorismsViewController_UI: UIViewControllerRepresentable { let bundle = Bundle(for: AphorismsViewController.self) let nc = UINavigationController() func makeUIViewController(context: Context) -> UINavigationController { let vc = UIStoryboard(name: "Dashboard", bundle: bundle).instantiateViewController(identifier: "Aphorisms") as! AphorismsViewController nc.setViewControllers([vc], animated: false) return nc } func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { } } public struct AphorismsViewController_UI_Previews: PreviewProvider { public static var previews: some View { AphorismsViewController_UI().previewDevice("iPhone SE") } } #endif
We can also switch to SwiftUI-Previews-in-Modular-app scheme and preview several controllers or views, used in App, at once.
import Foundation import SharedUI import Dashboard import Settings #if canImport(SwiftUI) && DEBUG import SwiftUI struct Application_UI_Previews: PreviewProvider { static var previews: some View { Group { GradientView_UI().previewDevice("iPhone SE"); DashboardViewController_UI().previewDevice("iPhone SE"); SettingsViewController_UI().previewDevice("iPhone SE") } } } #endif
Live Preview and Debug Preview also works!
The modular iOS application, which uses several frameworks, works with SwiftUI previews. Only small fix addressed run-time search path, @rpath
, was needed.
Happy modular coding!
Top comments (1)
Big thanks to the author! Was struggling with that @rpath issue. Almost got to the point of moving all UI modules into the main app...