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)"] }
}
}
Related
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
}
}
}
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)"] }
}
}
I'm setting up a collapsable tableView, but something strange happens on the collapsable item. When you look at the video keep an eye on the "Where are you located" line.. (I'm using a .plist for the question and answer items)
Where do I go wrong, is it somewhere in my code? I don't want to let that line stick on the top :(
Here is the code I'm using but I can't find anything strange...
class FAQViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var questionsArray = [String]()
var answersDict = Dictionary<String, [String]>() // multiple answers for a question
var collapsedArray = [Bool]()
#IBOutlet weak var tableView: UITableView!
override func viewWillAppear(_ animated: Bool) {
// Hide the navigation bar on the this view controller
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
addTableStyles()
readQAFile()
tableView.delegate = self
tableView.dataSource = self
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
func addTableStyles(){
navigationController?.isNavigationBarHidden = false
self.tableView?.backgroundView = {
let view = UIView(frame: self.tableView.bounds)
return view
}()
tableView.estimatedRowHeight = 43.0;
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = UITableViewCell.SeparatorStyle.singleLine
}
func readQAFile(){
guard let url = Bundle.main.url(forResource: "QA", withExtension: "plist")
else { print("no QAFile found")
return
}
let QAFileData = try! Data(contentsOf: url)
let dict = try! PropertyListSerialization.propertyList(from: QAFileData, format: nil) as! Dictionary<String, Any>
// Read the questions and answers from the plist
questionsArray = dict["Questions"] as! [String]
answersDict = dict["Answers"] as! Dictionary<String, [String]>
// Initially collapse every question
for _ in 0..<questionsArray.count {
collapsedArray.append(false)
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return questionsArray.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if collapsedArray[section] {
let ansCount = answersDict[String(section)]!
return ansCount.count
}
return 0
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
// Set it to any number
return 70
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 1
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if collapsedArray[indexPath.section] {
return UITableView.automaticDimension
}
return 2
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: CGRect(x:0, y:0, width:tableView.frame.size.width, height:40))
headerView.tag = section
let headerString = UILabel(frame: CGRect(x: 10, y: 10, width: tableView.frame.size.width, height: 50)) as UILabel
headerString.text = "\(questionsArray[section])"
headerView .addSubview(headerString)
let headerTapped = UITapGestureRecognizer (target: self, action:#selector(sectionHeaderTapped(_:)))
headerView.addGestureRecognizer(headerTapped)
return headerView
}
#objc func sectionHeaderTapped(_ recognizer: UITapGestureRecognizer) {
let indexPath : IndexPath = IndexPath(row: 0, section:recognizer.view!.tag)
if (indexPath.row == 0) {
let collapsed = collapsedArray[indexPath.section]
collapsedArray[indexPath.section] = !collapsed
//reload specific section animated
let range = Range(NSRange(location: indexPath.section, length: 1))!
let sectionToReload = IndexSet(integersIn: range)
self.tableView.reloadSections(sectionToReload as IndexSet, with:UITableView.RowAnimation.fade)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cellIdentifier = "Cell"
let cell: UITableViewCell! = self.tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
cell.textLabel?.adjustsFontSizeToFitWidth = true
cell.textLabel?.numberOfLines = 0
let manyCells : Bool = collapsedArray[indexPath.section]
if (manyCells) {
let content = answersDict[String(indexPath.section)]
cell.textLabel?.text = content![indexPath.row]
}
return cell
}
override var prefersStatusBarHidden: Bool {
return true
}
}
You need to change the style of the tableView to grouped, when you initialize it:
let tableView = UITableView(frame: someFrame, style: .grouped)
or from Storyboard:
After that you will have this issue, which I solved by setting a tableHeaderView to the tableView that has CGFloat.leastNormalMagnitude as its height:
override func viewDidLoad() {
super.viewDidLoad()
var frame = CGRect.zero
frame.size.height = .leastNormalMagnitude
tableView.tableHeaderView = UIView(frame: frame)
}
Just remove your headerView from view hierarchy here
#objc func sectionHeaderTapped(_ recognizer: UITapGestureRecognizer) {
headerView.removeFromSuperview()
...
}
By the way, yes creating a openable tableview menu with using plist is one of the methods but it could be more simple. In my opinion you should refactor your code.
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?
I made a custom UITableView to be implemented with SwiftUI, to customize the header view and section headers. Every item is written in SwiftUI, and has a set height. The table is wrapped inside a GeometryReader.
I need to save the scroll offset while navigating between pages, so everytime I tap on an item, I save the contentOffset in an #ObservableObject, and when navigating back to that view, I just pass the saved offset (I'm not using the standard NavigationLink navigation, but a custom stack, so it is not saved between pages).
The problem is that, whenever the UITableView content is loaded with a previously set contentOffset (which is (x:0; y:0) by default), the content shown is always the previous content (i.e. if I have 14 rows and I tap on row 14, the setContentOffset only shows rows up to row 8/9).
This doesn't happen if I tap on the first rows, like 5 or 6.
I've already tried different solutions, like setting a height dictionary for rows, saving their height and passing it to the delegate methods, but it doesn't work.
Also layoutIfNeeded(), applied to the UITableView during the makeUIView doesn't do anything.
I currently can't set automaticallyAdjustScrollViewInsets = false because
I would have to rewrite the entire component to fit in a UIViewController
The contentInset is already always zero, which I think is the purpose of that instruction.
What I've noticed though, is that my UITableViewRepresentable inside the GeometryReader is drawn twice. I'm not sure why, but it just happens. Only the second time, the containerSize is different than zero.
This is my code:
UITableViewRepresentable
import SwiftUI
import UIKit
struct UITableViewRepresentable: UIViewRepresentable {
var sections: [String]
var items: [Int:[AnyView]]
var tableHeaderView: AnyView? = nil
var separatorStyle: UITableViewCell.SeparatorStyle = .singleLine
var separatorInset: UIEdgeInsets?
var scrollOffset: CGPoint
var onTap: (CGPoint) -> Void
var sectionHorizontalPadding: CGFloat = 5
var sectionHeight: CGFloat = 50
var containerSize: CGSize
func makeUIView(context: Context) -> UITableView {
assert(items.count > 0)
let uiTableView = UITableView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: self.containerSize), style: .plain)
uiTableView.sizeToFit()
uiTableView.separatorStyle = self.separatorStyle
if(self.separatorStyle == .singleLine && self.separatorInset != nil) {
uiTableView.separatorInset = self.separatorInset!
}
uiTableView.automaticallyAdjustsScrollIndicatorInsets = false
uiTableView.dataSource = context.coordinator
uiTableView.delegate = context.coordinator
if(tableHeaderView != nil) {
let hostingHeader: UIHostingController = UIHostingController<AnyView>(rootView: tableHeaderView!)
uiTableView.tableHeaderView = hostingHeader.view
uiTableView.tableHeaderView!.sizeToFit()
}
uiTableView.register(HostingCell.self, forCellReuseIdentifier: "Cell")
return uiTableView
}
func updateUIView(_ uiTableView: UITableView, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(self, sectionHeight: self.sectionHeight)
}
class HostingCell: UITableViewCell { // just to hold hosting controller
var host: UIHostingController<AnyView>?
}
class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource {
var parent: UITableViewRepresentable
var sectionHeight: CGFloat
var scrollOffset: CGPoint
var alreadyScrolled: Bool
init(_ parent: UITableViewRepresentable, sectionHeight: CGFloat) {
self.parent = parent
self.sectionHeight = sectionHeight
self.scrollOffset = self.parent.scrollOffset
self.alreadyScrolled = false
}
func numberOfSections(in tableView: UITableView) -> Int {
return self.parent.items.keys.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return parent.items[section]?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
let view = self.parent.items[indexPath.section]![indexPath.row]
// 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.layoutIfNeeded()
return tableViewCell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.scrollOffset = tableView.contentOffset
self.parent.onTap(self.scrollOffset)
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if(sectionHeight == 0) {
return nil
}
let headerView = UIView(
frame: CGRect(
x: 0,
y: 0,
width: tableView.frame.width,
height: sectionHeight
)
)
headerView.backgroundColor = App.Colors.NumberIcon.MainColor_UI
let label = UILabel()
label.frame = CGRect.init(
x: self.parent.sectionHorizontalPadding,
y: headerView.frame.height / 2,
width: headerView.frame.width,
height: headerView.frame.height / 2
)
label.text = self.parent.sections[section].uppercased()
label.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.footnote).bold()
label.textColor = .white
headerView.addSubview(label)
return headerView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return sectionHeight
}
fileprivate var heightDictionary: [Int : CGFloat] = [:]
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
heightDictionary[indexPath.row] = cell.frame.size.height
// if the first row has been drawed, then the content is ready, and the UITableView can scroll
if let _ = tableView.indexPathsForVisibleRows?.first, self.scrollOffset.y != 0 {
if indexPath.row == 0 && !self.alreadyScrolled {
tableView.setContentOffset(self.scrollOffset, animated: false)
self.alreadyScrolled = true // to prevent further updates of redeclarations of Coordinator
}
}
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let height = heightDictionary[indexPath.row]
return height ?? UITableView.automaticDimension
}
}
}
And this is my ContentView
struct ContentView: View {
#ObservedObject var listData: ListData = ListData()
var body: some View {
GeometryReader { geometry -> AnyView in
let tableHeaderView = AnyView(Text("TableHeaderView"))
let itemHeight: CGFloat = geometry.size.height * 1/3
let items:[AnyView] = [AnyView(Text("Item 1").frame(height: itemHeight)), AnyView(Text("Item 2").frame(height: itemHeight))]
return UITableViewRepresentable(
sections: ["Section 1"],
items: [0:items],
tableHeaderView: tableHeaderView,
separatorStyle: .none,
scrollOffset: self.listData.scrollOffset,
onTap: { (scrollOffset) in
self.listData.scrollOffset = scrollOffset
// navigate to other page...
},
sectionHorizontalPadding: itemHorizontalPadding,
containerSize: CGSize(width: pageWidth, height: listHeight)
).frame(width: geometry.size.width, height: geometry.size.height * 0.9)
}
}
}
ListData just holds the scrollOffset
class ListData: ObservableObject {
#Published var scrollOffset: CGPoint = CGPoint(x:0, y:0)
}
I don't understand this behaviour, but I'm also a beginner of UIKit, so I don't know if it's intended or not. Any help is much appreciated.
In the end I had to resort to the UIScrollView.contentOffset property, which is correct 100% of the time.
Updated code:
func updateUIView(_ uiTableView: UITableView, context: Context) {
if(!context.coordinator.alreadyScrolled) {
uiTableView.layoutIfNeeded()
Utilities.Threading.UI {
// remove animations so it doesn't do the scrolling animation during the begin/endUpdates, it can be omitted if you like
UIView.performWithoutAnimation {
uiTableView.beginUpdates()
uiTableView.setContentOffset(self.scrollOffset, animated: false)
uiTableView.endUpdates()
}
context.coordinator.alreadyScrolled = true
}
}
}
and in the Coordinator
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.scrollIndex = indexPath
self.parent.onTap(indexPath, self.scrollOffset)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.scrollOffset = scrollView.contentOffset
}
I've also removed the code inside the willDisplayCell delegate method that scrolls automatically.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
heightDictionary[indexPath.row] = cell.frame.size.height
}