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?
1 Answer 1
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.