I'm currently trying, to implement a UITableViewController in a UIViewControllerRepresentable, where the contents of the cells are SwiftUI Views again. I cannot use a SwiftUI List, because I want to add an UISearchController later on.
Because I want to to be able, to put a custom SwiftUI View as the content of each cell, it's no possibility for me, to do it without SwiftUI Views inside the cells.
My current code, which isn't working looks like this:
class SearchableListCell: UITableViewCell {
let contentController: UIViewController
init(withContent content: UIViewController, reuseIdentifier: String) {
self.contentController = content
super.init(style: .default, reuseIdentifier: reuseIdentifier)
self.addSubview(self.contentController.view)
// Tried also
// self.contentView.addSubview(self.contentController.view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct SearchableList: UIViewControllerRepresentable {
let data: [String]
var viewBuilder: (String) -> ContentView
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UITableViewController {
return context.coordinator.tableViewController
}
func updateUIViewController(_ tableViewController: UITableViewController, context: Context) {
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var parent: SearchableList
let tableViewController = UITableViewController()
init(_ searchableList: SearchableList) {
self.parent = searchableList
super.init()
tableViewController.tableView.dataSource = self
tableViewController.tableView.delegate = self
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return parent.data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let string = self.parent.data[indexPath.row]
let view = parent.viewBuilder(string)
let hostingController = UIHostingController(rootView: view)
let cell = SearchableListCell(withContent: hostingController, reuseIdentifier: "cell")
// Tried it with and without this line:
tableViewController.addChild(hostingController)
return cell
}
}
}
When I run this, for example with this Preview setup:
#if DEBUG
struct SearchableList_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
SearchableList(data: ["Berlin", "Dresden", "Leipzig", "Hamburg"]) { string in
NavigationLink(destination: Text(string)) { Text(string) }
}
.navigationBarTitle("Cities")
}
}
}
#endif
I see just a TableView with 4 apparently empty cells. In the view hierarchy debugger I can see though, that each cell has indeed the NavigationLink with Text inside as a subview, it's just not visible. Therefore I think, it has to do with adding the UIHostingController as a child of the UITableViewController, but I just don't know where I should add it else.
Is there a way to do this at the moment?
To solve cells visibility problem change UIHostingController translatesAutoresizingMaskIntoConstraints property to false
and then set its view frame equal to cell contentView bounds or you can use NSLayoutConstraint,
check below
class SearchableListCell: UITableViewCell {
let contentController: UIViewController
init(withContent content: UIViewController, reuseIdentifier: String) {
self.contentController = content
super.init(style: .default, reuseIdentifier: reuseIdentifier)
contentController.view.translatesAutoresizingMaskIntoConstraints = false
contentController.view.frame = self.contentView.bounds
self.contentView.addSubview(self.contentController.view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I found this while trying to do the same and this worked for me.
For me this was necessary to subclass and hide the Nav Bar
import UIKit
import SwiftUI
/// SwiftUI UIHostingController adds a navbar for some reason so we must disable it
class ControlledNavigationHostingController<Content>: UIHostingController<AnyView> where Content: View {
public init(rootView: Content) {
super.init(rootView: AnyView(rootView.navigationBarHidden(true)))
}
@objc dynamic required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = true
}
}
And this is the main object to use
/// This UITableViewCell wrapper allows you to have a SwiftUI View in your UITableView
class HostingTableViewCell<Content: View>: UITableViewCell {
/// This holds the SwiftUI View being displayed in this UITableViewCell wrapper
private weak var swiftUIContainer: ControlledNavigationHostingController<Content>?
/// Put the SwiftUI View into the contentView of the UITableViewCell, or recycle an exisiting instance and add the new SwiftUIView
///
/// - Parameter view: The SwiftUI View to be used as a UITableViewCell
/// - Parameter parent: The nearest UIViewController to be parent of the UIHostController displaying the SwiftUI View
/// - Warning: May be unpredictable on the Simulator
func host(_ view: Content, parent: UIViewController) {
if let container = swiftUIContainer {
// Recycle this view
container.rootView = AnyView(view)
container.view.layoutIfNeeded()
} else {
// Create a new UIHostController to display a SwiftUI View
let swiftUICellViewController = ControlledNavigationHostingController(rootView: view)
swiftUIContainer = swiftUICellViewController
// Setup the View as the contentView of the UITableViewCell
swiftUICellViewController.view.backgroundColor = .clear
// Add the View to the hierarchy to be displayed
parent.addChild(swiftUICellViewController)
contentView.addSubview(swiftUICellViewController.view)
swiftUICellViewController.view.translatesAutoresizingMaskIntoConstraints = false
if let view = swiftUICellViewController.view {
contentView.addConstraint(NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1.0, constant: 0.0))
contentView.addConstraint(NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: contentView, attribute: .trailing, multiplier: 1.0, constant: 0.0))
contentView.addConstraint(NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 0.0))
contentView.addConstraint(NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: 0.0))
}
swiftUICellViewController.didMove(toParent: parent)
swiftUICellViewController.view.layoutIfNeeded()
}
}
}
And Register like so:
tableView.register(HostingTableViewCell<YourSwiftUIView>.self, forCellReuseIdentifier: "WhateverIDYouWant")
And then use like so:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "YourCellID", for: indexPath) as? HostingTableViewCell<SomeSwiftUIView> else {
print("Error: Could Not Dequeue HostingTableViewCell<SomeSwiftUIView>")
return UITableViewCell()
}
cell.host(SomeSwiftUIView(), parent: self)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With