Add SwiftUI View to an UITableViewCell contentView
Asked Answered
K

3

13

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?

Kosey answered 23/8, 2019 at 13:44 Comment(8)
Is this how one usually embeds sub-viewcontrollers in UIKit?Nuristan
@Nuristan yeah, you can take a look hereKosey
Pragmatic question - do you absolutely need the cells to be a SwiftUI View? I have a working UITableView inside a UITableViewRepresentable working because of needing a UINavigationController subclass. If the only thing you have to gain by using SwiftUI within a UIKit table within a SwiftUI app is... forward compatibility? PreviewProvider? Well, remember, a UIViewControllerRepresentable is considered to be merely a SwiftUI view.Numbskull
@dfd I want to use just a NavigationLink, I can edit the question to show it betterKosey
Ah. I needed navigation also! But I didn't need searching. What I have/had (sample code available) is/was a full UIKit table view, cells and all, that could be either a child view controller or a view in SwiftUI. But I was having issues making my "Edit" button triggering things in my SwiftUI model, so I'm headed down the way of using a List - with an additional button for "Add", since I couldn't find a good way to hide it in edit mode. Could you expose your UIKit logic as delegates, using a Coordinator?Numbskull
Could you maybe provide this code? Also I don't completely understand your last question, because I do work with an Coordinator.Kosey
I just posted a question here: #57676751 my "production" needs are to create an editable array - like you - with an Add button (along with the Edit/Done) and navigate through any cells into a detail view. At this point I'm having the issue I posted. My problem with a List? Embedding it in a NavigationView limits the appearance. I'm not expecting a solution to my issue... but one option I'm exploring involves using a SwiftUI Button as a replacement for the Add button....Numbskull
.... pretty much what you need maybe. If I think I can help, I'll post what I find.Numbskull
A
4

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)
Antimere answered 2/8, 2021 at 16:0 Comment(0)
C
3

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")
    }
}
Commissionaire answered 28/11, 2019 at 11:48 Comment(0)
C
1

iOS 16 and above

UIHostingConfiguration A content configuration suitable for hosting a hierarchy of SwiftUI views. https://developer.apple.com/documentation/SwiftUI/UIHostingConfiguration

class SomeCell: UITableViewCell {

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    func set(someData: SomeData) {
        self.contentConfiguration = UIHostingConfiguration {
            SomeView(someData: someData)
        }
        .margins(.all, 0)
    }
    
}

By default adding view this way will have padding around it. To remove it we use .margins(.all, 0). Credits to Remove padding when using UIHostingConfiguration in cell?.

Clothing answered 10/8, 2023 at 11:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.