How can I create a picker (to choose a person's body weight and height) to display imperial/metric measurement based on their locale?

209 views Asked by At

I am newish to SwiftUI. I am creating a picker, that will let a user choose their body weight (and similarly for their height). As you know, in the US (and a few other countries), people use imperial measurements (pounds, feet, etc.), while in most other countries, people use the metric system (kilograms, meters, etc.). How can I make this picker easily and accurately adapt to the user's region/locale? For example, if user's region uses metric system, display [40kg, 42.5kg, 45kg, 47.5kg, .... 300kg]; if imperial, display [80lb, 85lb, 90lb, ..., 600lb]

This is my draft code that basically hard-coded things for imperial measurement:

import SwiftUI

struct SetMyWeightView: View {
    
    // This works for imperial system. But how I can make it work for both imperial and metric system? For example, if user's region uses metric system, display [40kg, 42.5kg, 45kg, 47.5kg, .... 300kg]; if imperial, display [80lb, 85lb, 90lb, ..., 600lb]

    @State private var myWeight: Int?
    
    var body: some View {
        Form {
            Section {
                Picker("", selection: $myWeight) {
                        // Offer user an option to hide their weight
                        Text("Hide").tag(nil as Int?)
                    
                        // Choose from a list [80, 85, 90, ..., 600]
                        ForEach(Array(stride(from: 80, to: 601, by: 5)), id: \.self) { weight in
                            Text("\(weight) lb").tag(weight as Int?)
                        }
                }
                .pickerStyle(.wheel)
            } header: {
                Text("Weight")
            }
        }
    }
}

#Preview {
    SetMyWeightView()
}

enter image description here

2

There are 2 answers

0
MatBuompy On

you can achieve that by using the Locale.current class. In that there are two properties (one of them introduced in iOS 16 that tells you in which metric system the device is. Here's the code:

struct SetMyWeightView: View {
    
    
    var isMetric: Bool {
        let locale = Locale.current
        if #available(iOS 16, *) {
            return locale.measurementSystem == .metric
        } else {
            return locale.usesMetricSystem
        }
        
    }
    
    var weightUnit: String {
        return isMetric ? "kg" : "lb"
    }
    
    var weightRange: ClosedRange<Int> {
        return isMetric ? 40...300 : 80...600
    }
    
    @State private var myWeight: Int?
    
    var body: some View {
        Form {
            Section {
                Picker("", selection: $myWeight) {
                    // Offer user an option to hide their weight
                    Text("Hide").tag(nil as Int?)
                    
                    ForEach(Array(stride(from: weightRange.lowerBound, through: weightRange.upperBound, by: 5)), id: \.self) { weight in
                        Text("\(weight) \(weightUnit)").tag(weight as Int?)
                    }
                }
                .pickerStyle(.wheel)
            } header: {
                Text("Weight")
            }
        }
    }
}

Try that out and let me know if that worked out for you!

0
Sweeper On

You should use a state of type Measurement to store the selected weight.

First, I would like to suggest using a TextField instead of a Picker. This gives the user a lot more freedom to enter whatever they want, and they can choose whatever unit they want. See this post for how to do this. That said, if you really want to use a Picker, there are two options:


Hardcod different options for different measurement systems.

You can do something like this:

@State var selectedWeight: Measurement<UnitMass>?

@Environment(\.locale) var locale

// these are the hardcoded options for each measurement system...
let imperialOptions = stride(from: 80, through: 600, by: 5).map {
    Measurement(value: Double($0), unit: UnitMass.pounds)
}
let metricOptions = stride(from: 80, through: 600, by: 5).map {
    Measurement(value: Double($0) / 2, unit: UnitMass.kilograms)
}

// here you determine which set of options to use
var options: [Measurement<UnitMass>] {
    if locale.measurementSystem == .metric {
        return metricOptions
    } else {
        return imperialOptions
    }
}

Do note that there are other measurement systems that are not metric. You might want to take that into account too, depending on where your users are located.

Then the Picker can be created like this:

Picker("", selection: $selectedWeight) {
    Text("Hide").tag(nil as Measurement<UnitMass>?)
    
    ForEach(options, id: \.self) { option in
        Text(
            option,
            format: .measurement(width: .abbreviated, usage: .asProvided)
        ).tag(option as Measurement<UnitMass>?)
    }
}
.pickerStyle(.wheel)

By doing it this way, selectedWeight can have different units. You can convert it to a desired unit using converted(to:), for further processing/storing in a database/whatever else.


One set of options but format them differently depending on locale.

For example:

// only have one set of options, in kilograms for example
var options = stride(from: 80, through: 600, by: 5).map {
    Measurement(value: Double($0) / 2, unit: UnitMass.kilograms)
}

...

ForEach(options, id: \.self) { option in
    // here we use the .personWeight usage instead of .asProvided
    Text(
        option,
        format: .measurement(width: .abbreviated, usage: .personWeight)
    ).tag(option as Measurement<UnitMass>?)
}

The disadvantage of this is that you get less control over what units are actually used, and the numbers are not nice and "round" numbers.