Some cells of UITableView with UIViewRepresantable are positioned wrong - ios

I am trying to use UITableView with UIViewRepresantable, it kinda works, but not really. Some rows are placed in the wrong place, either overlapping other rows, either extra padding is added. Also it doesn't seem to adapt the row height to the content of the row even if I set automaticDimension.
Here is the demo:
Here is the code:
import SwiftUI
struct ContentView: View {
#State var rows : [String] = []
var listData = ListData()
var body: some View {
VStack {
Button(action: {
self.getDataFromTheServer()
}) {
Text("Get 100 entries from the server")
}
UIList(rows: $rows)
}
}
func getDataFromTheServer() {
for _ in 1...100 {
self.rows.append(self.listData.data)
self.listData.data.append("a")
}
}
}
class ListData {
var data: String = ""
}
class HostingCell: UITableViewCell {
var host: UIHostingController<AnyView>?
}
struct UIList: UIViewRepresentable {
#Binding var rows: [String]
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.dataSource = context.coordinator
tableView.delegate = context.coordinator
tableView.register(HostingCell.self, forCellReuseIdentifier: "Cell")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = UITableView.automaticDimension
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
DispatchQueue.main.async {
uiView.reloadData()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: $rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
#Binding var rows: [String]
init(rows: Binding<[String]>) {
self._rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
let view = Text(rows[indexPath.row])
.background(Color.blue)
.lineLimit(nil)
// create & setup hosting controller only once
if tableViewCell.host == nil {
let hostingController = UIHostingController(rootView: AnyView(view))
tableViewCell.host = hostingController
let tableCellViewContent = hostingController.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
} else {
// reused cell, so just set other SwiftUI root view
tableViewCell.host?.rootView = AnyView(view)
}
tableViewCell.setNeedsLayout()
return tableViewCell
}
}
}

From Apple:
Thank you for your feedback, it is noted. Engineering has determined that there are currently no plans to address this issue.
We recommend using List, and filing a separate enhancement for any needs beyond what List can offer.

UIHostingControllers don’t size themselves very well, which I suspect is what’s wrecking your autolayout. Try adding this after you set the controller’s rootView:
hostingController.preferredContentSize = hostingController.sizeThatFits( )
That said, you may want to avoid the UIHostingController entirely… if you’re going to the trouble of using UITableView, why not make the cell in UIKit too? Or if you need SwiftUI for the rows, why not use List instead of hosting a UITableView?

Related

How to correctly make 2-way binding with UITableViewRepresentable and SwiftUI

