从 SwiftUI List 访问底层 UITableView

Posted

技术标签:

【中文标题】从 SwiftUI List 访问底层 UITableView【英文标题】:Access underlying UITableView from SwiftUI List 【发布时间】:2019-09-26 13:18:13 【问题描述】:

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

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

这个问题是为了帮助找到Bottom-first scrolling in SwiftUI 的答案。


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

【问题讨论】:

如您所见,SwiftUI 还不适合。制作一些演示屏幕很好,但要构建任何接近功能齐全的应用程序,现在,您需要继续使用 UIKit。 【参考方案1】:

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

【讨论】:

【参考方案2】:

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

换句话说,您放弃了可定制性,而是为您自动实现滑动、导航、移动、删除等操作的简单性。

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

【讨论】:

我已经提交了请求此功能的反馈,或者添加了一个文档化的方式来实现可以直接修改 UITableView 的自定义 ListStyles。 我认为这个答案不正确。或者标记时是正确的。您可以从 SwiftUI 视图轻松访问底层 UIKit 组件。例如,SwiftUI List 是建立在 UITableView 之上的。有一个 Introspect 库可以向您展示如何访问和自定义底层 UIKit 视图。在我的回答中找到链接***.com/a/61054760/3849039【参考方案3】:

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

该库用于实现RefreshUIalso on GitHub同一作者。

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

RefreshUI 库使用这个库来实现对 SwiftUI List 的刷新控制(您可以进入 GitHub 链接并查看源代码以了解它是如何实现的)。

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

【讨论】:

【参考方案4】:

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

    添加任何自定义 UIView 使用 UIResponder 回溯,直到找到 table View。 按你喜欢的方式修改 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")

            
    


截图:

【讨论】:

【参考方案5】:

要从刷新操作更新,正在使用绑定 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
            
        
    
    

【讨论】:

【参考方案6】:

目前没有办法访问或修改底层的UITableView

【讨论】:

可以修改。只需在 SwiftUI ContentView 结构体的 init 方法中自定义 UITableView 即可。访问是可能的。有两个库已经这样做了:github.com/noppefoxwolf/Rotoscope 和 github.com/siteline/SwiftUI-Introspect

以上是关于从 SwiftUI List 访问底层 UITableView的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 隐藏/取消List的分割线

索引超出范围 SwiftUI

为啥从 Realm DB 加载图像时 SwiftUI List 会崩溃? RAM 达到极限

Swiftui - 从 UIViewRepresentable 访问 UIKit 方法/属性

无法从 SwiftUI 框架中的资产目录访问的图像

SwiftUI不借助ScrollViewReader和ScrollViewProxy实现List自动滚动到底部