wombat
wombat

Reputation: 698

Change the options in a Picker dynamically using distinct arrays

I'm trying to get a Picker to update dynamically depending on the selection of the prior Picker. In order to achieve this, I'm using a multidimensional array. Unfortunately this seems to confuse my ForEach loop and I noticed the following message in the logs:

ForEach<Range<Int>, Int, Text> count (3) != its initial count (5).ForEach(:content:)should only be used for *constant* data. Instead conform data toIdentifiableor useForEach(:id:content:)and provide an explicitid!

This kinda makes sense, I'm guessing what is happening is that I'm passing it one array and it keeps referring to it, so as far as it is concerned, it keeps changing constantly whenever I pass it another array. I believe the way to resolve this is to use the id parameter that can be passed to ForEach, although I'm not sure this would actually solve it and I'm not sure what I would use. The other solution would be to somehow destroy the Picker and recreate it? Any ideas?

My code follows. If you run it, you'll notice that moving around the first picker can result in an out of bounds exception.

import SwiftUI

struct ContentView: View {
    @State private var baseNumber = ""
    @State private var dimensionSelection = 1
    @State private var baseUnitSelection = 0
    @State private var convertedUnitSelection = 0

    let temperatureUnits = ["Celsius", "Fahrenheit", "Kelvin"]
    let lengthUnits = ["meters", "kilometers", "feet", "yards", "miles"]
    let timeUnits = ["seconds", "minutes", "hours", "days"]
    let volumeUnits = ["milliliters", "liters", "cups", "pints", "gallons"]
    let dimensionChoices = ["Temperature", "Length", "Time", "Volume"]
    let dimensions: [[String]]

    init () {
        dimensions = [temperatureUnits, lengthUnits, timeUnits, volumeUnits]
    }

    var convertedValue: Double {

        var result: Double = 0
        let base = Double(baseNumber) ?? 0
        if temperatureUnits[baseUnitSelection] == "Celsius" {
            if convertedUnitSelection == 0 {
                result = base
            } else if convertedUnitSelection == 1 {
                result = base * 9/5 + 32
            } else if convertedUnitSelection == 2 {
                result = base + 273.15
            }
        }

        return result
    }


    var body: some View {
        NavigationView {
            Form {
                Section {
                    TextField("Enter a number", text: $baseNumber)
                        .keyboardType(.decimalPad)
                }

                Section(header: Text("Select the type of conversion")) {
                    Picker("Dimension", selection: $dimensionSelection) {
                        ForEach(0 ..< dimensionChoices.count) {
                            Text(self.dimensionChoices[$0])
                        }
                    }.pickerStyle(SegmentedPickerStyle())
                }

                Group {
                    Section(header: Text("Select the base unit")) {
                        Picker("Base Unit", selection: $baseUnitSelection) {
                            ForEach(0 ..< self.dimensions[self.dimensionSelection].count) {
                                Text(self.dimensions[self.dimensionSelection][$0])
                            }
                        }.pickerStyle(SegmentedPickerStyle())
                    }

                    Section(header: Text("Select the unit to convert to")) {
                        Picker("Converted Unit", selection: $convertedUnitSelection) {
                            ForEach(0 ..< self.dimensions[self.dimensionSelection].count) {
                                Text(self.dimensions[self.dimensionSelection][$0])
                            }
                        }.pickerStyle(SegmentedPickerStyle())
                    }
                }

                Section(header: Text("The converted value is")) {
                    Text("\(convertedValue) \(dimensions[dimensionSelection][convertedUnitSelection])")
                }

            }.navigationBarTitle("Unit Converter")
        }
    }
}

Upvotes: 4

Views: 1958

Answers (2)

wombat
wombat

Reputation: 698

I hate to answer my own question, but after spending some time on it, I think it's worth summarizing my findings in case it helps somebody. To summarize, I was trying to set the second Picker depending on what the selection of the first Picker was.

If you run the code that I pasted as is, you will get an out of bounds. This is only the case if I set @State private var dimensionSelection = 1 and the second array is larger than the first array. If you start with smaller array, you will be fine which you can observe by setting @State private var dimensionSelection = 0. There are a few ways to solve this.

  1. Always start with the smallest array (Not great)
  2. Instead of using an array of String, use an array of objects implementing Identifiable. this is the solution proposed by fuzz above. This got past the out of bound array exception. In my case though, I needed to specify the id parameter in the ForEach parameters.
  3. Extend String to implement Identifiable as long as your strings are all different (which works in my trivial example). This is the solution proposed by gujci and his proposed solution looks much more elegant than mine, so I encourage you to take a look. Note that this to work in my own example. I suspect it might be due to how we built the arrays differently.

HOWEVER, once you get past these issues, it will still not work, You will hit an issue that appears be some kind of bug where the Picker keep adding new elements. My impression is that to get around this, one would have to destroy the Picker every time, but since I'm still learning Swift and SwiftUI, I haven't gotten round doing this.

Upvotes: 2

gotnull
gotnull

Reputation: 27224

So you'll want to make sure according to Apple's documentation that the array elements are Identifiable as you've mentioned.

Then you'll want to use ForEach like this:

struct Dimension: Identifiable {
    let id: Int
    let name: String
}

var temperatureUnits = [
    Dimension(id: 0, name: "Celsius"),
    Dimension(id: 1, name: "Fahrenheit"),
    Dimension(id: 2, name: "Kelvin")
]

ForEach(temperatureUnits) { dimension in
    Text(dimension.name)
}

Upvotes: 0

Related Questions