swiftswiftuifoundation

How to easily format UnitVolume for metric and imperial usage in Swift


I'm working on making something that uses fluid volume, and wanted to make it adaptable to both users who use imperial and metric.

I'm currently having issue with converting the metric to imperial on the slider, with the ClosedRange maxing out at the wrong values.

I'm not entirely sure where I'm going wrong, but also if this is the best way to deal with it.

My thought process was to store everything in metric and for those users who want imperial convert the UI for them - since metric is my main measurement (and easier to read for me).

This is the test code I was working with:

struct SettingsMenu: View {
  private let types: [UnitVolume] = [.milliliters, .fluidOunces]
  @State private var selectedUnitVolume: UnitVolume = .milliliters
  @State private var selectedGoal: Double = 2000

  var body: some View {
    Form {
      Section {
        Picker("Measurement unit", selection: $selectedUnitVolume) {
          ForEach(types, id: \.self) { type in
            Text(type.symbol).tag(type)
          }
        }
        .onChange(of: selectedUnitVolume) { _, newUnit in
          if newUnit == .milliliters {
            selectedGoal = Measurement(value: selectedGoal, unit: UnitVolume.fluidOunces).converted(to: .milliliters).value
          } else {
            selectedGoal = Measurement(value: selectedGoal, unit: UnitVolume.milliliters).converted(to: .fluidOunces).value
          }
        }

        VStack(alignment: .leading) {
          Label(label, systemImage: "drop.fill")
            Slider(value: $selectedGoal, in: range, step: step) {
              Text("Drink goal")
            } minimumValueLabel: {
              Text("\(Int(range.lowerBound))")
                .font(.footnote)
                .foregroundStyle(.gray)
            } maximumValueLabel: {
              Text("\(Int(range.upperBound))")
                .font(.footnote)
                .foregroundStyle(.gray)
            }
          }
        }
      }
  }

  var range: ClosedRange<Double> {
    selectedUnitVolume == .milliliters ? 100...5000 : 3...175
  }

  var step: Double {
    selectedUnitVolume == .milliliters ? 50 : 1
  }

  var label: String {
    let value = selectedUnitVolume == .milliliters ? selectedGoal : Measurement(value: selectedGoal, unit: UnitVolume.milliliters).converted(to: .fluidOunces).value.rounded()
    return "Daily saving goal: \(value.formatted(.number)) \(selectedUnitVolume.symbol)"
  }
}

Which results in this:

iOS app with slider switching between metric millilitres and imperial fluid ounce, but the conversion in the UI is incorrect

As you can see, the switch in the unit does update the labels for the min/max of the slider, but the actual range and the label are incorrect.


Solution

  • The main logic of your code is fine. But there are two issues here.

    I would advice you to round the values, when changing the units, to fit better in the step range you gave.