Well, dear readers, it appears that my grand plans to pen an article a week have gone the way of my New Year’s resolutions. It’s been nearly five months since my last post, but who’s counting? Juggling full-time work, a life outside programming, the school run, a newborn, and childcare has turned out to be a bit more challenging than anticipated. But here we are, finally, back in action with tales from the trenches of iOS development. Buckle up for a deep dive into converting an app to SwiftUI. It’s a journey!
Over the last seven months, I’ve been collaborating with a team whose iOS product is predominantly built using UIKit. From the onset, I’ve been championing the significant benefits of SwiftUI (including its declarative syntax, real-time previews, and seamless integration with Swift) to my team. During this period, I undertook the task of converting a section of our app to SwiftUI using a `UIHostingController`. This process not only highlighted the challenges posed by our existing architecture but also offered insights into planning a seamless migration from UIKit to SwiftUI.
The Initial Hurdle: Target Platform Compatibility
The first major challenge I encountered was the target platform. Underestimating the additional work required for SwiftUI on iOS 13 was a misstep. The lack of full SwiftUI support in iOS 13 meant that numerous workarounds were necessary to achieve the intended view behaviors, resulting in excess boilerplate code.
**Step One: Updating the Target Platform**
Given that this isn’t my personal app, any changes needed to align with business objectives. Leveraging data from StatCounter, I compiled statistics showing that a negligible percentage of devices in our target market use iOS 13 or 14. Practically none, to be precise; possibly just our big boss. With this data in hand, and a bit of humor (adding a code marker `// @available(*, deprecated, message: “Nobody has iOS 13 installed anymore”)`), I successfully advocated for updating our target platform. Within two weeks, we were ready to dive into SwiftUI with minimal legacy constraints.
Navigating the Conversion Path: Strategic Considerations
Converting an app to SwiftUI isn’t as straightforward as merely rewriting views. The underlying architecture and navigation patterns need thoughtful consideration. This raises the question: From which angle should we approach the conversion?
Two Conversion Strategies
1. **View-by-View Conversion:** This method allows manageable, incremental updates by converting one view at a time. Once all views are converted, the underlying navigation and architecture can be transitioned to SwiftUI. This approach minimizes disruptions but requires interim patches and wrappers to integrate SwiftUI views with the existing UIKit codebase.
2. **Architecture-First Conversion:** This strategy involves restructuring the underlying architecture and navigation patterns to SwiftUI upfront, using `UIViewControllerRepresentable` to wrap existing UIKit views. This approach avoids the need for numerous interim patches but demands a more significant initial effort.
**Case in Point: Presenting SwiftUI Popups in a UIKit Environment**
In our scenario, presenting a SwiftUI view within a `UIHostingController` posed challenges, especially with our floating TabBar. The SwiftUI view often ended up behind the TabBar. The solution required either a `UIViewControllerRepresentable` to present the view within UIKit or a coordinator hook to manage UIKit view interactions. Both solutions were less than ideal, emphasizing the need for a balanced approach.
Toward a Seamless Integration
In our upcoming architecture meeting, I’m optimistic about adopting a hybrid approach: transitioning the app scene and navigation to SwiftUI while gradually converting critical views. This approach ensures our core classes, such as data subscriptions, are effectively managed by our evolving dependency container. Eventually, our app will be fully SwiftUI-based, devoid of `UIViewControllerRepresentable` or `UIHostingController` workarounds.
Practical Implementation: Code Wrappers
To facilitate a smooth transition from UIKit to SwiftUI and vice versa, here are some essential code wrappers:
Preview Handler for SwiftUI
One of the advantages of SwiftUI is the ability to quickly preview your views across different devices, appearances, and locales. The following extension makes it simple to configure these previews:
extension View {
func applyPreviewConfiguration(_ config: PreviewConfiguration = PreviewConfiguration()) -> some View {
self
.previewDevice(config.device.map(PreviewDevice.init(rawValue:)))
.previewLayout(config.layout)
.preferredColorScheme(config.colorScheme)
.environment(\.locale, config.locale ?? Locale.current)
.environment(\.sizeCategory, config.contentSizeCategory ?? .medium)
.previewDisplayName(config.displayName ?? “Default Preview”)
}
}
struct PreviewConfiguration {
var layout: PreviewLayout = .device
var device: String?
var locale: Locale?
var colorScheme: ColorScheme? = .dark
var displayName: String?
var contentSizeCategory: ContentSizeCategory?
}
This extension allows you to easily set up a default preview configuration, making it effortless to see how your views will look under different conditions. Whether you’re testing for different devices, color schemes, or locales, this configuration ensures consistent and comprehensive previews.
Generic Wrapper for UIKit Views in SwiftUI
Integrating UIKit views within a SwiftUI interface can be streamlined with a `UIViewControllerRepresentable`. This wrapper helps embed existing UIKit view controllers into SwiftUI:
struct GenericViewControllerRepresentable<ViewController: UIViewController>: UIViewControllerRepresentable {
let makeViewController: () -> ViewController
init(_ makeViewController: @escaping () -> ViewController) {
self.makeViewController = makeViewController
}
func makeUIViewController(context: Context) -> ViewController {
makeViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
// Add specific code for particular viewControllers
}
}
struct ViewControllerPreview<ViewController: UIViewController>: PreviewProvider {
static var previews: some View {
GenericViewControllerRepresentable {
MyUIKitViewController()
}
.applyPreviewConfiguration()
}
}
This code block provides a flexible way to incorporate UIKit view controllers into your SwiftUI app. By using this generic representable, you can maintain the functionality of existing view controllers while gradually shifting your app’s architecture to SwiftUI.
Generic Wrapper for UIKit Views
Sometimes, you only need to integrate specific UIKit views rather than entire view controllers. The following `UIViewRepresentable` makes this process straightforward:
struct GenericUIViewRepresentable<View: UIView>: UIViewRepresentable {
let makeView: () -> View
var updateView: ((View) -> Void)?
init(_ makeView: @escaping () -> View, updater updateView: ((View) -> Void)? = nil) {
self.makeView = makeView
self.updateView = updateView
}
func makeUIView(context: Context) -> View {
return makeView()
}
func updateUIView(_ uiView: View, context: Context) {
updateView?(uiView)
(uiView as? mySpecificView)?.doSomething() // If View is mySpecificView then run .doSomething
}
}
This wrapper is perfect for adding individual UIKit views into your SwiftUI layout. Whether it’s a custom UIView or a standard component, this generic representable allows for seamless integration and updating of views.
Hosting Controller for SwiftUI in UIKit
For scenarios where you need to integrate SwiftUI views within an existing UIKit framework, a custom `UIHostingController` can be highly effective:
class GenericSwiftUIHostingController<Content>: UIHostingController<Content> where Content: View {
var title: UILabel? // Access this from UIKit to change the UINavigation Title
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let title = title {
let titleHeight: CGFloat = 44 // Or as appropriate
title.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: titleHeight) // Ensure full width
}
}
}
This custom `UIHostingController` enables the presentation of SwiftUI views within a UIKit-based navigation structure. It provides a bridge between the two frameworks, allowing for a gradual and controlled transition from UIKit to SwiftUI.
Final Thoughts
Transitioning an app from UIKit to SwiftUI is a complex but rewarding endeavour. By carefully planning the migration strategy and utilizing these practical code wrappers, you can ensure a smooth and efficient transition, ultimately creating a modern, future-proof application.
Till the Next Byte, British Dev’s Diary