要销毁的视图主体被调用(但不应该)



在验证绑定如何(间接(使视图无效时,我发现了一个意外的行为。

  • 如果视图层次结构是

    list view -> detail view 
    

    在详细信息视图中按下一个按钮来删除项目是很好的(正如预期的那样(。

  • 但是,如果视图层次结构是

    list view -> detail view -> another detail view (containing the same item)
    

    当我按下最顶部细节视图中的按钮删除项目时,它会崩溃。崩溃发生在第一个局部视图(底层视图(中,因为调用了它的主体。

换句话说,行为是:

  • 如果详细信息视图是导航堆栈中最顶部的视图,则不会调用其主体。

  • 否则,它的身体会被召唤。

我想不出这种行为的任何原因。我的调试显示以下是崩溃前发生的情况:

  • 我在最上面的细节视图中按下了一个按钮来删除项目
  • 调用了ListView的主体(作为调用ContentView主体的结果(。它只为左侧项目创建了局部视图
  • 然后调用第一个DetailView的主体。这就是造成坠机的原因我想不出为什么会发生这种情况,因为它肯定不会发生在最上面的细节视图

下面是代码。注意,ListViewDetailView只包含绑定和正则属性(它们不包含可观察对象或环境对象,我知道这会使视图无效行为复杂化(。

import SwiftUI
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
@Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
}
struct ListView: View {
@Binding var foos: [Foo]
var body: some View {
NavigationView {
List {
ForEach(foos) { foo in
NavigationLink {
DetailView(foos: $foos, fooID: foo.id, label: "First detail view")
} label: {
Text("(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
@Binding var foos: [Foo]
var fooID: Int
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(fooID)
return VStack {
Text(label)
Divider()
Text("Value: (foos.get(fooID).value)")
NavigationLink {
DetailView(foos: $foos, fooID: fooID, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
foos.remove(fooID)
}
}
}
}
struct ContentView: View {
@StateObject var dataModel = DataModel()
var body: some View {
ListView(foos: $dataModel.foos)
}
}

测试1:启动应用程序,单击列表视图中的某个项目进入详细信息视图,然后单击"删除它"按钮这很好用。

视图层次结构:列表视图->详细视图

测试2:启动应用程序,单击列表视图中的项目进入详细信息视图,然后单击"创建另一个细节视图";转到另一个详细视图。然后点击";删除它"按钮将破坏第一个局部视图。

视图层次结构:列表视图->详细视图->另一个细节视图

它可能只是@Binding的另一个bug吗?有什么强有力的方法来解决这个问题吗?

您需要使用数据模型,而不是在视图中执行过程代码。此外,不要通过id传递项目;只需传递项目。

因为使用Fooid而不是Foo本身,并且在get函数中有一个强制展开,所以会发生崩溃。

如果你重构以使用你的模型而不使用id,它会按照你的意愿工作。

您并不真正需要您的数组扩展。作为通用对象的扩展的专业代码在我看来不太合适

删除代码非常简单,您可以在模型中处理它,并通过条件展开来安全地执行。

class DataModel: ObservableObject {
@Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]

func delete(foo: Foo) {
if let index = firstIndex(where: { $0.id == id }) {
self.foos.remove(at: index)
}
}
}
struct ListView: View {
@ObservedObject var model: DataModel
var body: some View {
NavigationView {
List {
ForEach(model.foos) { foo in
NavigationLink {
DetailView(model: model, foo: foo, label: "First detail view")
} label: {
Text("(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
@ObservedObject var model: DataModel
var foo: Foo
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo.id)
return VStack {
Text(label)
Divider()
Text("Value: (foo.value)")
NavigationLink {
DetailView(model: model, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
model.delete(foo:foo)
}
}
}
}

我认为这非常像Paul的方法。我只是像OP.中那样,用力打开数组扩展

struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
@Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2), Foo(id: 3, value: 3)]
}

struct ListView: View {
@EnvironmentObject var dataModel: DataModel
var body: some View {
NavigationView {
List {
ForEach(dataModel.foos) { foo in
NavigationLink {
DetailView(foo: foo, label: "First detail view")
} label: {
Text("(foo.value)")
}
}
}
}
}
}

struct DetailView: View {
@EnvironmentObject var dataModel: DataModel
var foo: Foo
var label: String

var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo.id)

return VStack {
Text(label)
Divider()
Text("Value: (foo.value)")
NavigationLink {
DetailView(foo: foo, label: "Yet Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
dataModel.foos.remove(foo.id)
}
}
}
}

struct ContentView: View {
@StateObject var dataModel = DataModel()

var body: some View {
ListView()
.environmentObject(dataModel)
}
}

这是一个工作版本。最好传递模型,这样您就可以使用数组下标进行变异。

我还将您的id更改为UUID,因为这是我习惯的,并更改了一些应该让的变量。

import SwiftUI
struct Foo: Identifiable {
//var id: Int
let id = UUID()
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
//extension Array where Element == Foo {
//    func get(_ id: Int) -> Foo {
//        return first(where: { $0.id == id })!
//    }
//
//    mutating func remove(_ id: Int) {
//        let index = firstIndex(where: { $0.id == id })!
//        remove(at: index)
//    }
//}
class DataModel: ObservableObject {
//@Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
@Published var foos: [Foo] = [Foo(value: 1), Foo(value: 2)]

func foo(id: UUID) -> Foo? {
foos.first(where: { $0.id == id })
}
}
struct ListView: View {
//@Binding var foos: [Foo]
@StateObject var dataModel = DataModel()
var body: some View {
NavigationView {
List {
//ForEach(foos) { foo in
ForEach(dataModel.foos) { foo in
NavigationLink {
//DetailView(foos: $foos, fooID: foo.id, label: "First detail view")
DetailView(dataModel: dataModel, foo: foo, label: "First detail view")
} label: {
Text("(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
//@Binding var foos: [Foo]
@ObservedObject var dataModel: DataModel
//var fooID: Int
let foo: Foo
let label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
//print(fooID)
print(foo.id)
return VStack {
Text(label)
Divider()
//Text("Value: (foos.get(fooID).value)")
if let foo = dataModel.foo(id:foo.id) {
Text("Value: (foo.value) ")
}
NavigationLink {
DetailView(dataModel: dataModel, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
//foos.remove(fooID)
if let index = dataModel.foos.firstIndex(where: { $0.id == foo.id } ) {
dataModel.foos.remove(at: index)
}
}
}
}
}
struct ContentView: View {
// no need for @ here because body doesn't need to update when model changes
//@StateObject var dataModel = DataModel()
var body: some View {
//ListView(foos: $dataModel.foos)
ListView()
}
}

这是一个使用Paul方法但仍然使用绑定的版本请注意,这两个版本都不是真正的";解决";这个问题(我在最初的问题中描述的行为仍然存在(,但取而代之的是";避免";在主体中渲染视图层次结构时未访问数据模型导致的崩溃我认为这是成功使用框架的关键点-不要与之斗争。

关于代码示例中绑定的使用,我知道大多数人使用ObservableObjectEnvironmentObject。我以前也这么做过。我注意到在苹果的演示应用程序中使用了绑定。但我可能会考虑回到视图模型方法。

import SwiftUI
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
@Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
}
struct ListView: View {
@Binding var foos: [Foo]
var body: some View {
NavigationView {
List {
ForEach(foos) { foo in
NavigationLink {
DetailView(foos: $foos, foo: foo, label: "First detail view")
} label: {
Text("(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
@Binding var foos: [Foo]
var foo: Foo
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo)
return VStack {
Text(label)
Divider()
Text("Value: (foo.value)")
NavigationLink {
DetailView(foos: $foos, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
foos.remove(foo.id)
}
}
}
}
struct ContentView: View {
@StateObject var dataModel = DataModel()
var body: some View {
ListView(foos: $dataModel.foos)
}
}

最新更新