How do I create optional parameters for Published property wrappers while using MVVM architecture?

438 Views Asked by At

In my pursuit to learn SwiftUI, I am really struggling with this MVVM architecture. With that being said, I have a project which I have been working on to learn this. With some help of other folks I have gotten this far.

The current issue is, I need to make the @Published property wrapped on the View Model to have optional parameters. Unfortunately, everything I try is not working. What I am looking to do is take what the user inputs on the TextFields and use them in math functions.

Model.swift

// Model.
protocol Height {
    var heightInCentimeters: Int { get }
}

struct ImperialHeight: Height {
    var feet: Int
    var inches: Int
    
    var heightInCentimeters: Int {
        Int(((Double(feet) * 12.0)) + Double(inches) * 2.54)
    }
}

struct MetricHeight: Height {
    var heightInCentimeters: Int
}

protocol Weight {
    var weightInKilograms: Int { get }
}

struct ImperialWeight: Weight {
    var pounds: Int
    
    var weightInKilograms: Int {
        Int(Double(pounds) * 0.4535924)
    }
}

struct MetricWeight {
    var weightInKilograms: Int
}
struct UserData {
    var age: Int = 18
    var weight: Weight
    var height: Height
}

View Model

class UserDataViewModel: ObservableObject {
    
    @Published var userData: UserData = UserData(weight: ImperialWeight(pounds: 220), height: ImperialHeight(feet: 6, inches: 2))
...

As stated above, the current issue is that the @Published property wrapper requires the weight and height. Ideally I would like to make those optionals which populates that data via users TextField input.

I've tried reading numerous documentations, tutorials and guides. Unfortunately I'm failing to grasp the concept still in terms of how to apply it with my project.

1

There are 1 best solutions below

0
On

I think you are over complicating it a little. You can use Measurement to do most of the work for you. If you want to have different ways editing a measurement just have a single source of truth. Below is an example with height

import SwiftUI
//The key for something like this is to save everything to one variable in this case I will use centimeters as the source of truth
struct Height{
    ///Source of truth
    var centimeters: Double = 0
    ///This will have a multitude of uses and has its on Formatter for when you want to display units based on locale, etc
    var heightInCentimeters: Measurement<UnitLength> {
        get{
            Measurement(value: centimeters, unit: .centimeters)
        }
        //A measurement of any UnitLength kind will be converted to cm
        //Make sure you use the given units and you dont create new ones or you will get zero
        set{
            centimeters = newValue.converted(to: .centimeters).value
        }
    }
    /// centimeters converted to feet using Swift.Measurement
    var feet: Double {
        get{
            return heightInCentimeters.converted(to: .feet).value
        }
        set{
            heightInCentimeters = Measurement(value: newValue, unit: UnitLength.feet)
        }
    }
    /// centimeters converted to feet and inches. They depend on each other like this
    var feetAndInches: (feet:Int, inches:Int){
        get{
            let feetDouble = heightInCentimeters.converted(to: .feet).value
            //figure out the decimal
            let feetForInches = feetDouble.truncatingRemainder(dividingBy: 1)
            //remove the decimal
            let feet = feetDouble - feetForInches
            //convert decimal to inches
            let inches = Measurement(value: feetForInches, unit: UnitLength.feet).converted(to: .inches).value
            //return whole numbers
            return (Int(feet), Int(inches))
        }
        set(newValue){
            let ftCM = Measurement(value: Double(newValue.feet), unit: UnitLength.feet).converted(to: .centimeters).value
            let inCM = Measurement(value: Double(newValue.inches), unit: UnitLength.inches).converted(to: .centimeters).value
            centimeters = ftCM + inCM
        }
    }
}

struct UserData {
    var age: Int = 18
    var height: Height
}

class UserDataViewModel: ObservableObject {
    
    @Published var userData: UserData = UserData(height: Height())
}
struct UserDataView: View {
    @StateObject var vm: UserDataViewModel = UserDataViewModel()
    var numFormatter: NumberFormatter{
        let format = NumberFormatter()
        format.maximumFractionDigits = 2
        return format
    }
    var body: some View {
        //Notice that TextField with formatter only "saves" the new value when the user presses return/done on the keyboard
        List{
            Section(header: Text("age"), content: {
                HStack{
                    Text("age")
                    TextField("age", value: $vm.userData.age, formatter: numFormatter)
                }
            })
            Section(header: Text("height = \(vm.userData.height.heightInCentimeters.description)"), content: {
                HStack{
                    Text("feet")
                    TextField("feet", value: $vm.userData.height.feet, formatter: numFormatter)
                }
                VStack{
                    HStack{
                        Text("feet")
                        TextField("feet", value: $vm.userData.height.feetAndInches.feet, formatter: numFormatter)
                    }
                    HStack{
                        Text("inches")
                        TextField("inches", value: $vm.userData.height.feetAndInches.inches, formatter: numFormatter)
                    }
                }
                HStack{
                    Text("centimeters")
                    TextField("centimeters", value: $vm.userData.height.centimeters, formatter: numFormatter)
                }
            })
        }
    }
}

struct UserDataView_Previews: PreviewProvider {
    static var previews: some View {
        UserDataView()
    }
}

And this code can be for any length. Not just height.