我在 SwiftUI 方面还是个初学者。我偶尔尝试过,但大部分时间我真的不知道自己在做什么。最近,我有机会再试一次。对于这个特定的实验,我想将一个现有的视图封装起来,以便我可以从 SwiftUI 访问它。这部分是相当标准的东西。然而,我遇到了一个有趣的问题,我敢打赌我不是唯一一个。
当我学习一个新的 API 时,我喜欢从一个新的项目开始。这可以帮助我专注于任务,避免与更大项目集成时的复杂性。我的目标是将 NSTableView
包装起来,并将其嵌入到 SwiftUI 的视图层次结构中。所以,我创建了一个新项目,并根据苹果的 SwiftUI 教程 Interfacing with UIKit 进行操作,按照需要将其适配为 AppKit 和我的问题。
我感觉挺不错的,因为我很快就实现了基本功能。这个实现遵循了 View
-NSView
-Coordinator
的模式,结构在仔细研究之后很容易理解。
以下是我编写的代码:
struct EventsTableView: NSViewRepresentable {
var events: [String]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> NSScrollView {
let column = NSTableColumn(identifier: .exampleColumn)
column.width = 100.0
let tableView = NSTableView()
tableView.headerView = nil
tableView.addTableColumn(column)
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
let view = NSScrollView()
view.documentView = tableView
return view
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
let tableView = nsView.documentView as! NSTableView
tableView.reloadData()
}
}
extension EventsTableView {
final class Coordinator: NSObject, NSTableViewDataSource, NSTableViewDelegate {
var parent: EventsTableView
init(_ parent: EventsTableView) {
self.parent = parent
}
func numberOfRows(in tableView: NSTableView) -> Int {
return parent.events.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let reusedView = tableView.makeView(withIdentifier: .exampleColumn, owner: self)
let view = reusedView as? NSTextField ?? NSTextField(labelWithString: "")
view.stringValue = parent.events[row]
return view
}
}
}
我之前提到一切都在正常运行。但是,当我进一步实验时,很快发现即使我在更新时重新加载表视图,其内容也始终没有变化。
当然,事后看来,问题显而易见。你可能已经注意到了。如果没有发现,也别感到糟糕,因为我花了不少时间调试和思考,才完全明白问题出在哪里。
SwiftUI 的互操作性围绕着一个包装视图 EventsTableView
及其协调器 Coordinator
展开。这个协调器实例会在视图需要更改时作为上下文传递回去。包装视图和协调器之间的这种关系至关重要,理解它们的生命周期关系也同样重要。
SwiftUI 的视图在正常执行期间会非常频繁地被重新创建,这很正常。但你的协调器实例只会创建一次,并尽可能地被重用。这其实是非常标准的值类型/引用类型的行为。当协调器将包装视图作为初始化参数时,它会拷贝那个视图的第一次实例化的版本。而当 SwiftUI 包装视图被重新创建时,这个拷贝并不会更新。
我的问题在于,Coordinator
创建时,父级属性及其相关事件只被复制了一次,然后就再也没有更新过。当我意识到这一点时,我觉得自己有点蠢。视图是值类型,当然它不会更新!但我也不想对自己太苛刻,因为这种协调器捕获父级视图的模式非常普遍。许多关于这一主题的博客文章中都存在这种模式。包括苹果的(表面上是权威的)教程,都使用了这种父级模式。我猜这就是问题的根源。
在我的案例中,解决方案其实很简单。关键是要在协调器实例中捕获并更新数据。
struct EventsTableView: NSViewRepresentable {
var events: [String]
func makeCoordinator() -> Coordinator {
Coordinator(events: events) // <- 这里!
}
// 省略部分代码...
func updateNSView(_ nsView: NSScrollView, context: Context) {
context.coordinator.events = events // <- 这里也是!
let tableView = nsView.documentView as! NSTableView
tableView.reloadData()
}
}
当我完全意识到协调器实例是长期存在的时,捕获父视图就毫无意义了。但是,它确实让我误以为我有一个引用类型的关系,而这种关系是可行的。如果这个属性被称为 firstParent
或者 creatingView
,也许我就不会犯这个错误了。
经历了这一切后,我不确定是否能想到任何一个你需要捕获 Coordinator
创建视图副本的理由。如果你知道这种父视图-协调器模式可能有用的原因,请告诉我。不过即便有一些合适的场景,我仍然认为这种模式太容易让人困惑,就像我一样,因此最好还是避免使用。
👉 https://www.massicotte.org/swiftui-coordinator-parent
🔗 查看链接 • 投稿/推荐/自荐 • Quick RSS • #41 • @jaywcjlove