从 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 的库(我不是这个的作者)。
该库用于实现RefreshUI
also 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的主要内容,如果未能解决你的问题,请参考以下文章
为啥从 Realm DB 加载图像时 SwiftUI List 会崩溃? RAM 达到极限