SwiftUI 状态变量数组不更新子视图

时间:2021-02-18 03:27:40

标签: swiftui swiftui-state

出于某种原因,我不明白,当我从 @State var 中的 MainView 添加/删除项目时,OutterView 未正确更新。

我想要实现的是,用户一次只能“标记”(选择)一项。例如,当我点击“item #1”时,它会被标记。如果我单击另一个项目,则不会再标记“项目 #1”,而只会标记我刚刚单击的新项目。

enter image description here

目前,我的代码显示所有项目,就好像它们被标记了一样,即使它们不再是。以下代码具有我为 MainViewOutterViewInnerView 实现的最低结构和功能。

我曾尝试在 State var 中使用 OutterView 代替计算属性,但它不起作用。此外,我尝试在 var 中使用 OutterView 代替计算属性并在 init() 中对其进行初始化,但也不起作用。

希望你能帮我找出我做错了什么。 谢谢!

struct MainView: View {
    @State var flagged: [String] = []
    
    var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
    
    var body: some View {
        VStack(spacing: 50) {
            VStack {
                ForEach(data, id:\.self) { text in
                    OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
                        if flag {
                            flagged = [text]
                        } else {
                            if let index = flagged.firstIndex(of: text) {
                                flagged.remove(at: index)
                            }
                        }
                    }
                }
            }
            
            Text("Flagged: \(flagged.description)")
            
            Button(action: {
                flagged = []
            }, label: {
                Text("Reset flagged")
            })
        }
    }
}

struct OutterView: View {
    @State private var flag: Bool
    
    private let text: String
    private var color: Color { flag ? Color.green : Color.gray }
    private var update: (Bool)->Void
    
    var body: some View {
        InnerView(color: color, text: text)
            .onTapGesture {
                flag.toggle()
                update(flag)
            }
    }
    
    init(text: String, flag: Bool = false, update: @escaping (Bool)->Void) {
        self.text = text
        self.update = update
        _flag = State(initialValue: flag)
    }
}

struct InnerView: View {
    let color: Color
    let text: String
    
    var body: some View {
        Text(text)
            .padding()
            .background(
                Capsule()
                    .fill(color))
    }
}

1 个答案:

答案 0 :(得分:0)

这是一个简单的版本,可以满足您的需求(解释如下):

struct Item : Identifiable {
    var id = UUID()
    var flagged = false
    var title : String
}

class StateManager : ObservableObject {
    @Published var items = [Item(title: "Item #1"),Item(title: "Item #2"),Item(title: "Item #3"),Item(title: "Item #4"),Item(title: "Item #5")]
    
    func singularBinding(forIndex index: Int) -> Binding<Bool> {
        Binding<Bool> { () -> Bool in
            self.items[index].flagged
        } set: { (newValue) in
            self.items = self.items.enumerated().map { itemIndex, item in
                var itemCopy = item
                if index == itemIndex {
                    itemCopy.flagged = newValue
                } else {
                    //not the same index
                    if newValue {
                        itemCopy.flagged = false
                    }
                }
                return itemCopy
            }
        }
    }
    
    func reset() {
        items = items.map { item in
            var itemCopy = item
            itemCopy.flagged = false
            return itemCopy
        }
    }
}

struct MainView: View {
    @ObservedObject var stateManager = StateManager()
    
    var body: some View {
        VStack(spacing: 50) {
            VStack {
                ForEach(Array(stateManager.items.enumerated()), id:\.1.id) { (index,item) in
                    OutterView(text: item.title, flag: stateManager.singularBinding(forIndex: index))
                }
            }
            
            Text("Flagged: \(stateManager.items.filter({ $0.flagged }).map({$0.title}).description)")
            
            Button(action: {
                stateManager.reset()
            }, label: {
                Text("Reset flagged")
            })
        }
    }
}

struct OutterView: View {
    var text: String
    @Binding  var flag: Bool
    private var color: Color { flag ? Color.green : Color.gray }
    
    var body: some View {
        InnerView(color: color, text: text)
            .onTapGesture {
                flag.toggle()
            }
    }
}

struct InnerView: View {
    let color: Color
    let text: String
    
    var body: some View {
        Text(text)
            .padding()
            .background(
                Capsule()
                    .fill(color))
    }
}

发生了什么:

  1. 有一个 Item,其中包含每个项目的 ID、项目的标记状态和标题
  2. StateManager 保存这些项目的数组。它还为数组的每个索引提供自定义绑定。对于 getter,它只返回模型在该索引处的状态。对于 setter,它创建了项目数组的新副本。只要设置了复选框,它就会取消选中所有其他复选框。
  3. ForEach 现在获得 items 的枚举。这可以在没有枚举的情况下完成,但是像这样通过索引编写自定义绑定很容易。您还可以按 ID 而不是索引进行过滤。请注意,由于枚举,它使用 .1.id 作为 id 参数 - .1 是项目,而 .0index
  4. ForEach 中,之前的自定义绑定被创建并传递给子视图
  5. 在子视图中,不使用@State,而是使用@Binding(这是自定义Binding传递给的)

使用包含您的所有状态并通过@Published 属性和@Bindings 传递它的ObservableObject 的这种策略可以更轻松地组织您的数据。它还避免了像最初使用 update 函数那样来回传递闭包。这最终成为 SwiftUI 中一种非常惯用的处理方式。