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

almostSOLVED: Day 19, ConverterApp Challenge. ENUM use. Help needed.

Forums > 100 Days of SwiftUI

I solved this task in a very simple way some time ago but I feel this was kinda cheating from my side. So I want to turn the page and move on but It does not let me sleep calm.

I checked multiple topics about this challenge how other people managed to solve it and there was a lot of cases(maybe most of) where people used "switch/case" option. It's a great way and it works but in one of the topics @Obelix suggested to use the power of Enums. https://www.hackingwithswift.com/forums/100-days-of-swiftui/day-17-challenge-switch-case-default/15294

@Obelix Your computed property relies on the value of toUnit. Strings, like toUnit can contain gazillion possible combinations of characters. Can you refactor a solution where toUnit contains possibilities limited to those you define in an enum? Instead of defining toUnit as a variable that holds a string, what might be a better way to define toUnit ?

So I want to learn how to use Enum to solve this in a smart and efficient way and to be able to use this knowledge further on in other tasks.

There's also another case where @roosterboy offers to use Enum to solve the task in "RockPaperScissors Challenge" https://www.hackingwithswift.com/forums/100-days-of-swiftui/rock-scissor-paper-project-crash/10171

I tried to implement the Idea in "converterApp" and also failed. So decided to start from all the beginning. Maybe this way it will work out.

Please help me with this. Thank you

and yes. Tere are many errors respectively...

import SwiftUI
struct ContentView: View {

    @State private var inputValue = 0.0
    @State private var conversionType = ""
    @State private var inputUnits = Units.km

    // so here with just "= Units" I get the error and the only way to get rid of it I found is either to attach
    // the particular case from the enum to input or output variables like .km
    // or "=Units.self" works to get rid of error but doesn't fit in general with other code

    // Is it possible to make available all cases from Units Enum somehow?

    @State private var outputUnits = Units.miles
    @FocusState private var amountIsFocused: Bool

    private let conversionTypes = ["m", "km", "feet", "miles"]

    enum Units: Int {
        case m, km, feet, miles
    }

    var convertedValue: Double {
    var outputCoef = 1.0
        func calculateOutput(inputValue: Double, outputUnits: Units) -> Double {
        // maybe I don't even need to use function here but I tried to apply any option : /

            var outputUnits: Units {
                switch self {
                case .m:
                    outputCoef = 1.0
                case .km:
                    outputCoef = 1000
                case .feet:
                    outputCoef = 0.3048
                case .miles:
                    outputCoef = 1609.344
                default:
                    outputCoef = 1.0
                }
            }
            let outputValue = inputValue * outputCoef
            let roundedOutputValue = round(outputValue * 100) / 100.0

            return roundedOutputValue
        }
    }

    var body: some View {

        NavigationView {
            Form {
                Section {
                    TextField("Input Value", value: $inputValue, format: .number)
                        .keyboardType(.decimalPad)
                        .focused($amountIsFocused)
                } header: {
                    Text("Type in the value")
                }
                Section {
                    Picker("Input units", selection: $inputUnits) {
                        ForEach(conversionTypes, id: \.self) {
                            Text($0)
                        }
                    } .pickerStyle(SegmentedPickerStyle())
                } header: {
                    Text("Choose input units")
                }
                Section {
                    Picker("Measurement units", selection: $outputUnits) {
                        ForEach(conversionTypes, id: \.self) {
                            Text($0)
                        }
                    } .pickerStyle(SegmentedPickerStyle())
                } header: {
                    Text("Choose output units")
                }
                Section {
                    Text(convertedValue, format: .number)
                } header: {
                    Text("Conversion result")
                }
            }
            .navigationTitle(
                Text("Metric/Imperial converter")
            )
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Spacer()
                    Button("Done") {
                        amountIsFocused = false
                    }
                }
            }
        }
    }
}

2      

I hope someone will give me lesson

2      

@ngk seeks help with code.

I hope someone will give me lesson

It's great that you're revisiting your code looking for more Swifty ways to improve. Please paste the following into Playgrounds and run!

One lesson you may find interesting is that enums are much more than a list of possible selections. Here you can see code that links an enum case to a Swift defined UnitLength. This can be used in a Measurement object.

The Measurement object allows you to easily convert from one UnitLength to another.

See -> Measurement Objects in Swift

import Foundation
enum Conversions: String, CaseIterable {
    case cm   // centimeters
    case m    // meters
    case dm   // decameters
    case km   // kilometers
    case inch // inches

    var unitLength: UnitLength {
        switch self {
            // enum                 measurement type
            // --------------------------------------------
            case .cm:       return UnitLength.centimeters
            case .m:        return UnitLength.meters
            case .dm:       return UnitLength.decimeters
            case .km:       return UnitLength.kilometers
            case .inch:     return UnitLength.inches
        }
    }
}

// Perhaps you can use allLengthOptions in a Picker?
let allLengthOptions = Conversions.allCases.compactMap { $0.rawValue }
print (allLengthOptions)  // <-- What does this print?

// Don't do this.
let height = 178.0  // <-- This is meaningless. It's just a value

// In your user interface, collect this data from text fields, or Pickers
let convertValue      = 42.0            // standard double assignment
let convertFromLength = Conversions.m   // standard enum assignment
let convertToLength   = Conversions.dm  // standard enum assignment

// Do this: Initialize the Measurement object
// Measurement objects have both a value AND a measurement unit.
// Turn your enum into a UnitLength via Conversions' computed var
var towelToss = Measurement(value: convertValue, unit: convertFromLength.unitLength)

// Now use the magic of the Measurement's methods to convert from one unit to another
towelToss.convert(to: convertToLength.unitLength)

// If you have a string, how do you find the related enum?
let fromUnit = Conversions(rawValue: "inch")       // What type is fromUnit?
let fromUnit_as_UnitLength = fromUnit?.unitLength  // What type is this?

// Don't forget your towel!

Keep Coding!

Please return here and let us know what you learned.

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!

Thank you for the lesson, @Obelix. I already thought you gave up on such a "slow" student as me.

It's great that you're revisiting your code looking for more Swifty ways to improve.

I have no other choice. 1) I literally have problems sleeping. 2) You can't build a reliable wall if you will skip some briks on lower rows. So let's jump into your reply.

// Perhaps you can use allLengthOptions in a Picker?
let allLengthOptions = Conversions.allCases.compactMap { $0.rawValue }
print (allLengthOptions)  // <-- What does this print?*

As I understand this method thansformed our Enum Cases and printed them as an Array of Strings. At least that's what I can see from the output result ( and using Option + ? pointing on "compactMap" to find out more)

// In your user interface, collect this data from text fields, or Pickers
let convertValue      = 42.0            // standard double assignment
let convertFromLength = Conversions.m   // standard enum assignment
let convertToLength   = Conversions.dm  // standard enum assignment

So here, as I understand, I cant specify all cases after "." (And no need in this ?) It shuld be something set by default like in your case conversion from "m" to "dm" without touching the picker. (In my case further it will be "m" to "feet")

// Measurement objects have both a value AND a measurement unit.
// Turn your enum into a UnitLength via Conversions' computed var* 

Here I gut stuck.

let fromUnit = Conversions(rawValue: "inch")       // What type is fromUnit?
let fromUnit_as_UnitLength = fromUnit?.unitLength  // What type is this?

Here too. Sorry sir

What type is fromUnit? What type is fromUnitasUnitLength ? - can I assume both of them are Optionals ?

As I understand this line: let fromUnit = Conversions(rawValue: "inch") is the way of writing this(or not?) : @State private var convertFromUnit = Conversions.m