I'm creating my own custom SwiftUI List because I need to use some underlying methods of tableview. I would like to know how to correctly bind a TextField inside a UITableView Cell to my SwiftUI ViewModel.
So basically I have a textfield inside a SwiftUI View and use this as my tableview cell with the new UIHostingConfiguration struct. To check to see if things are working correctly I have a normal SwiftUI List above the TableViewRepresentable. If you type in a textfield of List, the TableViewRepresentable gets updated correctly as you type. But if you type in the textfield of TableViewRepresentable, only the Text beside the textfield updates. The List doesn't update the textfield's text. But then when you click inside any textfield inside the List, the actual update to the textfield happens.
I have found a way to get things to work but it doesn't seem correct. Let me know what you guys think. Hopefully someone has the answer. I have looked everywhere on the internet but nobody has done an example with UITableViewRepresentable or UICollectionViewRepresentable that works with 2 way binding from the cell to the ViewModel.
Also if anyone was curious why I need to use a TableView, it's to adjust the scrolling behavior of a List or TabView(.page). I need to get the velocity, like targetContentOffset method of scrollview, to have regular paging and then on fast swipe have fast paging.
Here is the ViewModel and data.
class Item: Identifiable, ObservableObject {
let id = UUID().uuidString
#Published var title: String
init(title: String) {
self.title = title
}
}
class ItemsViewModel: ObservableObject {
#Published var items: [Item] = Array(0...40).map({ Item(title: "\($0)")})
}
Hosting Cell. Just using as a sample cell to dequeue. This was the old way I was using to inject a SwiftUI View in a cell.
class HostingCell<Content: View>: UITableViewCell {
var host: UIHostingController<Content>?
func setup(with view: Content) {
if host == nil {
let controller = UIHostingController(rootView: view)
host = controller
guard let content = controller.view else { return }
content.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(content)
content.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
content.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
content.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
content.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
} else {
host?.rootView = view
}
setNeedsLayout()
}
}
Non-Working Code. Updated code to mimic sample code that #smileyborg suggested from Apple's sample code.
struct TableViewContent2: View {
#EnvironmentObject var itemsViewModel: ItemsViewModel
var body: some View {
VStack {
List($itemsViewModel.items) { $item in
TextField("", text: $item.title)
}
TableViewRepresentable2(itemsViewModel) { item in
CellView2(cellItem: item)
}
.ignoresSafeArea()
}
}
}
struct TableViewRepresentable2<Content: View>: UIViewRepresentable {
#ObservedObject private var itemsViewModel: ItemsViewModel
private var content: (Item) -> Content
init(_ itemsViewModel: ItemsViewModel, #ViewBuilder content: #escaping (Item) -> Content) {
self.itemsViewModel = itemsViewModel
self.content = content
}
private let cellID = "CellID"
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.allowsSelection = false
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingCell<Content>.self, forCellReuseIdentifier: cellID)
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self, content: content)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var parent: TableViewRepresentable2
var content: (Item) -> Content
init(_ parent: TableViewRepresentable2, content: #escaping (Item) -> Content) {
self.parent = parent
self.content = content
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.itemsViewModel.items.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
tableView.bounds.height / 4
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: parent.cellID, for: indexPath) as! HostingCell<Content>
let cellItem = parent.itemsViewModel.items[indexPath.row]
cell.configurationUpdateHandler = { cell, state in
cell.contentConfiguration = UIHostingConfiguration {
self.content(cellItem)
}
.margins(.all, 0)
}
return cell
}
}
}
struct CellView2: View {
#ObservedObject var cellItem: Item
var body: some View {
ZStack {
Color.blue
.ignoresSafeArea()
VStack {
TextField("Placeholder", text: $cellItem.title)
.background(RoundedRectangle(cornerRadius: 5).foregroundColor(.white))
.padding()
Text("item: \(cellItem.title)")
}
}
}
}
WorkAround code. Big Difference is the CellView. I found a way by passing in the cell item, finding the index to the item, and then creating two computed properties to give me the binding item and the item.
struct Item: Identifiable {
let id = UUID().uuidString
var title: String
}
struct TableViewContent: View {
#EnvironmentObject private var itemsViewModel: ItemsViewModel
var body: some View {
VStack {
List($itemsViewModel.items) { $item in
TextField("", text: $item.title)
}
TableViewRepresentable(itemsViewModel.items) { item in
CellView(cellItem: item)
}
}
}
}
struct TableViewRepresentable<Content: View>: UIViewRepresentable {
private var items: [Item]
private var content: (Item) -> Content
init(_ items: [Item], #ViewBuilder content: #escaping (Item) -> Content) {
self.items = items
self.content = content
}
private let cellID = "CellID"
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingCell<Content>.self, forCellReuseIdentifier: cellID)
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self, content: content)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var parent: TableViewRepresentable
var content: (Item) -> Content
init(_ parent: TableViewRepresentable, content: #escaping (Item) -> Content) {
self.parent = parent
self.content = content
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parent.items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: parent.cellID, for: indexPath) as? HostingCell<Content> ?? UITableViewCell()
let item = parent.items[indexPath.row]
cell.contentConfiguration = UIHostingConfiguration {
content(item)
}
return cell
}
}
}
struct CellView: View {
#EnvironmentObject private var itemsViewModel: ItemsViewModel
var cellItem: Item
private var item$: Binding<Item> {
let index = itemsViewModel.items.firstIndex(where: { $0.id == cellItem.id })
return $itemsViewModel.items[index!]
}
private var item: Item {
let index = itemsViewModel.items.firstIndex(where: { $0.id == cellItem.id })
return itemsViewModel.items[index!]
}
var body: some View {
ZStack {
Color.blue
VStack {
TextField("Placeholder", text: item$.title)
.background(RoundedRectangle(cornerRadius: 5).foregroundColor(.white))
Text("item: \(item.title)")
}
.padding()
}
}
}

SwiftUI Incorrect button behaviour in UITableView in Sheet view

