SwiftUI Custom UITableView Представляемый contentOffset всегда неправильно на последних элементах - PullRequest
0 голосов
/ 27 апреля 2020

Я сделал пользовательский 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, потому что

  1. Я бы хотел переписать весь компонент так, чтобы он поместился в UIViewController
  2. . 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, так что я не знаю, предназначено это или нет. Любая помощь очень ценится.

1 Ответ

0 голосов
/ 27 апреля 2020

В конце концов мне пришлось прибегнуть к свойству UIScrollView.contentOffset, которое является правильным в 100% случаев.

Обновленный код:

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

и в Координаторе

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    self.scrollIndex = indexPath
    self.parent.onTap(indexPath, self.scrollOffset)
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    self.scrollOffset = scrollView.contentOffset
}

Я также удалил код внутри метода делегата willDisplayCell, который прокручивается автоматически.

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    heightDictionary[indexPath.row] = cell.frame.size.height
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...