Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add SwiftUI View to an UITableViewCell contentView

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?

like image 858
Josef Zoller Avatar asked Aug 23 '19 13:08

Josef Zoller


2 Answers

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")
    }
}
like image 73
ozmpai Avatar answered Dec 04 '22 16:12

ozmpai


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)
like image 29
Michael Ellis Avatar answered Dec 04 '22 15:12

Michael Ellis