从 SwiftUI 列表访问底层 UITableView

Access underlying UITableView from SwiftUI List

使用 List 视图,有没有一种方法可以访问(并因此修改)底层 UITableView 对象而无需将整个 List 重新实现为 UIViewRepresentable

我已经尝试在我自己的 UIViewRepresentable 中初始化一个 List,但我似乎无法让 SwiftUI 在我需要时初始化视图,我只是得到一个空的没有子视图的基本 UIView

这个问题是为了帮助找到 的答案。


或者,在 SwiftUI 中重新实现 UITableView 的库或其他项目也会回答这个问题。

答案是否定的。截至iOS13,SwiftUI 的 List 目前并未设计为取代 UITableView 的所有功能和可定制性。它旨在满足 UITableView 的最基本用途:一个标准外观、可滚动、可编辑的列表,您可以在其中的每个单元格中放置一个相对简单的视图。

换句话说,您放弃了可定制性,因为轻扫、导航、移动、删除等功能会自动为您实现。

我确信随着 SwiftUI 的发展,List(或等效视图)将变得更加可定制,我们将能够执行诸如从底部滚动、更改填充等操作。最好的方法确保发生这种情况是 file feedback suggestions with Apple。我确信 SwiftUI 工程师已经在努力设计将在 WWDC 2020 上出现的功能。他们为指导社区的需求提供的意见越多越好。

目前无法访问或修改底层 UITableView

我找到了一个名为 Rotoscope on GitHub 的库(我不是它的作者)。

这个库被同一作者用来实现 RefreshUI also on GitHub

它的工作原理是 Rotoscope 有一个 tagging 方法,它在您的 List 之上覆盖一个 0 大小的 UIViewRepresentable(因此它是不可见的)。该视图将挖掘视图链并最终找到托管 SwiftUI 视图的 UIHostingView。然后,它会 return 托管视图的第一个子视图,其中应该包含 UITableView 的包装器,然后您可以通过获取包装器的子视图来访问 table 视图对象。

RefreshUI 库使用此库实现对 SwiftUI 的刷新控制 List(您可以进入 GitHub link 并查看源代码以看看它是如何实现的)。

但是,我认为这更像是一种 hack,而不是实际的方法,因此由您决定是否要使用它。无法保证它会在主要更新之间继续工作,因为 Apple 可能会更改内部视图布局并且此库会中断。

答案是肯定的。有一个很棒的库可以让你检查底层的 UIKit 视图。这是一个link

你可以的。但它需要一个黑客。

  1. 添加任何自定义 UIView
  2. 使用 UIResponder 回溯,直到找到 table 视图。
  3. 按照您喜欢的方式修改 UITableView。

添加拉取刷新的代码示例:

//1: create a custom view 
final class UIKitView : UIViewRepresentable {
    let callback: (UITableView) -> Void

    init(leafViewCB: @escaping ((UITableView) -> Void)) {
        callback = leafViewCB
    }

    func makeUIView(context: Context) -> UIView  {
        let view = UIView.init(frame: CGRect(x: CGFloat.leastNormalMagnitude,
                                             y: CGFloat.leastNormalMagnitude,
                                             width: CGFloat.leastNormalMagnitude,
                                             height: CGFloat.leastNormalMagnitude))
        view.backgroundColor = .clear
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        if let superView = uiView.superview {
            superView.backgroundColor = uiView.backgroundColor
        }

        if let tableView = uiView.next(UITableView.self) {
            callback(tableView)
        }
    }
}

extension UIResponder {
    func next<T: UIResponder>(_ type: T.Type) -> T? {
        return next as? T ?? next?.next(type)
    }
}

////Use:

struct Result: Identifiable {
    var id = UUID()
    var value: String
}


class RefreshableObject: ObservableObject {
    let id = UUID()
    @Published var items: [Result] = [Result(value: "Binding"),
                                      Result(value: "ObservableObject"),
                                      Result(value: "Published")]

    let refreshControl: UIRefreshControl

    init() {
        refreshControl = UIRefreshControl()
        refreshControl.addTarget(self, action:
            #selector(self.handleRefreshControl),
                                      for: .valueChanged)
    }

    @objc func handleRefreshControl(sender: UIRefreshControl) {
           DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {  [weak self] in
                sender.endRefreshing()
                self?.items = [Result(value:"new"), Result(value:"data"), Result(value:"after"), Result(value:"refresh")]
           }
    }
}



