DTTableViewManager
introduces support for rendering SwiftUI views in UITableViewCells starting with 11.x release. Registering SwiftUI view is done similarly to registering usual cells:
manager.registerHostingCell(for: Post.self) { model, indexPath in
PostSwiftUICell(model: model)
}
This functionality is supported on iOS 13 + / tvOS 13+ / macCatalyst 13+. It's important to understand, that this method of showing SwiftUI views in table / collection view cells is not supported by Apple, and has some hacks implemented to make it work.
Implementation for iOS 16+ method of showing SwiftUI views via hosting configuration (https://developer.apple.com/documentation/SwiftUI/UIHostingConfiguration) is hopefully coming a bit later.
Registration of SwiftUI views follows the same pattern as registering other table view cells, however there are some important distinctions:
- SwiftUI lifecycle management is done by special subclass of UITableViewCell -
HostingTableViewCell
, provided byDTTableViewManager
. - SwiftUI views need to be hosted in UIHostingController, which needs to be added as a child to view controller hierarchy, or appearance and sizing methods will not work in SwiftUI view. This is done automatically by
HostingTableViewCell
, but may have some unintended consequences, which you can read about below. - Because SwiftUI views are generally self-sizing, it's recommended to use this approach with self-sizing UITableView.
Let's dive into those topics, as they are important to understand how to use this approach correctly.
When SwiftUI view (it's UIHostingController) is added to view controller hierarchy, it tries to control several things that may be surprizing in context of UITableViewCell content:
- Navigation bar appearance
- Keyboard avoidance / safe area insets
- Other view controller behaviors I did not encounter yet
For example, in the app I'm working on, adding such hosted cell in view controller that had navigation bar hidden, immediately forced navigation bar to appear. In order to fix this problem, DTTableViewManager
provides a way to customize UIHostingController
used to host table view cells.
To always hide navigation bar in my view hierarchy, I implemented following subclass of UIHostingController (full credit to this answer on StackOverflow answer:
class ControlledNavigationHostingController<Content: View>: UIHostingController<Content> {
public override init(rootView: Content) {
super.init(rootView: rootView)
}
@objc dynamic required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = true
}
}
Using this subclass with DTTableViewManager
requires modifiying hosting configuration, available via mapping closure:
manager.registerHostingCell(for: Post.self) { model, _ in
PostSwiftUICell(model: model)
} mapping: { mapping in
mapping.configuration.hostingControllerMaker = {
ControlledNavigationHostingController(rootView: $0)
}
}
I'm assuming other potential issues, like keyboard avoidance, can also be solved by custom UIHostingController subclass, or swizzling UIHostingController methods. For example, here is great article by Peter Steinberger, that shows how to disable keyboard avoidance for SwiftUI views embedded in table view or collection view cells.
HostingTableViewCell
requires parent view controller to add SwiftUI to view controller hierarchy. DTTableViewManager
provides default parent view controller by typecasting DTTableViewManageable
instance to UIViewController type. If your class implementing DTTableViewManageable
is a view controller, you don't need to do anything.
However, if DTTableViewManageable
instance is not a view controller, you would need to specify parent view controller explicitly in mapping closure:
mapping.configuration.parentController = customParentViewController
Because SwiftUI views are generally self-sized, it's recommended to use self-sizing table view with them. To do that, use automatic height and provide estimated height for cell:
tableView.rowHeight = UITableView.automaticDimension
manager.registerHostingCell(for: Post.self) { model, _ in
PostSwiftUICell(model: model)
} mapping: { mapping in
mapping.estimatedHeightForCell { model, indexPath in
100 // pick appropriate cell height
}
}
If you can't or don't want to use automatic cell sizing, make sure SwiftUI view and cells have equal heights, otherwise SwiftUI and autolayout system may fight and produce unexpected results.
While HostingTableViewCell
hosts SwiftUI view, it does not communicate to UITableView
with any special information on doing so. So, if for example, you simultaneously implement SwiftUI.Button in a cell, and .didSelect event (tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
), they will not play together nicely.
Instead, consider implementing .onTapGesture
modifier on SwiftUI view and passing events through it's data model (view model would probably fit better here).
HostingTableViewCell
also supresses selection state by default to prevent clashing of different cell states SwiftUI is not aware of (UITableViewCell.SelectionStyle = .none
).
HostingTableViewCell
is designed to be just a container for SwiftUI view, so all it's views have background of UIColor.clear by default, and cell is not selectable. If you need, you can customize colors and selection state of the cell using configuration:
mapping.configuration.backgroundColor = customColor
mapping.configuration.contentViewBackgroundColor = customColor
mapping.configuration.hostingViewBackgroundColor = customColor
mapping.configuration.selectionStyle = .default
If you need any other changes on HostingTableViewCell
, you can also provide a closure, that is run after all cell updates:
mapping.configuration.configureCell = { cell in
// Customize cell
}
Each cell creates only one hosting controller, that is reused when cell is updated with new data.
In order to preserve perfomance, background colors and selection state is set only once when cell is first created. When cell is being reused, only configureCell
closure is called on each cell update.
I leave answer to this question for your consideration, since Apple does not support this, and some hacks may be required to work with hosted cells.
For me, however, it was 100% worth it. After applying navigation bar hack, I've encountered no problems, and live previewing table view cells in different view states is super helpful in implementing complex views, and is overall much simpler and efficient than doing it in UIKit.
SwiftUI hosted cells support all delegate methods implemenented for non-hosted cells, for example:
manager.registerHostingCell(for: Post.self) { model, _ in
PostSwiftUICell(model: model)
} mapping: { mapping in
mapping.willDisplay { cell, model, indexPath in
}
}
It seems possible, and code infrastructure is prepared to implement SwiftUI views in table view headers and footers, but I'm not rushing there yet. It's possible there might be some more hacks there, and I'm not sure at this point, if it's worth doing that, since Apple only introduced support for cells in iOS 16.
However, I might reconsider this, if there's demand for this feature.
UIHostingConfiguration on iOS 16 is supported by additional registration method:
manager.registerHostingConfiguration(for: Post.self) { cell, post, indexPath in
UIHostingConfiguration {
PostView(post: post)
}
}
Because this is officially supported way of integrating SwiftUI with table and collection view cells, I highly recommend watching WWDC 2022 session video on this topic.
All customization options for UIHostingConfiguration is fully supported, for example you can customize margins for cells content:
manager.registerHostingConfiguration(for: Post.self) { cell, post, indexPath in
UIHostingConfiguration {
PostView(post: post)
}.margins(.horizontal, 16)
}
Additionally, you can also use UICellConfigurationState
of a cell by simply adding one additional parameter:
manager.registerHostingConfiguration(for: Post.self) { state, cell, post, indexPath in
UIHostingConfiguration {
PostView(post: post, isSelected: state.isSelected)
}
}