4
\$\begingroup\$

The given task is to implement a time-units app. I guess I got the main logic right, but concerning the UI-coding there could be improvements.

Here's my code:

// Time-Units enum
enum TimeUnits: String, CaseIterable {
 case seconds = "Seconds"
 case minutes = "Minutes"
 case hours = "Hours"
 case days = "Days"
}
// UI and logic
struct ContentView: View {
 @State private var selectedInput = TimeUnits.seconds
 @State private var selectedOutput = TimeUnits.seconds
 @State private var inputValue: Double? = nil
 @State private var outputValue = 0.0
 
 func convertInput() {
 outputValue = inputValue ?? 0.0
 
 if var inputValue = inputValue {
 switch selectedInput {
 case .seconds:
 break
 case .minutes:
 inputValue *= 60.0
 case .hours:
 inputValue *= (60.0 * 60.0)
 case .days:
 inputValue *= (60.0 * 60.0 * 24.0)
 }
 
 switch selectedOutput {
 case .seconds:
 outputValue = inputValue
 case .minutes:
 outputValue = inputValue / 60.0
 case .hours:
 outputValue = inputValue / (60.0 * 60.0)
 case .days:
 outputValue = inputValue / (60.0 * 60.0 * 24.0)
 }
 }
 }
 
 var body: some View {
 VStack {
 Text("Time-Units Converter")
 .font(.title)
 Form {
 Section {
 TextField(
 "Value to convert",
 value: $inputValue,
 format: .number
 )
 .keyboardType(.decimalPad)
 UnitPickerView(selectedUnit: $selectedInput)
 } header: {
 Text("Input")
 .font(.title2)
 }
 Section {
 UnitPickerView(selectedUnit: $selectedOutput)
 Text("\(String(format: "%.2f", inputValue ?? 0.0)) \(selectedInput.rawValue) > \(String(format: "%.2f", outputValue)) \(selectedOutput.rawValue)")
 .font(.title3)
 .bold()
 } header: {
 Text("Output")
 .font(.title2)
 }
 }
 }
 .onChange(of: selectedInput) {
 convertInput()
 }
 .onChange(of: selectedOutput) {
 convertInput()
 }
 .onChange(of: inputValue) {
 convertInput()
 }
 }
}
// UI-logic used in multiple places
struct UnitPickerView: View {
 @Binding var selectedUnit: TimeUnits
 
 var body: some View {
 Picker("Please selected the input-unit", selection: $selectedUnit) {
 ForEach(TimeUnits.allCases, id: \.self) { unit in
 Text(unit.rawValue)
 }
 }.pickerStyle(.segmented)
 }
}
  • Is there a way to get rid of the three-times onChange-modifier? Should I avoid the approach completely and doing something else instead?

  • The way I'm handling the result-output: Is it a good idea or should I alter it?

  • What are your thoughts concerning my coding in general? What would you have done differently and why?

asked Oct 6, 2024 at 11:15
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

The conversion function should be separate from the UI, for example as a global function

func convert(_ inputValue: Double, from fromUnit: TimeUnits, to toUnit: TimeUnits) -> Double {
 // ...
}

so that it can be reused, and unit tests can be written for it. I'll come back to that function later.

Input value, input unit, and output unit are the "source of truth" for the view, and correctly defined with the @State attribute. The output value however, depends on these values, and there is no need to define it as a state variable. Just compute it from the input state where needed:

Section {
 let outputValue = convert(inputValue ?? 0.0, from: selectedInput, to: selectedOutput)
 Text(... outputValue ...)
}

SwiftUI automatically determines that the section depends on the three state variables, and recomputes it if any of them changes its value. The onChange modifiers are not needed at all.

The input field uses the number format style, which is locale dependent, but the output value is displayed using String(format: "%.2f", ...), which is locale independent.

As in example, the decimal separator in the German locale is the comma and not a period, so the input 12,34 is interpreted as \$ 12 +34/100 \$, and displayed as 12.34 in the output field.

To be consistent, you can use the same number format style for the output conversion, in the simplest case this is

Section {
 let outputValue = convert(inputValue ?? 0.0, from: selectedInput, to: selectedOutput)
 Text(outputValue.formatted(.number))
}

This will also display only the needed fractional digits (up to some maximum number), for example 12 instead of 12.00.

There is some repetition in the conversion function, the factor for a chosen unit is determined at two places. One option to simplify this is to make the conversion factor a property of the time unit:

enum TimeUnits: String, CaseIterable {
 case seconds = "Seconds"
 case minutes = "Minutes"
 case hours = "Hours"
 case days = "Days"
 
 func factor() -> Double {
 switch self {
 case .seconds: return 1.0
 case .minutes: return 60.0
 case .hours: return 60.0 * 60.0
 case .days: return 60.0 * 60.0 * 24.0
 }
 }
}
func convert(_ inputValue: Double, from fromUnit: TimeUnits, to toUnit: TimeUnits) -> Double {
 return inputValue * fromUnit.factor() / toUnit.factor()
}

I suggest to have a look at the Units and Measurement APIs, in particular UnitDuration, and check if you can use these instead of your custom-made conversions.

One final remark: One has always to be careful with conversions from seconds/minutes/hours to days and larger units. A "day" does not always have \$ 60 \cdot 60 \cdot 24 = 86400\$ seconds. Because of daylight saving time transitions, it can be one hour more or less.

answered Oct 6, 2024 at 14:38
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.