问题
I'm very new to Swift and I am currently trying to learn by building a rent splitting app with SwiftUI + Combine. I want to follow the MVVM pattern and am trying to implement this. At the moment I have the following Model, ViewModel and View files:
Model:
import Foundation
import Combine
struct InputAmounts {
var myMonthlyIncome : Double
var housemateMonthlyIncome : Double
var totalRent : Double
}
ViewModel (where I have attempted to use the data from the Model to conform to the MVVM pattern, but I am not sure I have done this in the cleanest way/correct way so please correct me if wrong)
import Foundation
import Combine
class FairRentViewModel : ObservableObject {
private var inputAmounts: InputAmounts
init(inputAmounts: InputAmounts) {
self.inputAmounts = inputAmounts
}
var yourShare: Double {
inputAmounts.totalRent = Double(inputAmounts.totalRent)
inputAmounts.myMonthlyIncome = Double(inputAmounts.myMonthlyIncome)
inputAmounts.housemateMonthlyIncome = Double(inputAmounts.housemateMonthlyIncome)
let totalIncome = Double(inputAmounts.myMonthlyIncome + inputAmounts.housemateMonthlyIncome)
let percentage = Double(inputAmounts.myMonthlyIncome / totalIncome)
let value = Double(inputAmounts.totalRent * percentage)
return Double(round(100*value)/100)
}
}
And then am trying to pass this all to the 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.totalRent)
.keyboardType(.decimalPad)
}
Section(header: Text("Enter your monthly income:")) {
TextField("Your monthly wage", text: $viewModel.myMonthlyIncome)
.keyboardType(.decimalPad)
}
Section(header: Text("Enter your housemate's monthly income:")) {
TextField("Housemate's monthly income", text: $viewModel.housemateMonthlyIncome)
.keyboardType(.decimalPad)
}
Section {
Text("Your share: £\(viewModel.yourShare, specifier: "%.2f")")
}
}
.navigationBarTitle("FairRent")
}
}
}
struct FairRentView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = FairRentViewModel(inputAmounts: <#InputAmounts#>)
FairRentView(viewModel: viewModel)
}
}
I am getting the build errors with the View: "Value of type 'ObservedObject.Wrapper' has no dynamic member 'totalRent' using key path from root type 'FairRentViewModel'"
"Value of type 'ObservedObject.Wrapper' has no dynamic member 'myMonthlyIncome' using key path from root type 'FairRentViewModel'"
"Value of type 'ObservedObject.Wrapper' has no dynamic member 'housemateMonthlyIncome' using key path from root type 'FairRentViewModel'"
My questions are:
- What does this error mean and please point me in the right direction to solve?
- Have I gone completely the wrong way at trying to implement the MVVM pattern here?
As I said I am a Swift beginner just trying to learn so any advice would be appreciated.
UPDATE IN RESPONSE TO ANSWER
var yourShare: String {
inputAmounts.totalRent = (inputAmounts.totalRent)
inputAmounts.myMonthlyIncome = (inputAmounts.myMonthlyIncome)
inputAmounts.housemateMonthlyIncome = (inputAmounts.housemateMonthlyIncome)
var totalIncome = Double(inputAmounts.myMonthlyIncome) 0.00 + Double(inputAmounts.housemateMonthlyIncome) ?? 0.00
var percentage = Double(myMonthlyIncome) ?? 0.0 / Double(totalIncome) ?? 0.0
var value = (totalRent * percentage)
return FairRentViewModel.formatter.string(for: value) ?? ""
}
I am getting errors here that "Value of optional type 'Double?' must be unwrapped to a value of type 'Double'" which I thought I was achieving with the ?? operands?
回答1:
Your view model should have properties for each of the model properties you want to work with in the view and the view model should also be responsible for converting them to a format suitable for the view. The properties should be marked as @Published to so that the view gets updated if they are changed.
For example
@Published var myMonthlyIncome: String
and this is as you see a String and we can convert it in the init
myMonthlyIncome = FairRentViewModel.formatter.string(for: inputAmounts.myMonthlyIncome) ?? ""
Here is my complete version of the view model
final class FairRentViewModel : ObservableObject {
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
private var inputAmounts: InputAmounts
@Published var myMonthlyIncome: String
@Published var housemateMonthlyIncome: String
@Published var totalRent: String
init(inputAmounts: InputAmounts) {
self.inputAmounts = inputAmounts
myMonthlyIncome = FairRentViewModel.formatter.string(for: inputAmounts.myMonthlyIncome) ?? ""
housemateMonthlyIncome = FairRentViewModel.formatter.string(for: inputAmounts.housemateMonthlyIncome) ?? ""
totalRent = FairRentViewModel.formatter.string(for: inputAmounts.totalRent) ?? ""
}
var yourShare: String {
let value = inputAmounts.totalRent * inputAmounts.myMonthlyIncome / (inputAmounts.myMonthlyIncome + inputAmounts.housemateMonthlyIncome)
return FairRentViewModel.formatter.string(for: Double(round(100*value)/100)) ?? ""
}
func save() {
inputAmounts.myMonthlyIncome = FairRentViewModel.formatter.number(from: myMonthlyIncome)?.doubleValue ?? 0
//...
}
}
You should do something similar for yourShare
and I the save
method is just a simple example of how to update the model from the view if you tie the function to a Button action or similar.
Also note that the number style I used for the formatter is just a guess, you might need to change that. And it is recommended to work with Decimal instead of Double when dealing with money.
来源:https://stackoverflow.com/questions/64151269/swiftui-combine-using-models-and-viewmodels-together