Using Combine + SwiftUI to update values when form values are updated

1k views Asked by At

I am learning SwiftUI and Combine to make a simple rent-splitting app. I am trying to follow the MVVM pattern and therefore have a Model, ViewModel and View as follows:

Model:

import Foundation
import Combine

struct Amounts {
    var myMonthlyIncome : String = ""
    var housemateMonthlyIncome : String = ""
    var totalRent : String = ""
}

ViewModel:

import Foundation
import Combine

class FairRentViewModel : ObservableObject {
    
    var amount: Amounts
    
    init(_ amount: Amounts){
        self.amount = amount
    }
    
    var myMonthlyIncome : String { return amount.myMonthlyIncome }
    var housemateMonthlyIncome : String { return amount.housemateMonthlyIncome }
    var totalRent : String { return amount.totalRent }
    
    var yourShare: Double {
        guard let totalRent = Double(totalRent) else { return 0 }
        guard let myMonthlyIncome = Double(myMonthlyIncome) else { return 0 }
        guard let housemateMonthlyIncome = Double(housemateMonthlyIncome) else { return 0 }
        let totalIncome = Double(myMonthlyIncome + housemateMonthlyIncome)
        let percentage = myMonthlyIncome / totalIncome
        let value = Double(totalRent * percentage)

        return Double(round(100*value)/100)
    }
}

View:


import SwiftUI
import Combine

struct FairRentView: View {
    
    @ObservedObject private var viewModel: FairRentViewModel
    
    init(viewModel: FairRentViewModel){
        self.viewModel = viewModel
    }
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Enter the total monthly rent:")) {
                    TextField("Total rent", text: $viewModel.amount.totalRent)
                        .keyboardType(.decimalPad)
                }
                
                Section(header: Text("Enter your monthly income:")) {
                    TextField("Your monthly wage", text: $viewModel.amount.myMonthlyIncome)
                        .keyboardType(.decimalPad)
                }
                
                Section(header: Text("Enter your housemate's monhtly income:")) {
                    TextField("Housemate's monthly income", text: $viewModel.amount.housemateMonthlyIncome)
                        .keyboardType(.decimalPad)
                }
                Section {
                    Text("Your share: £\(viewModel.yourShare, specifier: "%.2f")")
                }
            }
            .navigationBarTitle("FairRent")
        }
    }
}

struct FairRentView_Previews: PreviewProvider {
    static var previews: some View {
        FairRentView(viewModel: FairRentViewModel(Amounts()))
    }
}

The entry point:

@main
struct FairRentCalculatorApp: App {
    var body: some Scene {
        WindowGroup {
            FairRentView(viewModel: FairRentViewModel(Amounts(myMonthlyIncome: "", housemateMonthlyIncome: "", totalRent: "")))
        }
    }
}

I want the yourShare value to update as the other properties are entered by the user in the form. This is what I have been trying to achieve with the above code. Can anyone please help point me in the right direction? I'm very new to SwiftUI + Combine and am trying my best to code cleanly so any other pointers are also welcome.

Thanks

1

There are 1 answers

2
New Dev On BEST ANSWER

You need something to signal to SwiftUI that a view needs to be updated.

ObservableObject objects have two ways to do that. One is directly via self.objectWillChange publisher, and the other - more common - is through its @Published properties that, when changed, use the objectWillChange automatically.

So, in your case, all you need to is mark amount property as @Published. Because it's a struct - a value-type - any change to its properties also changes the whole object:

@Published var amount: Amounts

Because the computed property yourShare is only ever updated when amount is updated, this would just work. The view would recompute itself with the now-updated yourShare.