when cases in our enum are Strings and look like this:

    enum Conversions: String, CaseIterable {
        case "m"
        case "km"
        case "feet"
        case "miles"

But when I tried to use this option(with "string" cases ) in xcode the wave of red errors fall on me... So for now I'll stick to original example

2      

I immediately tried to implement your examples in field. But as usual I failed : / Here we go:

import SwiftUI

struct ContentView: View {

    @State private var convertValue = 0.0
    @State private var convertFromUnit = Conversions.m
    @State private var convertToUnit = Conversions.feet
    private let allLenghtOptions = Conversions.allCases.compactMap {$0.rawValue}

    @FocusState private var amountIsFocused: Bool

    enum Conversions: String, CaseIterable {
        case m, km, feet, miles

        var unitLenght: UnitLength {
            switch self {
            case .m:
                return UnitLength.meters
            case .km:
                return UnitLength.kilometers
            case .feet:
                return UnitLength.feet
            case .miles:
                return UnitLength.miles
            }
        }
    }
    private var convertedValue:Double {
        //var results = Measurement(value: convertValue, unit: convertFromUnit.unitLenght)
        //results.convert(to: convertToUnit.unitLenght)
        // and here we have the error "Missing return in getter expected to return 'Double'"
        // which I get here using your code.

        // I assume that I need to make an output value which should be a Double
        // cause I specified it myself. But how ?
        // I couldn't figure out and used the help of @MarcusKay's code:

        let results = Measurement(value: convertValue, unit: convertFromUnit.unitLenght)
        return results.converted(to: convertToUnit.unitLenght).value
        // I have no idea why it works but it works without creating an extra Double varible
    }
    var body: some View {
        NavigationView {
            Form {
                Section {
                    TextField("Convert Value", value: $convertValue, format: .number)
                        .keyboardType(.decimalPad)
                        .focused($amountIsFocused)

                    Picker("From units", selection: $convertFromUnit) {

                        //Second problem comes here. Code compiles but it's not possible to choose
                        //the conversion units. you see them but cant choose so the calculation
                        //goes only using the "default" values set in top of ContentView struct.

                        // Why is it like that ?

                        ForEach(allLenghtOptions, id: \.self) {
                            Text("\($0)")
                        }
                    } .pickerStyle(SegmentedPickerStyle())
                }

                Section {
                    Picker("From units", selection: $convertToUnit) {
                        ForEach(allLenghtOptions, id: \.self) {
                            Text($0)
                        }
                    } .pickerStyle(SegmentedPickerStyle())

                    Text("\(convertedValue, specifier: "%.4g")")
                }
            }
            .navigationTitle(Text("Converter"))
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Spacer()
                    Button("Done") {
                        amountIsFocused = false
                    }
                }
            }
        }
    }
}

2      

You are coding and failing. Then asking questions, then coding and failing.

This is the best way to learn. Well done.

But I don't want you to get frustrated and give up. Here some code to guide you.

struct ConverterView: View {
    let allUnits = Conversions.allCases.compactMap { $0.rawValue }
    // This is a computed property.
    // Whatever value and unit the user selects, my program can
    // use this variable, and SwiftUI, voila!,
    // will instantly calculate the convertedValue.
    var convertedValue: Double {
        guard let convertValue = Double(usersValue) else {return 0 }
        guard let fromUnit     = Conversions(rawValue: changeFromUnit ) else { return 0.0 }
        guard let toUnits      = Conversions(rawValue: changeToUnit   ) else { return 0.0 }

        return Measurement( value: convertValue, unit: fromUnit.unitLength )
            .converted(to: toUnits.unitLength)
            .value
    }

    // changeToUnit and changeFromUnit are Strings. I needs strings for the Picker.
    // But I cannot use Strings in the Measurement object. Conundrum!
    // Use the Enum's computed var to find the equivalent UnitLength
    @State private var changeFromUnit = "feet"
    @State private var changeToUnit   = "cm"
    @State private var usersValue     = "42.00"
    @FocusState private var valueIsFocused : Bool
    var body: some View {
        VStack {
            Image(systemName: "ruler")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Form {
                Section(header: Text("Convert from")) {
                    TextField("value", text: $usersValue)
                        .focused($valueIsFocused)
                        .keyboardType(.decimalPad)
                    Picker("From", selection: $changeFromUnit) {
                    // I think of ForEach as a view factory.
                    // It builds Text views from an array of strings.
                        ForEach( allUnits, id: \.self ) { unit in
                            Text( unit )
                        }
                    }
                    .padding(.horizontal)
                    .pickerStyle(SegmentedPickerStyle() )
                }
                Section(header: Text("Convert to")) {
                    Picker("To", selection: $changeToUnit) {
                        ForEach( allUnits, id: \.self ) { unit in
                            Text( unit )
                        }
                    }
                    .padding(.horizontal)
                    .pickerStyle(SegmentedPickerStyle() )
                }
                Section(header: Text("Results")) {
                    HStack{
                        Spacer()
                        Text("\(convertedValue, specifier: "%.2f") \(changeToUnit)" )
                            .fontWeight(.heavy)
                        Spacer()
                    }
                }
            }
        }
    }
}

