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 enumenum TimeUnits: String, CaseIterable { case seconds = "Seconds" case minutes = "Minutes" case hours = "Hours" case days = "Days"}// UI and logicstruct 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 placesstruct 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 elseinstead?
The way I'm handling the result-output: Is it a good idea or should Ialter it?
What are your thoughts concerning my coding in general? What would you have donedifferently and why?
1 Answer1
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 ...)}SwiftUIautomatically determines that the section depends on the three state variables, and recomputes it if any of them changes its value. TheonChange modifiers arenot needed at all.
The input field uses thenumber format style, which is locale dependent, but the output value is displayed usingString(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 input12,34 is interpreted as\$ 12 +34/100 \$, and displayed as12.34 in the output field.
To be consistent, you can use the samenumber 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 example12 instead of12.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 theUnits and Measurement APIs, in particularUnitDuration, 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 todays 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.
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.

