Skip to content

Instantly share code, notes, and snippets.

@simonbs
Last active November 10, 2024 07:46
Show Gist options
  • Save simonbs/457725e218fc85fc765ed770815314df to your computer and use it in GitHub Desktop.
Save simonbs/457725e218fc85fc765ed770815314df to your computer and use it in GitHub Desktop.
In iOS 16, Apple added UIHostingConfiguration, making it straightforward to use a SwiftUI view in instances of UICollectionViewCell and UITableViewCell. This project shows how UIHostingConfiguration can be backported to iOS 14 by implementing a type that conforms to the UIContentConfiguration protocol.

SwiftUIHostingConfiguration

In iOS 16, Apple added UIHostingConfiguration, making it straightforward to use a SwiftUI view in instances of UICollectionViewCell and UITableViewCell. This gist shows how UIHostingConfiguration can be backported to iOS 14 by implementing a type that conforms to UIContentConfiguration.

Starting from iOS 16, we can use SwiftUI views in an instance of UICollectionView or UITableViewCell like this:

cell.contentConfiguration = UIHostingConfiguration {
    ExampleCellContentView(color: Color(item.color), text: item.text)
}

With the SwiftUIHostingConfiguration, we can use a similar API starting from iOS 14.

cell.contentConfiguration = SwiftUIHostingConfiguration(parentViewController: self) {
    ExampleCellContentView(color: Color(item.color), text: item.text)
}

We pass an instance of our view controller because, under the hood, the SwiftUIHostingConfiguration creates a UIHostingController and inserts it into the view controller hierarchy.

video.mov
import SwiftUI
import UIKit
struct SwiftUIHostingConfiguration<Content: View>: UIContentConfiguration {
private weak var parentViewController: UIViewController?
private let content: Content
init(parentViewController: UIViewController, @ViewBuilder content: () -> Content) {
self.parentViewController = parentViewController
self.content = content()
}
private init(parentViewController: UIViewController, content: Content) {
self.parentViewController = parentViewController
self.content = content
}
func makeContentView() -> any UIView & UIContentView {
let contentView = ContentView(configuration: self)
contentView.layoutMargins = .zero
return contentView
}
func updated(for state: any UIConfigurationState) -> SwiftUIHostingConfiguration {
guard let parentViewController else {
fatalError("Cannot update \(self) because parentViewController has been deallocated")
}
return SwiftUIHostingConfiguration(parentViewController: parentViewController, content: content)
}
}
private extension SwiftUIHostingConfiguration {
final class ContentView: UIView, UIContentView {
var configuration: UIContentConfiguration {
get {
currentConfiguration
}
set {
guard let newConfiguration = newValue as? SwiftUIHostingConfiguration<Content> else {
fatalError("Expected configuration to be of type \(SwiftUIHostingConfiguration<Content>.self)")
}
currentConfiguration = newConfiguration
hostingController.rootView = newConfiguration.content
// Removing and re-adding the hosting controller from the view hierarchy and then invalidating
// the intrinsic content size seems to be the only way to cause the UICollectionVIew and
// UITableView to relayout the cells and update the heights.
removeHostingControllerFromViewHierarchy()
addHostingControllerToViewHierarchy()
invalidateIntrinsicContentSize()
}
}
private let hostingController: UIHostingController<Content>
private var currentConfiguration: SwiftUIHostingConfiguration<Content>
init(configuration: SwiftUIHostingConfiguration<Content>) {
self.hostingController = UIHostingController(rootView: configuration.content)
self.currentConfiguration = configuration
super.init(frame: .zero)
addHostingControllerToViewHierarchy()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
removeHostingControllerFromViewHierarchy()
}
func supports(_ configuration: any UIContentConfiguration) -> Bool {
configuration is SwiftUIHostingConfiguration<Content>
}
}
}
private extension SwiftUIHostingConfiguration.ContentView {
private func addHostingControllerToViewHierarchy() {
guard let parentViewController = currentConfiguration.parentViewController else {
fatalError("Cannot setup \(Self.self) as parentViewController has been deallocated")
}
parentViewController.addChild(hostingController)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
])
hostingController.didMove(toParent: parentViewController)
}
private func removeHostingControllerFromViewHierarchy() {
hostingController.willMove(toParent: nil)
hostingController.view.removeFromSuperview()
hostingController.removeFromParent()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment