UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Day 19 Challenge 1

Forums > 100 Days of SwiftUI

@moser  

I have no idea why my code is not working. There are no errors or warnings. Result always shows 0. When i return input.value instead of output.value it works fine but it refuses to return output.value for some reason.

import SwiftUI

struct ContentView: View {
    @State private var lengthInput = ""
    @State private var inputUnit = "m"
    @State private var outputUnit = "m"

    @FocusState private var lenghtIsFocused: Bool

    let units = ["m", "km", "ft", "yd", "mi"]

    var result: Double {
        guard lengthInput.isEmpty == false else {
            return 0
        }
        let input = Measurement(value: Double(lengthInput)!, unit: UnitLength(symbol: inputUnit))
        let output = input.converted(to: UnitLength(symbol: outputUnit))
        return output.value
    }

    var body: some View {
        Form {
            Section {
                TextField("Length", text: $lengthInput)
                    .keyboardType(.decimalPad)
                    .focused($lenghtIsFocused)
            }

            Section {
                Picker("Input Unit", selection: $inputUnit) {
                    ForEach(units, id: \.self) {
                        Text($0)
                    }
                }
                .pickerStyle(.segmented)
            } header: {
                Text("Input Unit")
            }

            Section {
                Picker("Output Unit", selection: $outputUnit) {
                    ForEach(units, id: \.self) {
                        Text($0)
                    }
                }
                .pickerStyle(.segmented)
            } header: {
                Text("Output Unit")
            }

            Section("Result") {
                Text(result, format: .number)
            }
        }
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                Spacer()
                Button("Done") {
                    lenghtIsFocused = false
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

2      

@moser poses a question:

I have no idea why my code is not working.

When I have this issue, I first resign myself to the conclusion that it must be a PEBCAK error. Next I try the rubber duck method to peel back the uncertainty and try to focus on what may be the root of the problem.

See -> Rubber Ducks 🐤 Teach Swift

Let's see how you implemented your code. Using the Rubber Duck method, let's add comments....

    @State private var lengthInput = ""
    @State private var inputUnit   = "m"   // 🐤 String!
    @State private var outputUnit  = "m"   // 🐤 String!

    let units = ["m", "km", "ft", "yd", "mi"]

    var result: Double {
       // 🐤 Gate guard returns ZERO if user didn't enter a starting value
        guard lengthInput.isEmpty == false else { return 0 }
        // 🐤 Initialize a new Measurement object providing Force-Unwrapped input
        // 🐤 and also providing a NEW UnitLength object initialized with a String.
        let input = Measurement(value: Double(lengthInput)!, unit: UnitLength(symbol: inputUnit))

        // 🐤 DEBUG TECHNIQUE
        print( input.description )  // <-- Let's see what Swift thinks input is....
        // prints -->. "12 ft" or something similar

        // 🐤 Convert this to another Measurement object using ANOTHER 
        // 🐤 NEW UnitLength object initialized with a String.
        let output = input.converted(to: UnitLength(symbol: outputUnit))

        // 🐤 WHY? This always returns ZERO.
        // 🐤 I think it's because it's not actually converting anything. Buy WHY??
        return output.value
    }

We see above that printing the description of the input object shows both value (12) and units (ft).

But, let's explore that a bit deeper.

See --> Measurement Documentation

Looking at the documentation for Measurement, you might miss this important bit:

Measurement objects are initialized with a Unit object and double value.

Unit Objects

You provided a nice clean double value. But are you providing a PROPER Unit Object? You're providing a String with the letters "yd" or "km" or "mi".

Rubber duck this part! Do the letters "km" represent a proper Unit Object? Are the letters "ft" a proper Unit object?

Digging further into the documentation, Apple tells us that the Unit object is a class and that the symbols (km, mi) are representations of NSMeasurement objects with the MeasurementFormatter class. Other words point to a Dimension subclass that allows conversions between dimensional units.

Yikes!

🐤 It looks like you're not using existing, prebuilt measurements that know how to convert themselves. Instead you created a NEW UnitLength object with a string symbol "mi", "km" that doesn't have any conversion information behind it.

Why might you do this? For example, you could create a new UnitLength called a Banana 🍌, with the symbol "ba". Then you could link to new NSMeasurements allowing you to convert the length of a small dog from inches to bananas, for scale. But you can't convert feet to bananas unless you provide additional structures. Swift doesn't know the maths to covert things to the "ba" UnitLength.

But inches, meters, yards, etc are common units of length and Swift has many prebuild UnitLength objects. You'll want to update your code so your user will select "km", "mi", "ft" from your Picker, but their selection will be changed to UnitLength objects. This will make conversions easy.

For more info see @twoStraw's article:

See --> Unit Conversions

Keep Coding

Please return here and share how you solved this.

2      

@moser  

The problem was me not reading the documentation properly after all... I added two switch statements to solve the problem. This is how my computed property looks like in the working code.

var result: Double {
        guard lengthInput.isEmpty == false else {
            return 0
        }
        var inputting: UnitLength
        var outputting: UnitLength

        switch inputUnit {
        case "m":
            inputting = .meters
        case "km":
            inputting = .kilometers
        case "ft":
            inputting = .feet
        case "yd":
            inputting = .yards
        default:
            inputting = .miles
        }

        switch outputUnit {
        case "m":
            outputting = .meters
        case "km":
            outputting = .kilometers
        case "ft":
            outputting = .feet
        case "yd":
            outputting = .yards
        default:
            outputting = .miles
        }

        let input = Measurement(value: Double(lengthInput)!, unit: inputting)
        let output = input.converted(to: outputting)
        return output.value
    }

2      

Your code appears to be mostly correct. However, there are a couple of minor issues and improvements you can make. Here's the corrected code:

import SwiftUI

struct ContentView: View { @State private var lengthInput = "" @State private var inputUnit = "m" @State private var outputUnit = "m"

@FocusState private var lengthIsFocused: Bool

let units = ["m", "km", "ft", "yd", "mi"]

var result: Double {
    guard let inputValue = Double(lengthInput) else {
        return 0
    }
    let input = Measurement(value: inputValue, unit: UnitLength(symbol: inputUnit))
    let output = input.converted(to: UnitLength(symbol: outputUnit))
    return output.value
}

var body: some View {
    Form {
        Section {
            TextField("Length", text: $lengthInput)
                .keyboardType(.decimalPad)
                .focused($lengthIsFocused)
        }

        Section(header: Text("Input Unit")) {
            Picker("Input Unit", selection: $inputUnit) {
                ForEach(units, id: \.self) {
                    Text($0)
                }
            }
            .pickerStyle(.segmented)
        }

        Section(header: Text("Output Unit")) {
            Picker("Output Unit", selection: $outputUnit) {
                ForEach(units, id: \.self) {
                    Text($0)
                }
            }
            .pickerStyle(.segmented)
        }

        Section(header: Text("Result")) {
            Text("\(result, specifier: "%.2f")")
        }
    }
    .toolbar {
        ToolbarItemGroup(placement: .keyboard) {
            Spacer()
            Button("Done") {
                lengthIsFocused = false
            }
        }
    }
}

}

struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }

Here are the changes made:

Renamed lenghtIsFocused to lengthIsFocused for consistency and corrected its usage in the code.

Added optional binding for converting the lengthInput to a Double and guarding against non-numeric input.

Modified the Text view in the "Result" section to format the result with two decimal places using specifier: "%.2f".

These changes should make your code work as expected.

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.