Добавить SwiftUI View в UITableViewCell contentView

В настоящее время я пытаюсь реализовать UITableViewController в UIViewControllerRepresentable, где содержимое ячеек снова является представлениями SwiftUI. Я не могу использовать список SwiftUI, потому что я хочу добавить UISearchController позже.
Поскольку я хочу иметь возможность помещать пользовательский вид SwiftUI в качестве содержимого каждой ячейки, я не могу сделать это без представлений SwiftUI внутри ячеек.
Мой текущий код, который не работает, выглядит так:

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
        }
    }
}

Когда я запускаю это, например, с этой настройкой предварительного просмотра:

#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

Я вижу только TableView с 4 явно пустыми ячейками. В отладчике иерархии представлений я вижу, что в каждой ячейке действительно есть NavigationLink с текстом внутри подпредставления, его просто не видно. Поэтому я думаю, что это связано с добавлением UIHostingController в качестве дочернего элемента UITableViewController, но я просто не знаю, где я должен добавить его еще.
Есть ли способ сделать это в данный момент?

4 ответа

Я обнаружил это, пытаясь сделать то же самое, и это сработало для меня.

Для меня это было необходимо для подкласса и скрытия панели навигации

      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
    }
}

И это основной объект использования

      /// 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()
        }
    }
}

И зарегистрируйтесь так:

      tableView.register(HostingTableViewCell<YourSwiftUIView>.self, forCellReuseIdentifier: "WhateverIDYouWant")

А затем используйте так:

      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)

Чтобы решить проблему с видимостью ячеек, измените UIHostingController для свойства AutoresizingMaskIntoConstraints значение false,
а затем установите его фрейм просмотра, равный границам содержимого ячейки, или вы можете использовать NSLayoutConstraint,
проверьте ниже

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")
    }
}

iOS 16 и выше

UIHostingConfiguration Конфигурация контента, подходящая для размещения иерархии представлений SwiftUI.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)
    }
    
}

По умолчанию добавление представления таким образом будет иметь отступы вокруг него. Чтобы удалить его, мы используем.margins(.all, 0). Кредиты на удаление отступов при использовании UIHostingConfiguration в ячейке?.

Если кому-то нужно универсальное решение, я собрал кое-что, что, кажется, работает именно так, как вы ожидаете. Я отправлю его здесь, если кто-нибудь найдет этот вопрос в Google, как я.

import SwiftUI

class HostingTableViewCell<Content: View>: UITableViewCell {

    private weak var controller: UIHostingController<Content>?

    func host(_ view: Content, parent: UIViewController) {
        if let controller = controller {
            controller.rootView = view
            controller.view.layoutIfNeeded()
        } else {
            let swiftUICellViewController = UIHostingController(rootView: view)
            controller = swiftUICellViewController
            swiftUICellViewController.view.backgroundColor = .clear

            layoutIfNeeded()

            parent.addChild(swiftUICellViewController)
            contentView.addSubview(swiftUICellViewController.view)
            swiftUICellViewController.view.translatesAutoresizingMaskIntoConstraints = false
            contentView.addConstraint(NSLayoutConstraint(item: swiftUICellViewController.view!, attribute: NSLayoutConstraint.Attribute.leading, relatedBy: NSLayoutConstraint.Relation.equal, toItem: contentView, attribute: NSLayoutConstraint.Attribute.leading, multiplier: 1.0, constant: 0.0))
            contentView.addConstraint(NSLayoutConstraint(item: swiftUICellViewController.view!, attribute: NSLayoutConstraint.Attribute.trailing, relatedBy: NSLayoutConstraint.Relation.equal, toItem: contentView, attribute: NSLayoutConstraint.Attribute.trailing, multiplier: 1.0, constant: 0.0))
            contentView.addConstraint(NSLayoutConstraint(item: swiftUICellViewController.view!, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: contentView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1.0, constant: 0.0))
            contentView.addConstraint(NSLayoutConstraint(item: swiftUICellViewController.view!, attribute: NSLayoutConstraint.Attribute.bottom, relatedBy: NSLayoutConstraint.Relation.equal, toItem: contentView, attribute: NSLayoutConstraint.Attribute.bottom, multiplier: 1.0, constant: 0.0))

            swiftUICellViewController.didMove(toParent: parent)
            swiftUICellViewController.view.layoutIfNeeded()
        }
    }
}

В вашем UITableViewController:

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(HostingTableViewCell<Text>.self, forCellReuseIdentifier: "textCell")
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "textCell") as! HostingTableViewCell<Text>
        cell.host(Text("Yay!"), parent: self)
        return cell
    }

Может превратить это в пакет, если люди будут его использовать.

Другие вопросы по тегам