Using SwiftUI with UIKit in 2022
At WWDC 22, Apple introduced many new ways to integrate SwiftUI views within an existing UIKit app. Now, SwiftUI integrates seamlessly into existing UIKit apps. In this article, we will go through some existing approaches that were available and some new ways to acheive this interoperability in iOS 16.
UIHostingController
UIHostingController is a subclass of UIViewController that contains a SwiftUI view in its view hierarchy. It is useful when you want to either add a SwiftUI view to a ViewController’s view’s hierarchy, or if you’d like to add the hosting controller as a child ViewController to the parent ViewController.
To create a UIHostingController, simply initialise it with the SwiftUI View.
let swiftUIView = ContentView()
let hostingController = UIHostingController(rootView: swiftUIView)
To add it as a child ViewController:
self.addChild(hostingController)
Then, add the hostingController
’s view to the parent’s view hierarchy.
self.view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
You can now set the frame of the hosting controller. You can even use constraints to pin the view to the edges of the screen.
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailing.constraint(equalTo: view.trailing),
hostingController.view.top.constraint(equalTo: view.top),
hostingController.view.bottom.constraint(equalTo: view.bottom),
])
In iOS 16, you can use the .preferredContentSize
and .intrinsicContentSize
properties on UIHostingController
to automatically update the ViewController
’s preferred content size and the view’s intrinsic content size. This can be enabled using the sizing options property on the UIHostingController
.
hostingController.sizingOptions = .preferredContentSize
Passing Data into a SwiftUI View
In MVVM, which stands for Model View View-Model architecture, the ViewModel
is the principle source of truth and supplies data to the ViewControllers. We can pass the data in this ViewModel to our SwiftUI views in two ways
Initialisation properties
A SwiftUI View is a light-weight struct
. This means you can simply pass values during the initialisation of the view struct. A view with some properties might look like this
struct TodayView: View {
let name: String
var body: some View {
Text("Hello, \(name)")
}
}
To initialise this view, you could either create a member wise intialiser and provide a default value for the property name
, or you could pass this while creating a new object of this struct
.
let todayView = TodayView(name: "Swapnanil")
Using this example, we can see that we can simply pass data while creating a new view using the struct
’s initialiser. This can be integrated into a UIKit ViewController
in this way:
let hostingController: UIHostingController<TodayView>
private func setupHostingController() {
let userName = viewModel.userName
hostingController = UIHostingController(TodayView(name: userName))
// Add code for adding this view to the ViewController's hierarchy
}
Passing data this way makes it your responsibility to update the hosting controller when the data in the view model changes. Since the instance of the view is only created once, SwiftUI cannot and will not update the view’s contents even when the view model’s data changes. Please note that SwiftUI’s views are struct
which are value types. This means that if you were to store by itself while creating the hostingController
constant, it would create a separate copy and hence we wouldn’t be able to update the view. Hence, we pass a generic property to the UIHostingController that is of type TodayView
so that we can create and set a new value of this TodayView
later inside of our UIViewController
.
If this is not the behaviour you’d like, you can choose this next approach.
ObservableObject
The data that is owned by SwiftUI is marked with the property wrappers @State
and @StateObject
. These wrappers ensure that anytime the data managed by them changes, the view updates automatically to reflect the new changes. However, the data we want to pass is not generated or owned by our SwiftUI view. It comes from an external source, which is our view model. SwiftUI has always had property wrappers to manage data coming from external data models. These wrappers are @ObservedObject
and @EnvironmentObject
. They let us pass an external reference to a view model that conforms to the ObservableObject
protocol. Inside these classes, we can have properties marked with the @Published
property wrapper, that when their data changes, updates the observing SwiftUI view.
class ViewModel: ObservableObject {
@Published var name = ""
}
Now, in your SwiftUI view:
struct TodayView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
Text("Hello, \(viewModel.name)")
}
}
While initialising a new UIHostingController
, simply pass the view-model hosted by the parent ViewController to the SwiftUI view.
hostingController = UIHostingController(TodayView(viewModel: viewModel))
This method ensures that your SwiftUI view inside of your hosting controller will always remain in sync with the data in your view-model without you having to manually refresh or create and set a new instance of a SwiftUI view to your hosting controller every time the data marked with the @Published
property wrapper changes.
SwiftUI with UICollectionView and UITableView
Previously, you could use a UIHostingController’s view to be a cell’s view inside of a collectionView
or a tableView
. This meant you’d have to use extra UIViewControllers
or UIView
s in your cell’s contentView
. New in iOS 16 is UIHostingConfiguration
. UICollectionViewCells
and UITableViewCells
have a new property called contentConfiguration
. A UIHostingConfiguration
can be set with a SwiftUI view directly and set to the cell’s contentConfiguration
property.
cell.contentConfiguration = UIHostingConfiguration {
Text("Hello, world!")
}
That’s it! It is now this easy to use a SwiftUI view inside of your UIKit lists.
Nice to know
List separators are now auto aligned to the text by default incase you are using something like a Label
view. This can be customised using the .alignmentGuide
modifier.
Additional
- SwiftUI views inside of UIKit’s lists allow for actions such as list swipe gestures, which are handled within the view created for the cell’s
contentConfiguration
. Apple recommends using a stable identifier as it might happen that during cell reuse, the identifiers get messed up, causing a swipe action to appear or perform for an incorrect cell. This should be something unique and stable and not theindexPath
. - Data can be passed through using a typical
collectionView.reloadData()
or by leveraging the newDiffableDataSource
introduced in iOS 14. SwiftUI cells inside of list cells, that are tied to observable properties inside of the parent’s view-model can perform auto-resizing and update its data without having to perform a batch update or a reload of the whole list. This means you can have a one-way or a two-way binding wherein the cells can write back to the properties on your view-model and also display the data in the current state.
Additional resources
The session Use SwiftUI with UIKit from WWDC 2022 is a great resource for learning the new enhacements available in SwiftUI and UIKit interoperability available in iOS 16.
Conclusion
There has never been a better time to integrate SwiftUI with your existing UIKit projects. With the launch of iOS 16, apps will be able to drop support for iOS 12 and iOS 13 and start integrating SwiftUI into their existing code base. Apple is steadfast in its commitment to developing SwiftUI, and the progress this framework makes each year demonstrates that Apple believes SwiftUI will be the future of developing for the Apple platform. Now is a good time to prepare for the future. WWDC ’22 introduces the most mature version of SwiftUI, and its new APIs to integrate with popular UIKit APIs such as UICollectionView
and UITableView
will surely make it popular with iOS developers across the spectrum. Thank you for reading. See you in the next one!