Добавить 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
}
Может превратить это в пакет, если люди будут его использовать.