You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 15:30:53