SwiftUI 中的 @Binding 和 ForEach
Posted
技术标签:
【中文标题】SwiftUI 中的 @Binding 和 ForEach【英文标题】:@Binding and ForEach in SwiftUI 【发布时间】:2019-12-11 21:55:40 【问题描述】:我无法理解如何在 SwiftUI 中将 @Binding
与 ForEach
结合使用。假设我想从一个布尔数组中创建一个 Toggle
s 列表。
struct ContentView: View
@State private var boolArr = [false, false, true, true, false]
var body: some View
List
ForEach(boolArr, id: \.self) boolVal in
Toggle(isOn: $boolVal)
Text("Is \(boolVal ? "On":"Off")")
我不知道如何将数组内的布尔值的绑定传递给每个Toggle
。上面的代码给出了这个错误:
使用未解析的标识符“$boolVal”
好吧,这对我来说很好(当然)。我试过了:
struct ContentView: View
@State private var boolArr = [false, false, true, true, false]
var body: some View
List
ForEach($boolArr, id: \.self) boolVal in
Toggle(isOn: boolVal)
Text("Is \(boolVal ? "On":"Off")")
这次的错误是:
在 'ForEach' 上引用初始化程序 'init(_:id:content:)' 需要 'Binding' 符合 'Hashable'
有没有办法解决这个问题?
【问题讨论】:
【参考方案1】:您可以使用类似下面的代码。请注意,您将收到一条已弃用的警告,但要解决此问题,请查看其他答案:https://***.com/a/57333200/7786555
import SwiftUI
struct ContentView: View
@State private var boolArr = [false, false, true, true, false]
var body: some View
List
ForEach(boolArr.indices) idx in
Toggle(isOn: self.$boolArr[idx])
Text("boolVar = \(self.boolArr[idx] ? "ON":"OFF")")
【讨论】:
我会接受你的回答,我真的很喜欢简单,但我决定接受无警告的回答。我很想知道苹果将如何处理下标(_:) 问题。我们很可能能够在未来的测试版中改进这个问题的答案。 @superpuccio 如链接答案中所述,Xcode 的 beta 6 修复了警告。 对使用索引的更改也将 ForEach 从显示动态内容更改为静态内容。如果你想从被迭代的列表中删除一个元素,你可能会得到一个错误。文档:developer.apple.com/documentation/swiftui/foreach/3364099-init 扩展 @EmmaKAlexandra 所说的内容 - 如果您正在执行删除操作,您的应用将因索引越界错误而崩溃。 不要使用索引。错误的代码。 Swift 和 SwiftUI 文档中都有关于此的危险信号。索引会产生错误。 Swift 和 SwiftUI 提供了多种其他方式来获取数组成员。谁给了这个绿色复选标记?!【参考方案2】:⛔️ 不要使用坏习惯!
大多数答案(包括@kontiki 接受的答案)方法会导致引擎在每次更改时重新呈现整个 UI,Apple 在 wwdc2021(大约 7:40)提到这是一种不好的做法
✅斯威夫特 5.5
从这个版本的 swift 中,您可以通过传入可绑定项直接使用绑定数组元素,例如:
⚠️ 注意 ios 14 及更低版本不支持 Swift 5.5 但至少检查操作系统版本,不要继续不良做法!
【讨论】:
我明白,但至少检查操作系统,不要继续这种不良做法@paulz 实际上,这适用于 iOS 14.0 及更高版本;刚刚使用 Xcode 13 beta 3 进行了测试。并非所有 Swift 5.5 功能都需要 iOS 15。 在这种情况下如何使用 .onDelete? 我无法让它工作。我从 $directions 得到Cannot declare entity named '$direction'; the '$' prefix is reserved for implicitly-synthesized declarations
和 Initializer 'init(_:rowContent:)' requires that 'Binding<[String]>' conform to 'RandomAccessCollection'
【参考方案3】:
Swift 5.5 更新
struct ContentView: View
struct BoolItem: Identifiable
let id = UUID()
var value: Bool = false
@State private var boolArr = [BoolItem(), BoolItem(), BoolItem(value: true), BoolItem(value: true), BoolItem()]
var body: some View
NavigationView
VStack
List($boolArr) $bi in
Toggle(isOn: $bi.value)
Text(bi.id.description.prefix(5))
.badge(bi.value ? "ON":"OFF")
Text(boolArr.map(\.value).description)
.navigationBarItems(leading:
Button(action: self.boolArr.append(BoolItem(value: .random())) )
Text("Add")
, trailing:
Button(action: self.boolArr.removeAll() )
Text("Remove All") )
以前的版本,允许更改Toggle
s 的数量(不仅仅是它们的值)。
struct ContentView: View
@State var boolArr = [false, false, true, true, false]
var body: some View
NavigationView
// id: \.self is obligatory if you need to insert
List(boolArr.indices, id: \.self) idx in
Toggle(isOn: self.$boolArr[idx])
Text(self.boolArr[idx] ? "ON":"OFF")
.navigationBarItems(leading:
Button(action: self.boolArr.append(true) )
Text("Add")
, trailing:
Button(action: self.boolArr.removeAll() )
Text("Remove All") )
【讨论】:
我们可以有类似的东西,但使用 ForEach 而不是列表? 当然可以,@GrandSteph,为什么不呢?VStack ForEach(boolArr.indices, id: \.self) idx in Toggle(isOn: self.$boolArr[idx]) Text(self.boolArr[idx] ? "ON":"OFF") .padding()
。或者HStack
,或者`Group,
Section, etc.
List`只有这个简洁的初始化(包括ForEach
),因为它是一个非常常见的事情......列出项目。
感谢@Paul,但我正在努力处理 ForEach 和动态数组列表。我似乎无法满足以下 3 个条件:1 - 动态数组(索引是静态的,删除元素会崩溃) 2 - 使用绑定,以便我可以修改子视图中的每个元素 3 - 不使用列表(我想要定制设计,没有标题等...)
删除时崩溃是一些人在处理 SwiftUI 时遇到的问题。在某些情况下,使用预计值会有所帮助:func delete(at offsets: IndexSet) $store.wrappedValue.data.remove(atOffsets: offsets) // instead of store.data.remove()
。在其他情况下,使用 Binding()
init(不是 @Binding 指令)创建 var 是此类问题的最佳解决方案。但是这个问题太笼统了,@GrandSteph。如果在 SO 正确地制定它,您可能会得到更好的答案。您还可以在此处查看数组驱动接口的一些变体:***.com/a/59739983【参考方案4】:
在 SwiftUI 中,只需使用 Identifiable 结构而不是 Bools
struct ContentView: View
@State private var boolArr = [BoolSelect(isSelected: true), BoolSelect(isSelected: false), BoolSelect(isSelected: true)]
var body: some View
List
ForEach(boolArr.indices) index in
Toggle(isOn: self.$boolArr[index].isSelected)
Text(self.boolArr[index].isSelected ? "ON":"OFF")
struct BoolSelect: Identifiable
var id = UUID()
var isSelected: Bool
【讨论】:
感谢您的回答。我实际上并不喜欢创建一个结构来包装一个简单的 bool 值(因为我可以使用 \.self 来识别 bool 本身),但是您的答案是无警告的,所以目前可能是正确的答案(让我们密切关注 Apple 我们将如何处理下标(_:) 问题) ps:“Hashable”实际上在 BoolSelect 定义中是多余的。 现在好像坏了。上面的代码在 XCode 11 beta 2 中给出了“表达式类型在没有上下文的情况下不明确”。 但在我看来,最新的 Xcode 无法处理“ForEach($boolArr)”之类的问题。我还收到“type of expression is ambiguous without more context
”错误。当在 ForEach 循环中使用自定义视图并将循环变量作为 @Binding
传递时,编译器发现另一个问题“无法使用类型为 '@987654324 的参数列表调用类型为 'ForEach<_ _>' 的初始化程序” @'"
谢谢!我一直试图让这样的东西工作几个小时。对我来说,关键是使用“索引”作为循环遍历的数组。
如果我们在@State
结构中有一个Identifiable
数组并且需要对其进行迭代,则将无法工作。如果我们打算插入或删除数组元素,无论如何都必须使用List(model.boolArr.indices, id: \.self)
。【参考方案5】:
在 WWDC21 视频中,Apple 明确指出在 ForEach
循环中使用 .indices
是一种不好的做法。除此之外,我们需要一种方法来唯一标识数组中的每一项,所以你不能使用ForEach(boolArr, id:\.self)
,因为数组中有重复的值。
正如@Mojtaba Hosseini 所说,Swift 5.5 的新手现在可以使用绑定数组元素直接传递可绑定项。但是如果你仍然需要使用以前版本的 Swift,我是这样实现的:
struct ContentView: View
@State private var boolArr: [BoolItem] = [.init(false), .init(false), .init(true), .init(true), .init(false)]
var body: some View
List
ForEach(boolArr) boolItem in
makeBoolItemBinding(boolItem).map
Toggle(isOn: $0.value)
Text("Is \(boolItem.value ? "On":"Off")")
struct BoolItem: Identifiable
let id = UUID()
var value: Bool
init(_ value: Bool)
self.value = value
func makeBoolItemBinding(_ item: BoolItem) -> Binding<BoolItem>?
guard let index = boolArr.firstIndex(where: $0.id == item.id ) else return nil
return .init(get: self.boolArr[index] ,
set: self.boolArr[index] = $0 )
首先,我们通过创建一个符合Identifiable
的简单结构来使数组中的每个项目都可识别。然后我们创建一个函数来创建自定义绑定。我本可以使用强制展开来避免从 makeBoolItemBinding
函数返回一个可选项,但我总是尽量避免它。从函数返回一个可选绑定需要 map 方法来解包它。
我已经在我的项目中测试过这种方法,到目前为止它运行良好。
【讨论】:
以上是关于SwiftUI 中的 @Binding 和 ForEach的主要内容,如果未能解决你的问题,请参考以下文章
从 @Binding var 修改 @State var 不会刷新 SwiftUI 中的视图