0

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

1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

thanks @NewDev - this actually didn't work? However, what does work is marking these properties as Published: @ Published var myMonthlyIncome : String @ Published var housemateMonthlyIncome : String @ Published var totalRent : String
Is your code different than what you posted? myMonthlyIncome, housemateMonthlyIncome are computed properties - you can't use a property wrapper with computed properties

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.