I have problem with use Button as content in UITableView(UIViewRepresentable), which contains in Sheet. Button stayed pressed, when you try scroll table.
Here toy can download my proj https://github.com/MaksimBezdrobnoi/UITableViewInSheet
I create a simple UITableView where put SUI Button, and put Table in SUI Sheet.
Here my SUI view
struct TableSample: View {
var body: some View {
ZStack {
Color.clear
.sheet(isPresented: .constant(true)) {
AwesomeNewTable {
Button(action: {
print("HELLO")
}, label: {
Color.red
.frame(height: 30)
.padding(.horizontal, 16)
.padding(.bottom, 4)
})
}
}
}
}
}
And here my TableView
struct AwesomeNewTable<Content: View>: UIViewRepresentable {
private let content: () -> Content
init(content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.separatorStyle = .none
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
tableView.register(HostingCell<Content>.self, forCellReuseIdentifier: "Cell")
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
context.coordinator.parent = self
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource {
var parent: AwesomeNewTable
init(parent: AwesomeNewTable) {
self.parent = parent
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
150
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? HostingCell<Content> else {
return UITableViewCell()
}
let view = parent.content()
tableViewCell.setup(with: view)
return tableViewCell
}
}
}

iOS: UI Distortion when using SwiftUI views in UIKit TableView [duplicate]

I am trying to use UITableView in a SwiftUI app
struct UIList: UIViewRepresentable {
var rows: [String]
func makeUIView(context: Context) -> UITableView {
let collectionView = UITableView(frame: .zero, style: .plain)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
return collectionView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var rows: [String]
init(rows: [String]) {
self.rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) //as! AlbumPrivateCell
let view = Text(rows[indexPath.row]).frame(height: 50).background(Color.blue)// UIFactory(appComponent:
let controller = UIHostingController(rootView: view)
let tableCellViewContent = controller.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
return tableViewCell
}
}
}
When I scroll the table quickly the cells content get a random padding at top and bottom of each cell, any idea why this happens ?
PS: I know I could use List , I try to use UITableView because I have to add multiple swipe actions, but List only allows one swipe action (delete)
I assume this is due to corrupted reused table view cells... (and probably lost hosting controllers, because there where created on stack and not stored anywhere)
Please find below corrected a bit your code with mentioned fixes. Tested & worked with Xcode 11.2 / iOS 13.2.
Here is code (with some comments inline):
class HostingCell: UITableViewCell { // just to hold hosting controller
var host: UIHostingController<AnyView>?
}
struct UIList: UIViewRepresentable {
var rows: [String]
func makeUIView(context: Context) -> UITableView {
let collectionView = UITableView(frame: .zero, style: .plain)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(HostingCell.self, forCellReuseIdentifier: "Cell")
return collectionView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var rows: [String]
init(rows: [String]) {
self.rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
let view = Text(rows[indexPath.row])
.frame(height: 50).background(Color.blue)
// create & setup hosting controller only once
if tableViewCell.host == nil {
let controller = UIHostingController(rootView: AnyView(view))
tableViewCell.host = controller
let tableCellViewContent = controller.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
} else {
// reused cell, so just set other SwiftUI root view
tableViewCell.host?.rootView = AnyView(view)
}
tableViewCell.setNeedsLayout()
return tableViewCell
}
}
}
Added demo code for just UIList itself - works fine with pro models as well.
struct TestUIList: View {
var body: some View {
UIList(rows: generateRows())
}
func generateRows() -> [String] {
(0..<100).reduce([]) { $0 + ["Row \($1)"] }
}
}

Diffable Data Source with a custom collection view layout?

