如何使用 ForEach 中制作的 TextFields 更新结构的属性

How to update attributes of a struct with TextFields made in ForEach

在 SwiftUI 中,我有一个菜单项列表,每个菜单项都有一个名称、价格等。有很多类别,每个类别下面都有一个项目列表。

struct ItemList: Identifiable, Codable {
    var id: Int
    var name: String
    var picture: String
    var list: [Item]
    
    @State var newItemName: String
}

我一直在寻找一种方法来在每个类别中创建一个 TextField 以添加到其项目数组中。

通过 ForEach 循环制作 TextFields 非常简单,但我在尝试使用输入的文本将新项目添加到正确的类别时遇到了困难。

ForEach(menu.indices) { i in
    Section(header: Text(menu[i].name)) {
        ForEach(menu[i].list) { item in
            Text(item.name)
        }
        TextField("New Type:", text: /*some kind of bindable here?*/) {
            menu[i].list.append(Item(name: /*the text entered above*/))
        }
    }
}

我考虑过使用 @Published 和 Observable 对象,比如 this other question,但我需要 ItemList 是一个 Codable 结构,所以我无法弄清楚如何将那里的答案适合我的情况。

TextField("New Type:", text: menu[i].$newItemName)

无论如何,任何想法将不胜感激,谢谢!

您可以将 ObservableObject 作为您的数据模型,存储类别,然后再存储项目。

然后您可以使用 Swift 5.5 语法绑定到这些项目。这意味着我们可以写 List($menu.categories) { $category in /* ... */ }。然后,当我们写 $category.newItem 时,我们在 Category.

中有一个 Binding<String>newItem 属性

示例:

struct ContentView: View {
    @StateObject private var menu = Menu(categories: [
        Category(name: "Milk Tea", items: [
            Item(name: "Classic Milk Tea"),
            Item(name: "Taro Milk Tea")
        ]),
        Category(name: "Tea", items: [
            Item(name: "Black Tea"),
            Item(name: "Green Tea")
        ]),
        Category(name: "Coffee", items: [
            Item(name: "Black Coffee")
        ])
    ])

    var body: some View {
        List($menu.categories) { $category in
            Section(header: Text(category.name)) {
                ForEach(category.items) { item in
                    Text(item.name)
                }

                TextField("New item", text: $category.newItem, onCommit: {
                    guard !category.newItem.isEmpty else { return }
                    category.items.append(Item(name: category.newItem))
                    category.newItem = ""
                })
            }
        }
    }
}
class Menu: ObservableObject {
    @Published var categories: [Category]

    init(categories: [Category]) {
        self.categories = categories
    }
}

struct Category: Identifiable {
    let id = UUID()
    let name: String
    var items: [Item]
    var newItem = ""
}

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

结果:

您只需要专注于 View

import SwiftUI

struct ExpandingMenuView: View {
    @State var menu: [ItemList] = [
        ItemList(name: "Milk Tea", picture: "", list: [ItemModel(name: "Classic Milk Tea"), ItemModel(name: "Taro milk tea")]),
        ItemList(name: "Tea", picture: "", list: [ItemModel(name: "Black Tea"), ItemModel(name: "Green tea")]),
        ItemList(name: "Coffee", picture: "", list: [])
        
    ]
    var body: some View {
        List{
            //This particular setup is for iOS15+
            ForEach($menu) { $itemList in
                ItemListView(itemList: $itemList)
            }
        }
    }
}

struct ItemListView: View {
    @Binding var itemList: ItemList
    @State var newItemName: String = ""
    var body: some View {
        Section(header: Text(itemList.name)) {
            ForEach(itemList.list) { item in
                Text(item.name)
            }
            TextField("New Type:", text: $newItemName, onCommit: {
                //When the user commits add to array and clear the new item variable
                itemList.list.append(ItemModel(name: newItemName))
                newItemName = ""
            })
        }
    }
}
struct ItemList: Identifiable, Codable {
    var id: UUID = UUID()
    var name: String
    var picture: String
    var list: [ItemModel]
    //@State is ONLY for SwiftUI Views
    //@State var newItemName: String
}
struct ItemModel: Identifiable, Codable {
    var id: UUID = UUID()
    var name: String
    
}
struct ExpandingMenuView_Previews: PreviewProvider {
    static var previews: some View {
        ExpandingMenuView()
    }
}

如果您不使用 Xcode 13 和 iOS 15+,那么在 SO 中有许多用于绑定数组元素的解决方案。以下只是其中之一

ForEach(menu) { itemList in
    let proxy = Binding(get: {itemList}, set: { new in
        let idx = menu.firstIndex(where: {
            [=11=].id == itemList.id
        })!
        menu[idx] = new
    })
    ItemListView(itemList: proxy)
}

另请注意,使用 indices 被认为是不安全的。您可以观看 WWDC2021 的 Demystifying SwiftUI 了解更多详情。