Learn how to take advantage of the power of SwiftUI in your UIKit app. Build custom UICollectionView and UITableView cells seamlessly with SwiftUI using UIHostingConfiguration. We'll also show you how to manage data flow between UIKit and SwiftUI components within your app.
To get the most out of this session, we encourage basic familiarity with SwiftUI.
♪ Mellow instrumental hip-hop music ♪ ♪ Hello, I'm Sara Frederixon, an engineer on the Health app, and I'm here to talk to you about using SwiftUI with UIKit.
Like many of you, I work on an existing UIKit app.
For me, this is the Health app.
The Health app has many visualizations to help people understand their health data, but building these views can be quite complex.
I've been interested in taking advantage of SwiftUI, so I worked with the UIKit and SwiftUI teams to learn how to integrate both into the same app.
In this video, I will teach you how easy it is to start using SwiftUI in your own UIKit apps.
First I'll cover the existing UIHostingController, which has some new updates that add even more flexibility.
Next I'll dive into populating SwiftUI views with data that already exist in your app, and how to ensure the SwiftUI views update when that data changes.
Then, I'll talk about some exciting new functionality that lets you build UICollectionView and UITableView cells using SwiftUI.
Finally, I'll walk through the unique aspects of data flow with collection and table views when you're using SwiftUI inside cells.
Let's get started by talking about UIHostingController.
UIHostingController is a UIViewController that contains a SwiftUI view hierarchy.
You can use a hosting controller anywhere you can use a view controller in UIKit.
This makes UIHostingController an easy way to start using SwiftUI.
Let's examine how a hosting controller works.
A hosting controller is a view controller, which means it has a UIView stored in its view property.
And inside that view is where your SwiftUI content is rendered.
Let's go through an example of how to use a hosting controller.
Here, we create a HeartRateView, a SwiftUI view.
We then create a hosting controller with HeartRateView as its root view, and present it.
UIHostingController works with all of the UIKit view controller APIs.
Let's go through another example.
We have the same HeartRateView and hosting controller as before.
Here we add the hosting controller as a child view controller.
Then we can position and size the hosting controller's view.
When your SwiftUI content inside UIHostingController changes, it may cause the view to need to be resized.
New in iOS 16, UIHostingController allows you to enable automatic updates of the view controller's preferred content size and the view's intrinsic content size.
You can enable this using the new sizingOptions property on UIHostingController.
Let's go through an example.
To start, we make our HeartRateView, and create the hostingController.
We use the new sizingOptions API to tell the hostingController to automatically update its preferredContentSize.
Then, we set the modalPresentationStyle to popover.
Using the new sizingOptions API ensures that the popover is always sized appropriately to fit the SwiftUI content.
Now that you're familiar with UIHostingController, let's talk about how to get data into SwiftUI from other parts of your UIKit app, and ensure that your SwiftUI views update when that data changes.
Here's a diagram of your UIKit app, It contains an existing model layer that owns and manages your app's data model objects.
Your app also contains a number of view controllers.
If you want start using SwiftUI, you'll need a hosting controller with a SwiftUI view inside one of the view controllers.
You will want to populate this SwiftUI view with data that is still owned by your existing model layer.
In this section, we're going to focus on how to bridge data across the boundary between UIKit and SwiftUI.
SwiftUI offers a variety of data flow primitives to help you manage the data in your app.
Let's go through the different options.
To store data that is created and owned by a SwiftUI view, SwiftUI provides the @State and @StateObject property wrappers.
Since we're focused on data owned outside of SwiftUI, these property wrappers aren't the right choice.
So, I'm not going to cover these in this video.
Watch "Data Essentials in SwiftUI" to learn more about data owned by a SwiftUI view.
One way to handle data external to SwiftUI is to pass values directly when you initialize your views.
Because you are just passing raw data that is not owned or managed by SwiftUI, you are responsible for manually updating the UIHostingController when the data changes.
Let's go through an example.
Here is a SwiftUI view named HeartRateView.
This view has a single property -- the heart rate beatsPerMinute stored as an integer -- and it displays the value as text.
We're displaying this HeartRateView by embedding a UIHostingController inside an existing view controller named HeartRateViewController.
We save a reference to the hosting controller so we can update its root view later.
Remember, the SwiftUI HeartRateView is a value type, so storing it by itself would create a separate copy, and wouldn't allow us to update the UI.
The HeartRateViewController owns the data used to populate the HeartRateView.
This data is stored in the beatsPerMinute property, and when the beatsPerMinute value changes, we call a method to update the view.
Inside of the update method, we create a new HeartRateView using the latest beatsPerMinute value, and then assign that view as the rootView of our hosting controller.
This is a simple way to get data from UIKit into SwiftUI, but it requires manually updating the rootView of the hosting controller any time the data changes.
Let's go through some other SwiftUI data primitives to make these updates happen automatically.
The @ObservedObject and @EnvironmentObject property wrappers allow you to reference an external model object that conforms to the ObservableObject protocol.
When you use these property wrappers, SwiftUI automatically updates your view when the data changes.
We're going to focus on the @ObservedObject property wrapper in this video, but you can learn more about EnvironmentObject in the "Data Essentials in SwiftUI" video mentioned earlier.
Let's go through how to create an @ObservedObject.
The first step is to take a model object that is owned by an existing part of your app and make it conform to the ObservableObject protocol.
Next, we store the model as an @ObservedObject property in our SwiftUI view.
Connecting the ObservableObject to SwiftUI allows it to update the view when one of its properties change.
Let's go back to our HeartRateView example and wire this up.
Our app has a class named HeartData that contains the property beatsPerMinute.
We make it an ObservableObject by conforming to the protocol.
Then we add the @Published property wrapper to the beatsPerMinute property.
This property wrapper is what triggers SwiftUI to update our views on changes.
In the HeartRateView, we store the HeartData in a property annotated with the @ObservedObject property wrapper.
In the body of the view, we display the beatsPerMinute directly from the HeartData.
Now, let's use these together in our view controller.
Here is our HeartRateViewController.
It stores the HeartData ObservableObject in a property.
Because this property is not inside a SwiftUI view, we don't need to use a property wrapper here.
The HeartRateViewController is initialized with a HeartData instance, which is used to create a HeartRateView that becomes the rootView of the hosting controller.
The diagram illustrates how this comes together.
We fetch the current HeartData instance, which contains a heart rate of 78 beat per minute.
Then we create a new HeartRateViewController with that HeartData instance, which wires it up to the SwiftUI HeartRateView.
After a few seconds, when the next heart rate data sample arrives, the heart data's beatsPerMinute property is updated to 94.
Because this changes a published property on an ObservableObject, the HeartRateView is automatically updated to display the new value.
We no longer need to manually update the hosting controller when the data changes.
This is why ObservableObject is a great way to bridge data from UIKit to SwiftUI.
Next, let's talk about using SwiftUI in collection view and table view cells.
New in iOS 16 is UIHostingConfiguration, which lets you harness the power of SwiftUI inside your existing UIKit, collection, and table views.
UIHostingConfiguration makes it easy to implement custom cells using SwiftUI, without needing to worry about embedding an extra view or view controller.
Before we dive deeper on UIHostingConfiguration, let's review cell configurations in UIKit.
Cell configurations are a modern way to define the content, styling, and behavior of cells in UIKit.
Unlike a UIView or UIViewController, a configuration is just a lightweight struct.
It's inexpensive to create.
A configuration is only a description of a cell's appearance, so it needs to be applied to a cell to have an effect.
Configurations are composable, and work with both UICollectionView and UITableView cells.
For more details, you can watch "Modern cell configuration." With that in mind, let's dive in and start using UIHostingConfiguration! UIHostingConfiguration is a content configuration that is initialized with a SwiftUI ViewBuilder.
That means we can start writing SwiftUI code to create views directly inside it.
In order to render the hosting configuration, we set it to the contentConfiguration property of the UICollectionView or UITableView cell.
Let's start writing some SwiftUI code in this hosting configuration to build a custom heart rate cell.
We'll start by creating a label with the text "Heart Rate" and the image of a heart.
SwiftUI views receive default styling based on the context they are used in.
But we can start customizing the styling using standard SwiftUI view modifiers.
Let's make the image and text pink with a bold font, by adding the foregroundStyle and font modifiers to our label.
Since we're just writing regular SwiftUI code, we can factor out our code into a standalone view anytime we want.
Here, we create a new SwiftUI view named HeartRateTitleView, moved the code we had into its body, and then used that HeartRateTitleView in the hosting configuration.
As shown in the cell, it produces the exact same result.
Now we can start adding more views inside the HeartRateTitleView.
I've put the label inside of an HStack with a spacer, then added the current time in a Text view next to it.
That's looking pretty good so far.
Let's add some more content to this custom cell below the HeartRateTitleView.
To do this, we'll insert a VStack inside the hosting configuration so we can add more content below the HeartRateTitleView.
Then we'll put two Text views together in a HStack to display 90 BPM and then apply a few modifiers to style them the way we want.
Just like we did before with the HeartRateTitleView, we can move this new code into its own SwiftUI view.
Now the same code is extracted into the body of the HeartRateBPMView.
Our cell is looking great, but I have an idea for another thing we could add.
New in iOS 16 is the Swift Charts framework, which lets you visualize data with beautiful graphs in only a few lines of code.
Let's try using it to display a small line chart right inside the cell.
Using the new Chart view, we create a small line chart that visualizes recent heart rate samples and display that next to the BPM view in the cell.
To generate the chart, we pass in a collection of heart rate samples, and draw a LineMark that connects all of the samples.
We can add a circle symbol to indicate each sample on the line and apply a pink foreground style, to tint the chart to match the HeartRateTitleView.
We're only scratching the surface of what you can do with the new Swift Charts framework, so be sure to check out the video "Hello Swift Charts" to learn more about it.
Not only does our finished heart rate cell look awesome, but it was easy to make in just a couple of minutes.
That's how easy it is to start building custom cells with UIHostingConfiguration and SwiftUI.
Let's talk about four special features that UIHostingConfiguration supports.
By default, the root-level SwiftUI content is inset from the edges of the cell, based on the cell's layout margins in UIKit.
This ensures that the cell content is properly aligned with the content of adjacent cells and other UI elements, such as the navigation bar.
Sometimes, you may want to use different margins, or have the content extend to the edges of the cell.
For these cases, you can change the default margins by using the margins modifier on UIHostingConfiguration.
If you want to customize a cell's background appearance using SwiftUI, you can use the background modifier on the UIHostingConfiguration.
There are a few key differences between a UIHostingConfiguration's background and its content.
The background is hosted at the the back of the cell, underneath your SwiftUI content in the cell's content view.
Additionally, while content is typically inset from the cell's edges, backgrounds extend edge to edge in the cell.
Finally, when using self-sizing cells, only the cell content influences the size of the cell.
Next, let's examine two more special features of UIHostingConfiguration that you can use when you have a cell inside a collection view list or table view.
In a list, the separator below the cell is automatically aligned to the SwiftUI text in your hosting configuration by default.
In this example, notice how the leading edge of the separator is inset past the image so that it aligns with the text in the cell.
If you need to align the separator to a different SwiftUI view in your hosting configuration, use the alignmentGuide modifier.
When inside a collection view list or table view, you can configure swipe actions for a row directly with SwiftUI.
By creating your buttons inside the swipeActions modifier, you'll be able to swipe the cell to reveal and perform your custom actions.
Download the sample code for this video to find a complete example.
When defining swipe actions, make sure your buttons perform their actions using a stable identifier for the item represented.
Do not use the index path, as it may change while the cell is visible, causing the swipe actions to act on the wrong item.
When you're using UIHostingConfiguration in a cell, keep in mind that the cell interactions such as tap handling, highlighting, and selection will still be handled by the collection view or table view.
If you need to customize your SwiftUI views for any of these UIKit cell states, you can create your hosting configuration inside of the cell's configurationUpdateHandler, and use the state provided in your SwiftUI code.
The configurationUpdateHandler runs again any time the cell's state changes, creating a new UIHostingConfiguration for the new state and applying it to the cell.
In this example, we use the state to add a checkmark image when the cell is selected.
Now that you're familiar with UIHostingConfiguration, let's discuss how to manage data flow from your model layer to a UICollectionView or UITableView filled with cells using SwiftUI.
Our goal is to build this list of medical conditions.
In this example, we're using a UICollectionView, but everything we discuss applies equally to UITableView.
Let's go through the components involved.
Our app has a collection of MedicalCondition model objects, which we are going to display in the collection view.
For each item in this collection, we want to create a cell in the collection view to display that medical condition.
In order to do this, we'll create a diffable data source connected to the collection view.
Then, we need to populate a diffable data source snapshot with the identifiers of the MedicalCondition model objects in the data collection.
It's important that the diffable data source snapshot contains the unique identifier of each MedicalCondition, and not the MedicalCondition objects themselves.
This ensures that the diffable data source can accurately track the identity of each item, and compute the correct changes when new snapshots are applied later.
By applying a snapshot with these item identifiers to the diffable data source, it will automatically update the collection view, which will create a new cell for each item.
Each cell is configured to display one MedicalCondition, using a SwiftUI view in a UIHostingConfiguration.
Now that we're displaying cells built with SwiftUI, we need to handle updating the UI when the data changes.
There are two different types of changes that we need to handle separately.
The first type is when the data collection itself changes.
For example, when items are inserted, reordered, or deleted.
These changes are handled by applying a new snapshot to the diffable data source.
Diffable data source will diff the old and new snapshots, and perform the necessary updates to the collection view, causing cells to be inserted, moved, or deleted.
Because changes to the collection of data itself don't affect anything inside of cells, you handle these types of changes the same, whether you build your cells using UIKit or SwiftUI.
The second type of change we need to handle are when the properties of individual model objects change.
These changes often require updating the views in existing cells.
Because the diffable data source only contains item identifiers in its snapshot, it doesn't know when the properties of existing items change.
Traditionally, when using UIKit, you would need to manually tell the diffable data source about these changes by reconfiguring or reloading items in the snapshot.
But when using SwiftUI in cells, this isn't necessary anymore.
By storing the ObservableObject model in an ObservedObject property in our SwiftUI view, changes to published properties of the model will automatically trigger SwiftUI to refresh the view.
This establishes a direct connection between the model and the SwiftUI view inside of the cell.
When a change is made, the SwiftUI views in the cell are updated directly, without going through the diffable data source or the UICollectionView.
When a cell's data changes, it may cause the cell to need to grow or shrink to fit the new content.
But if the SwiftUI cell content is being updated directly without going through UIKit, how does the collection view know to resize the cell? UIHostingConfiguration takes advantage of a brand-new feature in UIKit to make this work.
In iOS 16, self-sizing cells in UICollectionView and UITableView are now also self-resizing! This is enabled by default, so that when you're using UIHostingConfiguration and the SwiftUI content changes, the containing cell is automatically resized if necessary.
You can learn more about how this new feature works in the "What's New in UIKit" video from WWDC 2022.
There's one more aspect of data flow that you may need to handle, and that's sending data out from a SwiftUI view back to other parts of your app.
Once again, ObservableObject has got you covered! You can create a two-way binding to a published property of an ObservableObject.
Not only will data flow from the ObservableObject into SwiftUI, but SwiftUI can write back changes to the property on the model object.
Let's go through a simple example of creating a two-way binding by making the text in our MedicalCondition cell editable.
Here is our ObservableObject, MedicalCondition.
It stores a unique identifier in an ID property.
This is the identifier used to populate the diffable data source snapshot.
And this published property stores the text of the medical condition.
Here's the MedicalConditionView that displays the medical condition text inside each cell.
Right now this text is read-only, so let's make it editable.
All we need to do is to change the Text view to a TextField and create a binding to the text property of our MedicalCondition by adding a dollar sign prefix.
When you type into the text field, this binding allows SwiftUI to write back changes directly to the ObservableObject.
That's really how simple it is to set up two-way data flow with SwiftUI.
UIHostingController is a powerful way to embed SwiftUI content into your UIKit app.
Your SwiftUI view is rendered inside the hosting controller's view, and you can use the hosting controller anywhere that you can use a view controller in UIKit.
When using UIHostingController, make sure to always add the view controller together with the view to your app.
Many SwiftUI features, such as toolbars, keyboard shortcuts, and views that use UIViewControllerRepresentable, require a connection to the view controller hierarchy in UIKit to integrate properly, so never separate a hosting controller's view from the hosting controller itself.
For comparison, when you apply a UIHostingConfiguration to a cell, your SwiftUI view is hosted in the cell without a UIViewController.
UIHostingConfiguration supports the vast majority of SwiftUI features.
But keep in mind that SwiftUI views that depend on UIViewControllerRepresentable can't be used inside of cells.
With UIHostingController and UIHostingConfiguration, you have two great ways to incorporate SwiftUI into your UIKit app.
SwiftUI integrates seamlessly into existing UIKit apps Use UIHostingController to add SwiftUI throughout your app.
Create custom cells in your collection and table view using UIHostingConfiguration.
And take advantage of ObservableObject, so your data and UI is always in sync.
Add SwiftUI to your app today! Thank you for watching! ♪
// Embedding a UIHostingControllerlet heartRateView =HeartRateView() // a SwiftUI viewlet hostingController =UIHostingController(rootView: heartRateView)
// Add the hosting controller as a child view controllerself.addChild(hostingController)
// Now position & size the hosting controller’s view as desired…