【问题标题】:Simple SwiftUI CRUD using structs rather than classes?使用结构而不是类的简单 SwiftUI CRUD?
【发布时间】:2020-11-23 16:39:36
【问题描述】:

我有一个复杂的数据结构,它使用值类型(结构和枚举),我面临着让基本 CRUD 工作的主要问题。具体来说:

  1. 如何最好地“重新绑定” ForEach 中的值以供子视图编辑
  2. 如何移除/删除值

重新绑定

如果我有一组项目为@State@Binding,为什么没有一种简单的方法可以将每个元素绑定到视图?例如:

import SwiftUI

struct Item: Identifiable {
  var id = UUID()
  var name: String
}

struct ContentView: View {
  @State var items: [Item]
  var body: some View {
    VStack {
      ForEach(items, id: \.id) { item in
        TextField("name", text: $item) // ???? Cannot find '$item' in scope 
      }
    }
  }
}

解决方法

我已经能够通过引入一个辅助函数来解决这个问题,以便在循环中为项目找到正确的索引:

struct ContentView: View {
  @State var items: [Item]

  func index(of item: Item) -> Int {
    items.firstIndex { $0.id == item.id } ?? -1
  }

  var body: some View {
    VStack {
      ForEach(items, id: \.id) { item in
        TextField("name", text: $items[index(of: item)].name)
      }
    }
  }
}

但是,这感觉很笨重,而且可能很危险。

删除

一个更大的问题:你应该如何正确删除一个元素?这听起来像是一个基本问题,但请考虑以下问题:

struct ContentView: View {
  @State var items: [Item]

  func index(of item: Item) -> Int {
    items.firstIndex { $0.id == item.id } ?? -1
  }

  var body: some View {
    VStack {
      ForEach(items, id: \.id) { item in
        TextField("name", text: $items[index(of: item)].name)
        Button( action: {
          items.remove(at: index(of: item))
        }) {
          Text("Delete")
        }
      }
    }
  }
}

单击前几个项目上的“删除”按钮按预期工作,但尝试删除最后一个项目会导致 Fatal error: Index out of range...

我的特定用例没有映射到列表,所以我不能在那里使用删除助手。

引用类型

我知道引用类型使这一切变得更容易,特别是如果它们可以符合@ObservableObject。但是,我有一个庞大的、嵌套的、预先存在的值类型,它不容易转换为类。

任何帮助将不胜感激!

更新:建议的解决方案

  • Deleting List Elements from SwiftUI's list:接受的答案提出了一个复杂的自定义绑定包装器。 Swift 功能强大,因此可以通过精心设计的变通方法解决许多问题,但我认为不需要精心设计的变通方法来获得可编辑项目列表。
  • 使用状态或私有变量将视图标记为“已删除”,然后有条件地隐藏它们,以避免越界错误。这可以工作,但感觉就像是 hack,应该由框架处理。

【问题讨论】:

  • 这能回答你的问题吗? Deleting list elements from SwiftUI's List
  • @NewDev 该问题的公认答案是一种有趣的方法,但它相当复杂且“自定义”。我一直希望有一种更简洁或更第一方的方式来做到这一点。
  • 好吧,首先,您的“解决方法”中不需要额外的功能。链接的问题/答案为您提供了“第一方”方法:迭代索引并使用 $items[index] - 这是一个绑定。但问题仍然存在与删除 - 这似乎是一个错误,因为它已在 SwiftUI2 中解决
  • 我现在正在使用 SwiftUI 2,但仍然面临这个错误(删除时超出范围)。其他场景是否修复?
  • 嗯..确实,您的示例在 XCode12/iOS14 中仍然崩溃。但我对另一个问题的回答仍然有效——而且它是一个相当简单的包装器。

标签: swift foreach swiftui


【解决方案1】:

我确认更适合 CRUD 的方法是使用基于 ObservableObject 类的视图模型。 @NewDev 在 cmets 中提供的答案是该方法的一个很好的演示。

但是,如果您已经有一个庞大的、嵌套的、预先存在的值类型,并且不容易转换为类。,它可以通过@State/@Binding 解决,但您应该考虑what/when/and how 更新每个视图并以每个顺序 - 这是所有此类 index out of bounds on delete 问题(以及更多问题)的起源。

这里是如何打破这种更新依赖以避免崩溃并仍然使用值类型的方法的演示。

基于您的代码使用 Xcode 11.4 / iOS 13.4 (SwiftUI 1.0+) 进行测试

struct ContentView: View {
  @State var items: [Item] = [Item(name: "Name1"), Item(name: "Name2"), Item(name: "Name3")]

  func index(of item: Item) -> Int {
    items.firstIndex { $0.id == item.id } ?? -1
  }

  var body: some View {
    VStack {
      ForEach(items, id: \.id) { item in
        // separate dependent views as much as possible to make them as 
        // smaller/lighter as possible
        ItemRowView(items: self.$items, index: self.index(of: item))
      }
    }
  }
}

struct ItemRowView: View {
    @Binding var items: [Item]
    let index: Int

    @State private var destroyed = false   // internal state to validate self

    var body: some View {
        // proxy binding to have possibility for validation
        let binding = Binding(
            get: { self.destroyed ? "" : self.items[self.index].name },
            set: { self.items[self.index].name = $0 }
        )

        return HStack {
            if !destroyed { // safety check against extra update
                TextField("name", text: binding)
                Button( action: {
                  self.destroyed = true
                  self.$items.wrappedValue.remove(at: self.index)
                }) {
                  Text("Delete")
                }
            }
        }
    }
}

是的,这并不容易解决,但有时我们需要它。

【讨论】:

  • 嗯.. 我的意思不是使用 ObservableObject 代替值类型。 OP 正在使用变通方法来获取索引(您也包含它 - 为什么?),而典型的方法是迭代索引并通过 $items[index] 访问绑定。但是根据链接的问题,删除会导致问题(我的回答引入了一种更灵活的方法来防止索引超出范围)
  • @Asperi,我喜欢手动创建绑定的想法——至少它在调用站点上很清楚发生了什么。但是必须在视图上维护“已销毁”或“isDeleted”属性感觉是错误的。也许在 SwiftUI 中没有令人满意的方式来充分利用值类型......
  • @mbxDev,在这种情况下destroyed只是SwiftUI bug(错误更新顺序)的一种解决方法,但它是小视图的内部私有状态,所以它没有那么大的邪恶。跨度>
  • @Asperi,我认为你是对的......我已将其归档为 FB8254559。如果他们提供任何更新,我一定会重新审视这个问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多