Here I have created a sample app that uses diffable data source for a collection view with a custom collection view layout. The specific layout I am using is from this tutorial.
Here is the relevant part of the code if you don't want to clone the repo and try it for yourself.
import UIKit
let cellIdentifier = "testRecordCell"
struct Record:Hashable {
let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: Record, rhs: Record) -> Bool {
return lhs.identifier == rhs.identifier
}
var timeStamp: Date
init(daysBack: Int){
self.timeStamp = Calendar.current.date(byAdding: .day, value: -1*daysBack, to: Date())!
}
}
class Cell:UICollectionViewCell {
}
class Section: Hashable {
var id = UUID()
// 2
var records:[Record]
init(records:[Record]) {
self.records = records
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Section, rhs: Section) -> Bool {
lhs.id == rhs.id
}
}
extension Section {
static var allSections: [Section] = [
Section(records: [
Record(daysBack: 5),Record(daysBack: 6)
]),
Section(records: [
Record(daysBack: 3)
])
]
}
class ViewController: UICollectionViewController {
private lazy var dataSource = makeDataSource()
private var sections = Section.allSections
fileprivate typealias DataSource = UICollectionViewDiffableDataSource<Section,Record>
fileprivate typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<Section,Record>
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView?.register(Cell.self, forCellWithReuseIdentifier: cellIdentifier)
applySnapshot()
if let layout = collectionView.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
}
}
extension ViewController {
fileprivate func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: self.collectionView,
cellProvider: { (collectionView, indexPath, testRecord) ->
UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
cell.backgroundColor = .black
return cell
})
return dataSource
}
func applySnapshot(animatingDifferences: Bool = true) {
// 2
var snapshot = DataSourceSnapshot()
snapshot.appendSections(sections)
sections.forEach { section in
snapshot.appendItems(section.records, toSection: section)
}
//This part errors out: "request for number of items in section 0 when there are only 0 sections in the collection view"
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
}
extension ViewController: PinterestLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
return CGFloat(10)
}
}
Somehow, the layout is not registering that the collection view does have items and sections in it. When you run it normally, it errors out when you are trying to apply the snapshot: "request for number of items in section 0 when there are only 0 sections in the collection view"
Then, in the prepare() function of the Pinterest layout, when I set a breakpoint and inspect collectionView.numberOfSections() it returns 0. So somehow the snapshot is not communicating with the collection view. Notice that I never use the collectionView's delegate method numberOfSections because I am using the diffable data source...
My impression is that diffable data source is usually used with compositional layout though I have not seen anywhere that this is a requirement.
So is there any way to do this?
The problem is this line:
func applySnapshot(animatingDifferences: Bool = true) {
Change true to false and you won't crash any more.
Hi it's too late for answer but i tried same way and same problem
My case problem is approach numberOfItems in collectionView
82 lines in PinterestLayout below
"for item in 0..<collectionView.numberOfItems(inSection: 0) {"
so.. i inject actual numberOfItems in snapshot from view to custom layout
myLayout.updateNumberOfItems(currentSnapshot.numberOfItems)
dataSource.apply(currentSnapshot)
and just use the numberOfItems instead of collectionView.numberOfItems.
and it's work for me
Please let me know if i'm wrong thx ")

UITableView with UIViewRepresentable in SwiftUI

I am trying to use UITableView in a SwiftUI app
struct UIList: UIViewRepresentable {
var rows: [String]
func makeUIView(context: Context) -> UITableView {
let collectionView = UITableView(frame: .zero, style: .plain)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
return collectionView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var rows: [String]
init(rows: [String]) {
self.rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) //as! AlbumPrivateCell
let view = Text(rows[indexPath.row]).frame(height: 50).background(Color.blue)// UIFactory(appComponent:
let controller = UIHostingController(rootView: view)
let tableCellViewContent = controller.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
return tableViewCell
}
}
}
When I scroll the table quickly the cells content get a random padding at top and bottom of each cell, any idea why this happens ?
PS: I know I could use List , I try to use UITableView because I have to add multiple swipe actions, but List only allows one swipe action (delete)
I assume this is due to corrupted reused table view cells... (and probably lost hosting controllers, because there where created on stack and not stored anywhere)
Please find below corrected a bit your code with mentioned fixes. Tested & worked with Xcode 11.2 / iOS 13.2.
Here is code (with some comments inline):
class HostingCell: UITableViewCell { // just to hold hosting controller
var host: UIHostingController<AnyView>?
}
struct UIList: UIViewRepresentable {
var rows: [String]
func makeUIView(context: Context) -> UITableView {
let collectionView = UITableView(frame: .zero, style: .plain)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(HostingCell.self, forCellReuseIdentifier: "Cell")
return collectionView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var rows: [String]
init(rows: [String]) {
self.rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
let view = Text(rows[indexPath.row])
.frame(height: 50).background(Color.blue)
// create & setup hosting controller only once
if tableViewCell.host == nil {
let controller = UIHostingController(rootView: AnyView(view))
tableViewCell.host = controller
let tableCellViewContent = controller.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
} else {
// reused cell, so just set other SwiftUI root view
tableViewCell.host?.rootView = AnyView(view)
}
tableViewCell.setNeedsLayout()
return tableViewCell
}
}
}
Added demo code for just UIList itself - works fine with pro models as well.
struct TestUIList: View {
var body: some View {
UIList(rows: generateRows())
}
func generateRows() -> [String] {
(0..<100).reduce([]) { $0 + ["Row \($1)"] }
}
}

Resources