SwiftUI Комбинировать, используя модели и ViewModels вместе

#swift #mvvm #swiftui #combine

#swift #mvvm #swiftui #объединить

Вопрос:

Я очень новичок в Swift, и в настоящее время я пытаюсь учиться, создавая приложение для разделения аренды с помощью SwiftUI Combine. Я хочу следовать шаблону MVVM и пытаюсь это реализовать. На данный момент у меня есть следующая модель, ViewModel и файлы просмотра:

Модель:

 import Foundation
import Combine

struct InputAmounts {
    var myMonthlyIncome : Double
    var housemateMonthlyIncome : Double
    var totalRent : Double
}
  

ViewModel (где я попытался использовать данные из модели для соответствия шаблону MVVM, но я не уверен, что сделал это самым чистым / правильным способом, поэтому, пожалуйста, поправьте меня, если я ошибаюсь)

 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)
    }
}
  

И затем я пытаюсь передать все это в представление:

 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)
    }
}

  

Я получаю ошибки сборки с представлением:
«Значение типа ‘ObservedObject.Оболочка ‘не имеет динамического члена ‘totalRent’, используя путь к ключу из корневого типа ‘FairRentViewModel'»

«Значение типа ‘ObservedObject.Оболочка ‘не имеет динамического члена ‘myMonthlyIncome’, используя путь к ключу из корневого типа ‘FairRentViewModel'»

«Значение типа ‘ObservedObject.Оболочка ‘не имеет динамического члена ‘housemateMonthlyIncome’, используя путь к ключу из корневого типа ‘FairRentViewModel'»

Мои вопросы:

  • Что означает эта ошибка и, пожалуйста, укажите мне правильное направление для решения?
  • Я пошел совершенно неправильным путем, пытаясь реализовать шаблон MVVM здесь?

Как я уже сказал, я новичок в Swift, просто пытаюсь учиться, поэтому буду признателен за любые советы.

ОБНОВЛЕНИЕ В ОТВЕТ НА ОТВЕТ

   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) ?? ""
    }
  

Я получаю ошибки здесь, что «Значение необязательного типа ‘Double?’ должно быть развернуто до значения типа’Double'», которое, как я думал, я достигал с помощью?? операнды?

Комментарии:

1. Похоже, вы пытаетесь получить доступ $viewModel.totalRent , но у вас viewModel нет никакого свойства totalRent . Я думаю, что вам может понадобиться $viewModel.inputAmounts.totalRent , если вы это сделаете, вам нужно удалить private from inputAmounts в ViewModel

2. Прежде всего, нет необходимости приводить Double к Double и никогда не делать ?? 0 этого при выполнении деления.

Ответ №1:

Ваша модель представления должна иметь свойства для каждого из свойств модели, с которыми вы хотите работать в представлении, и модель представления также должна отвечать за их преобразование в формат, подходящий для представления. Свойства должны быть помечены как @Published to, чтобы представление обновлялось при их изменении.

Например

 @Published var myMonthlyIncome: String
  

и это, как вы видите, строка, и мы можем преобразовать ее в init

 myMonthlyIncome = FairRentViewModel.formatter.string(for: inputAmounts.myMonthlyIncome) ?? ""
  

Вот моя полная версия модели представления

 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
        //...
    }
}
  

Вы должны сделать что-то подобное для yourShare и I save метод — это всего лишь простой пример того, как обновить модель из представления, если вы привязываете функцию к действию кнопки или аналогичному.

Также обратите внимание, что числовой стиль, который я использовал для форматирования, — это всего лишь предположение, возможно, вам придется это изменить. И при работе с деньгами рекомендуется работать с десятичной, а не с двойной.

Комментарии:

1. Спасибо @JoakimDanielson — означает ли это, что мне не нужен отдельный файл только для моделей?

2. Это действительно зависит от вас, но лично я предпочитаю иметь отдельные файлы для каждой из моих моделей (и моделей просмотра и представлений)

3. Спасибо @JoakimDanielson. Что вы подразумеваете под «сделать что-то подобное для yourShare «?

4. Превратить его в строку вместо Double

5. Извините, @JoakimDanielson, я все еще не понимаю, как это сделать — не могли бы вы обновить свой ответ, чтобы привести пример того, что вы имеете в виду, пожалуйста?