为什么这个 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!)
}
这是“主题”结构项的简单列表视图。目标是在列表的一行被点击时呈现一个编辑器视图。在此代码中,点击一行将导致所选主题在 @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!)
}