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
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 viaself.objectWillChange
publisher, and the other - more common - is through its@Published
properties that, when changed, use theobjectWillChange
automatically.So, in your case, all you need to is mark
amount
property as@Published
. Because it's astruct
- a value-type - any change to its properties also changes the whole object:Because the computed property
yourShare
is only ever updated whenamount
is updated, this would just work. The view would recompute itself with the now-updatedyourShare
.