struct ContentView: View {

    @ObservedObject var refreshableObject = RefreshableObject()
    var body: some View {
            NavigationView {
                Form {
                    Section(footer: UIKitView.init { (tableView) in
                        if tableView.refreshControl == nil  {
                           tableView.refreshControl = self.refreshableObject.refreshControl
                        }
                    }){
                        ForEach(refreshableObject.items) { result in
                            Text(result.value)
                        }
                    }

                }
                    .navigationBarTitle("Nav bar")

            }
    }
}

截图:

要从刷新操作更新,正在使用绑定 isUpdateOrdered

此代码是我在网上找到的代码,找不到作者


import Foundation
import SwiftUI

class Model: ObservableObject{
    @Published var isUpdateOrdered = false{
        didSet{
            if isUpdateOrdered{
                update()
                isUpdateOrdered = false
                print("we got him!")
            }
        }
    }
    var random = 0
    @Published var arr = [Int]()
    
    func update(){
        isUpdateOrdered = false
        //your update code.... maybe some fetch request or POST?
    }
}

struct ContentView: View {
    @ObservedObject var model = Model()
    var body: some View {
        NavigationView {
            LegacyScrollViewWithRefresh(isUpdateOrdered: $model.isUpdateOrdered) {
                VStack{
                    if model.arr.isEmpty{ 
//this is important to fill the
//scrollView with invisible data,
//in other case scroll won't work
//because of the constraints.
//You may get rid of them if you like. 
                        Text("refresh!")
                        ForEach(1..<100){ _ in
                            Text("")
                        }
                    }else{
                        ForEach(model.arr, id:\.self){ i in
                            NavigationLink(destination: Text(String(i)), label: { Text("Click me") })
                        }
                    }
                    
                }
                
            }.environmentObject(model)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


struct LegacyScrollViewWithRefresh: UIViewRepresentable {
    enum Action {
        case idle
        case offset(x: CGFloat, y: CGFloat, animated: Bool)
    }
    typealias Context = UIViewRepresentableContext<Self>
    @Binding var action: Action
    @Binding var isUpdateOrdered: Bool
    private let uiScrollView: UIScrollView
    private var uiRefreshControl = UIRefreshControl()
    
    init<Content: View>(isUpdateOrdered: Binding<Bool>, content: Content) {
        let hosting = UIHostingController(rootView: content)
        hosting.view.translatesAutoresizingMaskIntoConstraints = false
        self._isUpdateOrdered = isUpdateOrdered
        uiScrollView = UIScrollView()
        
        uiScrollView.addSubview(hosting.view)
        
        let constraints = [
            hosting.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
            hosting.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
            hosting.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor),
            hosting.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor),
            hosting.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
        ]
        uiScrollView.addConstraints(constraints)
        
        self._action = Binding.constant(Action.idle)
    }
    
    init<Content: View>(isUpdateOrdered: Binding<Bool>, @ViewBuilder content: () -> Content) {
        self.init(isUpdateOrdered: isUpdateOrdered, content: content())
    }
    
    init<Content: View>(isUpdateOrdered: Binding<Bool>, action: Binding<Action>, @ViewBuilder content: () -> Content) {
        self.init(isUpdateOrdered: isUpdateOrdered, content: content())
        self._action = action
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> UIScrollView {
        
        
        uiScrollView.addSubview(uiRefreshControl)
        uiRefreshControl.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl(arguments:)), for: .valueChanged)
        return uiScrollView
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) {
        switch self.action {
            case .offset(let x, let y, let animated):
                uiView.setContentOffset(CGPoint(x: x, y: y), animated: animated)
                DispatchQueue.main.async {
                    self.action = .idle
                }
            default:
                break
        }
    }
    
    
    class Coordinator: NSObject {
        let legacyScrollView: LegacyScrollViewWithRefresh
        
        init(_ legacyScrollView: LegacyScrollViewWithRefresh) {
            self.legacyScrollView = legacyScrollView
        }
        @objc func handleRefreshControl(arguments: UIRefreshControl){
            print("refreshing")
            self.legacyScrollView.isUpdateOrdered = true
            DispatchQueue.main.asyncAfter(deadline: .now() + 2){
                arguments.endRefreshing()
//refresh animation will
//always be shown for 2 seconds,
//you may connect this behaviour
//to your update completion
            }
        }
    }
    
}