为什么这个 SwiftUI 列表需要额外的 objectWillChange.send?

Why does this SwiftUI List require an extra objectWillChange.send?

这是“主题”结构项的简单列表视图。目标是在列表的一行被点击时呈现一个编辑器视图。在此代码中,点击一行将导致所选主题在 @State var 中存储为“tappedTopic”,并设置一个布尔值 @State var 以显示 EditorV。

当显示的代码为 运行 并点击一行时,其主题名称在 Button 操作的 Print 语句中正确打印,但随后应用程序崩溃,因为 self.tappedTopic!在 EditTopicV(...) 行中发现 tappedTopic 为 nil。

如果行“tlVM.objectWillChange.send()”未被注释,则代码 运行 没问题。为什么需要这个?

还有第二个谜题:在代码 运行 没有注释的情况下,objectWillChange.send() 未注释,EditTopicV init() 中的打印语句显示它 运行s 两次。为什么?

如有任何帮助,我们将不胜感激。我正在使用 Xcode 13.2.1,我的部署目标设置为 iOS 15.1.

Topic.swift:

struct Topic: Identifiable {
    var name: String = "Default"
    var iconName: String = "circle"
    var id: String { name }
}

TopicListV.swift:

struct TopicListV: View {
    @ObservedObject var tlVM: TopicListVM

    @State var tappedTopic: Topic? = nil
    @State var doEditTappedTopic = false
    
    var body: some View {
        VStack(alignment: .leading) {
            List {
                ForEach(tlVM.topics) { topic in
                    Button(action: {
                        tappedTopic = topic
                        
                        // why is the following line needed?
                        tlVM.objectWillChange.send()
                        
                        doEditTappedTopic = true
                        print("Tapped topic = \(tappedTopic!.name)")
                    }) {
                        Label(topic.name, systemImage: topic.iconName)
                            .padding(10)
                    }
                }
            }
            
            Spacer()
        }
        .sheet(isPresented: $doEditTappedTopic) {
            EditTopicV(tlVM: tlVM, originalTopic: self.tappedTopic!)
        }
    }
}

EditTopicV.swift(编辑器视图):

struct EditTopicV: View {
    @ObservedObject var tlVM: TopicListVM
    @Environment(\.presentationMode) var presentationMode
    let originalTopic: Topic
    
    @State private var editTopic: Topic
    @State private var ic = "circle"
    let iconList = ["circle", "leaf", "photo"]
    
    init(tlVM: TopicListVM, originalTopic: Topic) {
        print("DBG: EditTopicV: originalTopic = \(originalTopic)")
        self.tlVM = tlVM
        self.originalTopic = originalTopic
        self._editTopic = .init(initialValue: originalTopic)
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Button("Cancel") {
                    presentationMode.wrappedValue.dismiss()
                }
                Spacer()
                Button("Save") {
                    editTopic.iconName = editTopic.iconName.lowercased()
                    tlVM.change(topic: originalTopic, to: editTopic)
                    presentationMode.wrappedValue.dismiss()
                }
            }
            
            HStack {
                Text("Name:")
                TextField("name", text: $editTopic.name)
                Spacer()
            }
            Picker("Color Theme", selection: $editTopic.iconName) {
                ForEach(iconList, id: \.self) { icon in
                    Text(icon).tag(icon)
                }
            }
            .pickerStyle(.segmented)

            Spacer()
        }
        .padding()
    }
}

TopicListVM.swift(可观察对象视图模型):

class TopicListVM: ObservableObject {
    @Published var topics = [Topic]()
    
    func append(topic: Topic) {
        topics.append(topic)
    }
    
    func change(topic: Topic, to newTopic: Topic) {
        if let index = topics.firstIndex(where: { [=14=].name == topic.name }) {
            topics[index] = newTopic
        }
    }
    
    static func ex1() -> TopicListVM {
        let tvm = TopicListVM()
        tvm.append(topic: Topic(name: "leaves", iconName: "leaf"))
        tvm.append(topic: Topic(name: "photos", iconName: "photo"))
        tvm.append(topic: Topic(name: "shapes", iconName: "circle"))
        return tvm
    }

}

列表如下所示:

使用 sheet(isPresented:) 往往会导致这样的问题,因为 SwiftUI 计算目标视图的顺序似乎并不总是有意义。在你的情况下,在视图模型上使用 objectWillSend,即使它 不应该 有任何影响,似乎延迟了你的强制展开变量的计算并避免了崩溃.

要解决此问题,请使用 sheet(item:) 形式:

.sheet(item: $tappedTopic) { item in
    EditTopicV(tlVM: tlVM, originalTopic: item)
}

然后,您的 item 会安全地通过闭包,没有理由强制解包。

您也可以捕获 tappedTopic 以获得类似的结果,但您仍然必须强制展开它,这通常是我们要避免的事情:

.sheet(isPresented: $doEditTappedTopic) { [tappedTopic] in
    EditTopicV(tlVM: tlVM, originalTopic: tappedTopic!)
}