Swift无Storyboard:实现表单用户输入对ViewModel的可观察性
实现表单输入对ViewModel的可观察性方案
核心逻辑很简单:把表单的所有输入状态都交给ViewModel托管,View只做UI展示和双向绑定,这样ViewModel就能直接拿到实时的输入数据,不用反过来观察View。具体步骤如下:
1. 让EarningsViewModel成为ObservableObject,定义表单相关的@Published属性
把需要的出生日期、入职日期、退休年龄这些数据,作为ViewModel的@Published属性,这样属性变化时会自动发送通知,View能同步UI,ViewModel也能随时获取最新值。
import Foundation import SwiftUI class EarningsViewModel: ObservableObject { // 表单输入的状态属性 @Published var birthDate: Date = Date() @Published var hireDate: Date = Date() @Published var retirementAge: Int = 60 // 基于表单数据的实时计算属性 var yearsUntilRetirement: Int { let calendar = Calendar.current let ageNow = calendar.dateComponents([.year], from: birthDate, to: Date()).year ?? 0 return max(0, retirementAge - ageNow) } // 触发计算的方法 func calculateEarnings() -> Double { let calendar = Calendar.current let workYears = calendar.dateComponents([.year], from: hireDate, to: Date()).year ?? 0 return Double(workYears) * 1200.0 } }
2. 在ProfileFormView中绑定ViewModel的属性
在View中通过@StateObject(View内部创建ViewModel时用)或者@ObservedObject(从父View传入ViewModel时用)持有实例,然后把日期选择器、选择器和ViewModel的属性做双向绑定(用$前缀)。
struct ProfileFormView: View { // 内部创建ViewModel用@StateObject @StateObject private var viewModel = EarningsViewModel() // 父View传入则用@ObservedObject // @ObservedObject var viewModel: EarningsViewModel var body: some View { Form { Section(header: Text("个人信息")) { DatePicker("出生日期", selection: $viewModel.birthDate, displayedComponents: .date) DatePicker("入职日期", selection: $viewModel.hireDate, displayedComponents: .date) } Section(header: Text("退休设置")) { Picker("退休年龄", selection: $viewModel.retirementAge) { ForEach(55...65, id: \.self) { age in Text("\(age)岁").tag(age) } } } Section(header: Text("计算结果")) { Text("距离退休还有 \(viewModel.yearsUntilRetirement) 年") Button("计算收入") { let earnings = viewModel.calculateEarnings() print("收入计算结果:\(earnings)") } } } } }
3. 自动响应数据变化触发计算
如果需要在表单数据变化时自动触发计算,不用手动点按钮,可以用.onChange修饰符监听ViewModel的属性变化:
struct ProfileFormView: View { @StateObject private var viewModel = EarningsViewModel() var body: some View { Form { // 上面的表单内容... } .onChange(of: viewModel.birthDate) { _ in viewModel.updateCalculations() } .onChange(of: viewModel.hireDate) { _ in viewModel.updateCalculations() } .onChange(of: viewModel.retirementAge) { _ in viewModel.updateCalculations() } } } // 给EarningsViewModel扩展计算方法 extension EarningsViewModel { @Published var calculatedEarnings: Double = 0.0 func updateCalculations() { calculatedEarnings = calculateEarnings() } }
可选:拆分状态到专门的表单ViewModel
如果EarningsViewModel职责过重,可单独创建ProfileFormViewModel托管表单输入,让EarningsViewModel观察它的变化,符合单一职责原则:
class ProfileFormViewModel: ObservableObject { @Published var birthDate: Date = Date() @Published var hireDate: Date = Date() @Published var retirementAge: Int = 60 } class EarningsViewModel: ObservableObject { private let formViewModel: ProfileFormViewModel @Published var calculatedEarnings: Double = 0.0 private var cancellables = Set<AnyCancellable>() init(formViewModel: ProfileFormViewModel) { self.formViewModel = formViewModel // 监听表单属性变化自动计算 formViewModel.$birthDate .sink { [weak self] _ in self?.updateEarnings() } .store(in: &cancellables) formViewModel.$hireDate .sink { [weak self] _ in self?.updateEarnings() } .store(in: &cancellables) formViewModel.$retirementAge .sink { [weak self] _ in self?.updateEarnings() } .store(in: &cancellables) } private func updateEarnings() { let calendar = Calendar.current let workYears = calendar.dateComponents([.year], from: formViewModel.hireDate, to: Date()).year ?? 0 calculatedEarnings = Double(workYears) * 1200.0 } } // View中使用 struct ProfileFormView: View { @StateObject private var formViewModel = ProfileFormViewModel() @StateObject private var earningsViewModel: EarningsViewModel init() { let formVM = ProfileFormViewModel() _earningsViewModel = StateObject(wrappedValue: EarningsViewModel(formViewModel: formVM)) _formViewModel = StateObject(wrappedValue: formVM) } var body: some View { Form { DatePicker("出生日期", selection: $formViewModel.birthDate, displayedComponents: .date) DatePicker("入职日期", selection: $formViewModel.hireDate, displayedComponents: .date) Picker("退休年龄", selection: $formViewModel.retirementAge) { ForEach(55...65, id: \.self) { age in Text("\(age)岁").tag(age) } } Text("自动计算收入:\(earningsViewModel.calculatedEarnings)") } } }
内容的提问来源于stack exchange,提问作者weekendwarrior
相关产品推荐
相关产品推荐