// iOS 10 introduced a system for calculating distances, lengths, etc.
enum Conversions: String, CaseIterable {
    case cm       // centimeters
    case m        // meters
    case dm       // decimeters
    case inch     // inch
    case feet     // feet
    case yards    // yards
    case miles    // miles

    var unitLength: UnitLength {
        switch self {
            //    enum              measurement type
            // -----------------------------------------
            case .inch:     return UnitLength.inches
            case .feet:     return UnitLength.feet
            case .yards:    return UnitLength.yards
            case .miles:    return UnitLength.miles
            case .cm:       return UnitLength.centimeters
            case .dm:       return UnitLength.decameters
            case .m:        return UnitLength.meters
        }
    }
}

3      

Good news. Accidentally while experementing I fixed a problem with picker by myself:

private let allLenghtOptions = Conversions.allCases  
// I felt that something is wrong here. If we were getting printed array of strings in Playground
// then how we can switch between conversion types if there are none, only "km" strings ? 
// so got rid of ".compactMap {$0.rawValue}" ending to see what will happen 

Picker("From units", selection: $convertFromUnit) {
                        ForEach(allLenghtOptions, id: \.self) {
                            Text($0.rawValue)  
                            // and after that Xcode offered me to add .rawValue here and it got fixed
                        }
                    } .pickerStyle(SegmentedPickerStyle())

So now we can switch between different units and immediately get the output. In such moments I carefully continue to believe that I'm not wasted...

2      

Ach! Please don't use "ContentView" !

See -> Rename ContentView

First.

My apologies. I remember about your previous lesson and your sensitivity to "ContentView" definition. Although I allowed myself to think this is not the biggest problem for me atm and left it for better times.

If I won't be able to write code there will be no difference between: "ContentView", "MomentView" or "SnowLandView" Cause I will still suck in coding.

I hope you understand, sir

But If you will continue to help me with answers on my questions,

I promise, I will get my hands to "Rename ContentView" topic, and further will never use default "ContentView. Cause I honestly hate this default naming.

2      

Second. I think after your Project Code I can understand much more. That's good ! But still not everything.

Mooooore questions (I apologize...)

    // changeToUnit and changeFromUnit are Strings. I needs strings for the Picker.
    // But I cannot use Strings in the Measurement object. Conundrum!
    // Use the Enum's computed var to find the equivalent UnitLength
    @State private var changeFromUnit = "feet"
    @State private var changeToUnit   = "cm"
    @State private var usersValue     = "42.00"

I don't get it. Why using strings ?

1) we know that we can use the .km case from Enum. Then why use strings ?
2) perhaps we can use Int to acess indecies of the array which may contain our UnitLenghts 3) user will anyway be typing digits in the textField. Digits are either Int or Double,

then why use strings in @State variables ? : )

I can understand the need of strings for the picker. Picker needs text values.

But as we already know, we can avoid using them in changeFromUnit/changeToUnit. Then why ?

Or that all is about getting the same result but in different ways ?

 // Use the Enum's computed var to find the equivalent UnitLength

