使@State 变量和 UI 适应 SwiftUI 中的用户操作

问题描述

我是编程和探索 SwiftUI 的新手。我一直在应对挑战,希望有人能指引我走向正确的方向!

我想要一个相互链接的滑块列表(如 Interlinked Multiple Sliders in SwiftUI),但滑块的数量会根据用户采取的操作动态变化。

例如,用户可以选择各种项目,然后使用滑块调整百分比变量(以及这些百分比在链接示例中相互依赖的位置)。

class Items: ObservableObject {
    
    @Published var components = [ItemComponent]()
    
    func add(component: itemComponent){
        components.append(component)
        }

}

struct ItemComponent: Hashable,Equatable,Identifiable {
       
    var id = UUID().uuidString
    var name: String = ""
    var percentage: Double
}

从概念上讲,我似乎需要做两件事来调整链接代码

  1. 生成一个 Binding 数组,其元素数等于 Items.Component.Endindex 和
  2. 将每个 Binding 分配给每个 ItemComponent 的百分比。

我对两者都在摸索。对于 1.,我可以轻松地手动创建任意数量的变量,例如

@State var value1 = 100
@State var value2 = 100
@State var value3 = 100

let allBindings = [$value1,$value2,$value3]

但是我如何自动生成它们?

对于 2.,我可以使用 ForEach() 来调用组件或索引,但不能同时使用:

ForEach(Items.components){ component in 
    Text("\(component.name)")
    Text("\(component.percentage)")
}

ForEach(Items.components.indices){ i in 
    synchronizedSlider(from: allBindings,index: i+1)
}

在损坏的代码中,我想要的是:

ForEach(Items.component){component in
     HStack{
            Text("component.name")
Spacer()
synchronizedSlider(from: allBindings[$component.percentage],index: component.indexPosition)

      }


其中 allBindings[$component.percentage] 是由每个 itemComponent 的百分比组成的绑定数组,索引是 itemComponent 的索引。

如果相关,我很乐意分享更多代码。任何帮助将不胜感激!

解决方法

为了适应您链接的现有代码,如果您要拥有动态数量的滑块,您肯定希望您的 @State 是一个数组,而不是单个 @State 变量,这必须是硬编码的。

一旦你有了它,就会有一些小的语法问题将 synchronizedBinding 函数更改为接受 Binding<[ItemComponent]> 而不是 [Binding<Double>],但它们非常小。幸运的是,现有代码在初始硬编码状态之外非常健壮,因此无需任何额外的数学运算来进行计算。

我正在使用 ItemComponent 而不仅仅是 Double 因为您的示例代码包含它并且具有具有唯一 id 的模型使我正在使用的 ForEach 代码滑块更容易处理,因为它需要唯一可识别的项目。


struct ItemComponent: Hashable,Equatable,Identifiable {
    
    var id = UUID().uuidString
    var name: String = ""
    var percentage: Double
}

struct Sliders: View {
    
    @State var values : [ItemComponent] = [.init(name: "First",percentage: 100.0),.init(name: "Second",percentage: 0.0),.init(name: "Third",.init(name:"Fourth",]
    
    var body: some View {
        VStack {
            Button(action: {
                // Manually setting the values does not change the values such
                // that they sum to 100. Use separate algorithm for this
                self.values[0].percentage = 40
                self.values[1].percentage = 60
            }) {
                Text("Test")
            }
            
            Button(action: {
                self.values.append(ItemComponent(percentage: 0.0))
            }) {
                Text("Add slider")
            }
            
            Divider()
            
            ScrollView {
                ForEach(Array(values.enumerated()),id: \.1.id) { (index,value) in
                    Text(value.name)
                    Text("\(value.percentage)")
                    synchronizedSlider(from: $values,index: index)
                }
            }
            
        }.padding()
    }
    
    
    func synchronizedSlider(from bindings: Binding<[ItemComponent]>,index: Int) -> some View {
        return Slider(value: synchronizedBinding(from: bindings,index: index),in: 0...100)
    }
    
    func synchronizedBinding(from bindings: Binding<[ItemComponent]>,index: Int) -> Binding<Double> {
        
        return Binding(get: {
            return bindings[index].wrappedValue.percentage
        },set: { newValue in
            
            let sum = bindings.wrappedValue.indices.lazy.filter{ $0 != index }.map{ bindings[$0].wrappedValue.percentage }.reduce(0.0,+)
            // Use the 'sum' below if you initially provide values which sum to 100
            // and if you do not set the state in code (e.g. click the button)
            //let sum = 100.0 - bindings[index].wrappedValue
            
            let remaining = 100.0 - newValue
            
            if sum != 0.0 {
                for i in bindings.wrappedValue.indices {
                    if i != index {
                        bindings.wrappedValue[i].percentage = bindings.wrappedValue[i].percentage * remaining / sum
                    }
                }
            } else {
                // handle 0 sum
                let newOtherValue = remaining / Double(bindings.wrappedValue.count - 1)
                for i in bindings.wrappedValue.indices {
                    if i != index {
                        bindings[i].wrappedValue.percentage = newOtherValue
                    }
                }
            }
            bindings[index].wrappedValue.percentage = newValue
        })
    }
}