Home > Enterprise >  Dynamically sized @State var
Dynamically sized @State var

Time:09-20

I'm loading data into a struct from JSON. With this data, a new structure (itemStructures) is created and filled for use in a SwiftUI View. In this View I have a @State var which I manually initialise to have enough space to hold all items. This state var holds all parameters which are nested within the items, hence the nested array.

As long as this @State var has enough empty spaces everything works fine. But my question is, how do I modify this @State programmatically for when the number of items increases er decreases with the loading of a new JSON? I could make it really large but I'd rather have it the exact size after each load.

//Structs used in this example
struct MainViewState {
    var itemStructures: [ItemStructure]
}

struct ItemStructure: Identifiable, Hashable {
    var id: String {name}
    var name: String
    var parameters: [Parameter]
}

struct Parameter: Identifiable, Hashable {
    var id: String {name}
    var name: String
    var value: Double
    var range: [Double]
}

struct ContentView: View {
    
    //In this model json is loaded, this seemed out of scope for this question to include this
    @ObservedObject var viewModel: MainViewModel
    
    //This is the @State var which should be dynamically allocated according to the content size of "itemStructures"
    //For now 3 items with 10 parameters each are enough
    @State var parametersPerItem: [[Float]] = [
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0]
    ]
    
    init(viewModel: MainViewModel) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        
        let itemStructures = viewModel.mainState.itemStructures
        ForEach( Array(itemStructures.enumerated()), id: \.element ) { index, item in
            
            Text(item.name)
            
            ForEach( Array(item.parameters.enumerated()), id: \.element ) { i, parameter in
                    
                Text(parameter.name)
                
                SliderView(
                    label: parameter.name,
                    value: Binding(
                        get: { self.parametersPerItem[index][i] },
                        set: { (newVal) in

                            self.parametersPerItem[index][i] = newVal
                            //Function to send slider values and ranges to real time processing
                           //doStuffWithRangesAndValues() 
                        }
                    ),
                    range: parameter.range,
                    showsLabel: false
                ).onAppear {
                    //Set initial value slider
                    parametersPerItem[index][i] = Float(parameter.value)
                }
            }
        }
    }
}

struct SliderView: View {
    
    var label: String
    @Binding var value: Float
    var range: [Double]
    var showsLabel: Bool
    
    init(label: String, value: Binding<Float>, range: [Double], showsLabel: Bool = true) {
        self.label = label
        _value = value
        self.range = range
        self.showsLabel = showsLabel
    }
    
    var body: some View {
        GeometryReader { geometry in
                
            ZStack{
                if showsLabel { Text(label) }
                HStack {
                    Slider(value: $value)
                        .foregroundColor(.accentColor)
                        .frame(width: geometry.size.width * 0.8)
                    //In the real app range calculations are done here
                    let valueInRange = value
                    Text("\(valueInRange, specifier: range[1] >= 1000 ? "%.0f" : "%.2f")")
                        .foregroundColor(.white)
                        .font(.subheadline)
                        .frame(width: geometry.size.width * 0.2)
                }
            }
        }
        .frame(height: 40.0)
    }
}

CodePudding user response:

If you are looking for a solution where you want to initialise the array after the json has been loaded you could add a computed property in an extension to the main/root json model and use it to give the @State property an initial value.

extension MainViewState {
    var parametersPerItem: [[Float]] {
        var array: [[Float]] = []
        if let max = itemStructures.map(\.parameters.count).max(by: { $0 < $1 }) {
            for _ in itemStructures {
                array.append(Array(repeating: 0.0, count: max))
            }
        }
        return array
    }
}
  • Related