var unitLength: UnitLength {
      switch self {
          //    enum              measurement type
          // -----------------------------------------
          case .inch:     return UnitLength.inches
          case .feet:     return UnitLength.feet
          case .yards:    return UnitLength.yards
          case .miles:    return UnitLength.miles
          ...

Is this part Enum's computed var you're talking about ? if YES, then I've never been thinking about it as a computed var ...

return Measurement( value: convertValue, unit: fromUnit.unitLength )
            .converted(to: toUnits.unitLength)
            .value

var towelToss = Measurement(value: convertValue, unit: convertFromLength.unitLength)
towelToss.convert(to: convertToLength.unitLength)

I still dont get the trick between those two implementations.

Why did you use .convert(to:) option as an example to show me in Playground if it's not working in Project, and not .converted(to:).value from the beginning ?

but in your project code you used already .converted(to:).value ? :O

I double checked, in Playground both options work.

But in Project only .converted(to:).value.

that makes my head spin, sir.

         // var id: String { self.rawValue }  //   <- This line 

        var unitLength: UnitLength {

Almost forgot

What's that and why it is there as the note ?

First I thought it's related to id: \.self in Picker but I checked and it does not change anything in compilation while being a part of code. Than what is it for there ?


upd: @Obelix kindly provided me answers to some of my questions so I'm thankful for his help. Code compiles, app works. Unfortunately I didn't get the answers on all my questions. If I'll find them later - I will update my own topic here. To be able to help other people.

Meanwhile topic will be marked as: "almostSolved"

2      

@tomn  

Hello all, This is my attempt.

Wins:

  1. figured out how to declare enums, and used them successfully in a picker (after errors and searching 30 mins). Solution I came to (not sure if it's the definite correct one) was to loop through an enum with allCases, and using "verbatim:" to use each case in a picker.
  2. At one point, I was able to use a tuple with converted value number (Double), converted to unit (String) and number format specifier (%.2f). That supported one function to work for both Temperature and Time conversions.

Fails:

  1. Would have liked to add a function so that user can select another conversion type and repopulate the pickers with appropriate enum.
  2. Using outputNumber.formatted() and it works nicely, but when I change the unit types around (Fahrenheit to Celsius / Kelvin), the formatted() doesn't seem to work anymore, instead the number is displayed with 6 decimals. (see screenshot)Day 19 Challenge - Unit Converter
struct ContentView: View {
    // App may allow more types of unit conversions in future
    // For now, only 1 conversion is supported (temperature)
    enum ConversionType: Hashable, CaseIterable {
        case Temperature, Time, Length
        static let conversionTypes: [ConversionType] = [.Temperature, .Time, .Length]
    }
    @State private var conversionType = ConversionType.Temperature // temperature is default

    // populate units
    enum TemperatureUnit: Hashable, CaseIterable {
        case Celsius, Fahrenheit, Kelvin
        static let temperatureUnits: [TemperatureUnit] = [.Celsius, .Fahrenheit, .Kelvin]
    }
    enum TimeUnit: Hashable, CaseIterable {
        case Day, Hour, Minute, Second
        static let timeUnits: [TimeUnit] = [.Day, .Hour, .Minute, .Second]
    }

    @State private var fromUnit = TemperatureUnit.Celsius  // celsius is default input
    @State private var toUnit = TemperatureUnit.Fahrenheit  // Fahrenheit is default output

    @State private var inputNumber = 0.0

    // return the computed answer (converted value)
    var outputNumber: Double {
        var baseValue = 0.0
        var convertedValue = 0.0

        // convert to a base value first
        switch fromUnit {
        case .Celsius: baseValue = inputNumber
        case .Fahrenheit: baseValue = (inputNumber - 32) * 5 / 9
        case .Kelvin: baseValue = inputNumber - 273.15
        }

        // next, convert from base value to desired unit
        switch toUnit {
        case .Celsius: convertedValue = baseValue
        case .Fahrenheit: convertedValue = (baseValue * 9 / 5) + 32
        case .Kelvin: convertedValue = baseValue + 273.15
        }

        // return the converted value as outputNumber
        return convertedValue

    }

    var body: some View {
        NavigationView {
            Form {
                // select a conversion type
                Section {
                    Picker("Type", selection: $conversionType) {
                        ForEach(ConversionType.allCases, id: \.self) { conversiontype in
                            Text(verbatim: "\(conversiontype)")
                                .tag(conversiontype)
                        }
                    } .pickerStyle(.segmented)
                } header: {
                    Text("Select")
                }

                // display the main conversion box
                Section {
                    HStack {
                        // input a number to convert from
                        // & select a unit of measurement
                        VStack {
                            TextField("Input Number", value: $inputNumber, format: .number)
                                .textFieldStyle(.roundedBorder).font(.title)
                            Picker("Input Temperature Unit", selection: $fromUnit) {
                                ForEach(TemperatureUnit.allCases, id: \.self) {temperatureunit in
                                    Text(verbatim: "\(temperatureunit)").tag(temperatureunit)
                                }
                            }
                        }
                        // center divider
                        Text("=").font(.largeTitle)
                        // display the converted value
                        // & select unit to convert to
                        VStack {
                            Text(outputNumber.formatted()).font(.title)
                            Picker("Output Temperature Unit", selection: $toUnit) {
                                ForEach(TemperatureUnit.allCases, id: \.self) {temperatureunit in
                                    Text(verbatim: "\(temperatureunit)").tag(temperatureunit)
                                }
                            }
                        }
                    }

                } header: {
                    Text("Conversion")
                }
                .labelsHidden() // hide labels from all pickers in this HStack
                .navigationTitle("Unit Converter")

            }

        }

    }
}

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!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.