Я сделал пользовательский UITableView
для реализации с SwiftUI, чтобы настроить вид заголовка и заголовок раздела. Каждый элемент написан на SwiftUI и имеет заданную высоту. Таблица обернута в GeometryReader
.
. Мне нужно сохранить смещение прокрутки при переходе между страницами, поэтому каждый раз, когда я нажимаю на элемент, я сохраняю contentOffset
в @ObservableObject
, и когда возвращаясь к этому виду, я просто передаю сохраненное смещение (я не использую стандартную NavigationLink
навигацию, а пользовательский стек, поэтому он не сохраняется между страницами).
Проблема в том, что всякий раз, когда содержимое UITableView
загружается с ранее установленным contentOffset
(что по умолчанию равно (x:0; y:0)
), показанное содержимое всегда является предыдущим содержимым (т. е. если у меня есть 14 строк, и я нажимаю на строку 14, setContentOffset
показывает только строки до строки 8/9). Этого не произойдет, если я коснусь первых строк, например 5 или 6.
Я уже пробовал разные решения, например, установил словарь height для строк, сохранив их высоту и передача его методам делегата, но это не работает. Также layoutIfNeeded()
, примененный к UITableView
во время makeUIView
, ничего не делает.
В настоящее время я не могу установить automaticallyAdjustScrollViewInsets = false
, потому что
- Я бы хотел переписать весь компонент так, чтобы он поместился в
UIViewController
- .
contentInset
уже всегда равен нулю, что, я думаю, и является целью этой инструкции.
Что я заметил, однако, что мой UITableViewRepresentable
внутри GeometryReader
нарисован дважды. Я не уверен, почему, но это просто происходит. Только во второй раз containerSize
отличается от нуля.
Это мой код:
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
}
}
}
И это 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 просто содержит scrollOffset
class ListData: ObservableObject {
@Published var scrollOffset: CGPoint = CGPoint(x:0, y:0)
}
Я не понимаю этого поведения, но я также новичок в UIKit, так что я не знаю, предназначено это или нет. Любая помощь очень ценится.