SwiftUI `.popover` doesn't work within UIHostingConfiguration cell displayed in a UITableViewController?

233 Views Asked by At

I'd like to use SwiftUI cells using the .contentConfiguration APIs. However, the SwiftUI view that I'd like to use also needs to be able to display a popover. I'm able to present an alert, but not able to present a popover.

Is there a way to do this using the .contentConfiguration API or do I need to display the SwiftUI cells in another manner. Eventually, I'll refactor the UITableViewController into Swift but for now I'd like to start introducing SwiftUI piece by piece.

The following shows .popover not working within the UIHostingConfiguration but working when presented within a UIHostingController.

import UIKit
import SwiftUI

class ViewController: UITableViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        3
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
        cell.contentConfiguration = UIHostingConfiguration { SwiftUIView(text: "Hello \(indexPath.row)") }

        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        present(UIHostingController(rootView: SwiftUIView(text: "full screen")), animated: true)
    }
}

struct SwiftUIView: View {
    @State var presentAlert: Bool = false
    @State var showPopover: Bool = false

    let text: String

    var body: some View {
        HStack {
            Text(text)
            Button("Present Alert") { presentAlert = true }
            Button("Present Popover") { showPopover = true }
        }.popover(isPresented: $showPopover) {
            Button("Hide Popover") {
                showPopover = false
            }
        }.alert(isPresented: $presentAlert) {
            Alert(title: Text("Alert"))
        }
    }
}

struct SwiftUIView_Previews: PreviewProvider {
    static var previews: some View {
        SwiftUIView(text: "Hello")
    }
}
1

There are 1 best solutions below

0
Sweeper On

I don't think that is currently supported by UIHostingConfiguration. One way I found to work around this is adding a UIHostingController as a child VC of the table VC.

let host = UIHostingController(rootView: SwiftUIView(text: "..."))
addChild(host)
host.view.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(host.view)
NSLayoutConstraint.activate([
    host.view.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
    host.view.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
    host.view.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor),
    host.view.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor),
])
host.didMove(toParent: self)
return cell

That doesn't take the fact that table views reuse the cells into account though. To handle that, it's probably more convenient to create a custom cell.

class SwiftUICell: UITableViewCell {
    private var host: UIViewController?
    override func prepareForReuse() {
        host?.willMove(toParent: nil)
        host?.view.removeFromSuperview()
        host?.removeFromParent()
    }
    
    func addHost<V: View>(_ host: UIHostingController<V>, to parent: UIViewController) {
        parent.addChild(host)
        host.view.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(host.view)
        NSLayoutConstraint.activate([
            host.view.topAnchor.constraint(equalTo: contentView.topAnchor),
            host.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            host.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            host.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
        ])
        host.didMove(toParent: parent)
        self.host = host
    }
}

Also consider adding some constants to the constraints, or some padding to the SwiftUI view, to more closely resemble the appearance created by UIHostingConfiguration. Otherwise, there is very little spacing between the cells' boundaries and their content views.