Published on

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 UIHostingControllerto 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 present in this view-model 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 a parameter 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 are able to simply pass data via the struct's parameters while creating a new view. 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 it 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 UIViews 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 the indexPath.
  • Data can be passed through using a typical collectionView.reloadData() or by leveraging the new DiffableDataSource 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 UITableViewwill surely make it popular with iOS developers across the spectrum. Thank you for reading. See you in the next one!