咱先把事儿说透:你想知道SwiftUI里ViewModel该咋监听才不翻车,还得避开那些坑——这事儿就像给你家找了个“智能管家”,管得好,UI顺风顺水;管不好,要么UI抽风式刷新,要么管家赖着不走霍霍内存,主打一个“又菜又爱玩”!
先给核心结论:ViewModel监听的核心是「该传的传、该听的听、该放的放」,踩坑基本都是没搞懂这仨事儿,咱用大白话+段子给你唠明白~
一、ViewModel监听的“黄金原则”(管家的职业操守)
把ViewModel比作你家“管家”,UI是“主人”,监听就是“管家跟主人传话”,这4条原则能让你家“家政系统”稳如老狗:
原则1:管家只传“主人需要知道的话”(@Published 精准标记)
-
✅ 正经操作:只有「需要让UI刷新」的状态,才用
@Published标记(比如按钮是否可点、列表数据、加载状态); -
🤡 反面教材:把定时器计数、临时计算值也标
@Published——管家屁大点事都喊主人,UI跟打了鸡血似的疯狂刷新,卡到主人想摔手机; -
举个栗子:
-
class靠谱管家ViewModel: ObservableObject { @Published var 今日买菜钱: Int = 0 // UI要显示,该传 var 管家偷偷记的小账本: Int = 0 // 主人不用看,别传! }
-
原则2:主人只认“专属管家”(@StateObject 别用错)
-
✅ 正经操作:View里用
@StateObject绑定ViewModel——这是“终身制管家”,View生它生、View死它死,绝不乱换人; -
🤡 反面教材:用
@ObservedObject当“专属管家”,或者在body里创建ViewModel——相当于主人天天换管家,昨天记的买菜钱,今天新管家啥都不知道,状态直接丢光; -
灵魂对比:
-
struct 靠谱主人View: View { // ✅ 专属管家:View活一天,管家干一天 @StateObject private var 管家 = 靠谱管家ViewModel() // ❌ 临时临时工:body一刷新,管家就下岗(千万别这么写!) // @ObservedObject private var 管家 = 靠谱管家ViewModel() var body: some View { /* 省略 */ } }
-
原则3:别把管家“绑死”([weak self] 保命符)
-
✅ 正经操作:ViewModel里的闭包(定时器、网络请求)必须用
[weak self]——相当于跟管家说“你干完活就走,别赖在我家”; -
🤡 反面教材:闭包直接用
self——管家抱着主人的腿不撒手,哪怕View都删了,管家还在内存里蹲坑,内存泄漏+耗电,主打一个“死缠烂打”; -
救命示例:
-
extension 靠谱管家ViewModel { func 开始买菜倒计时() { // ✅ 弱引用:干完活就溜,不粘人 Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in guard let self = self else { return } // 先确认管家还在 self.今日买菜钱 -= 1 } // ❌ 强引用:管家赖着不走,内存直接炸 // Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in // self.今日买菜钱 -= 1 // 绑死了! // } } }
-
原则4:管家传话要“走正门”(MainActor 切主线程)
-
✅ 正经操作:异步任务(网络请求、后台计算)更新
@Published状态时,必须切到主线程——相当于管家从后门买完菜,得走正门把菜交给主人; -
🤡 反面教材:后台直接改状态——管家从窗户扔菜,主人(UI)根本接不着,状态改了UI却纹丝不动,主打一个“对牛弹琴”;
-
正确示范:
-
extension 靠谱管家ViewModel { func 远程买菜() { Task { // 后台买菜(异步任务) let 菜钱 = try await 菜市场API.查价格() // ✅ 走正门:切主线程交差 await MainActor.run { self.今日买菜钱 = 菜钱 } } } }
-
二、踩坑名场面(管家翻车实录)
咱盘点几个新手最容易掉的坑,个个都是“血的教训”:
坑1:啥都@Published,UI像打了鸡血
- 🕳️ 症状:UI毫无征兆疯狂刷新,甚至卡顿;
- 🎯 原因:把非UI相关的状态(比如定时器计数、临时缓存)也标了
@Published,body反复计算; - ✨ 解药:只给“UI需要响应”的状态加
@Published,其他状态用普通变量。
坑2:@ObservedObject用成“专属管家”
- 🕳️ 症状:状态莫名其妙重置(比如输入一半的文字没了);
- 🎯 原因:View的
body刷新时,@ObservedObject会重新创建ViewModel,相当于管家直接跑路; - ✨ 解药:View内部持有的ViewModel,一律用
@StateObject;只有父View传给子View的ViewModel,才用@ObservedObject。
坑3:闭包强引用,管家“躺平不离职”
- 🕳️ 症状:ViewModel的
deinit永远不执行,定时器/网络请求关不掉; - 🎯 原因:闭包没加
[weak self],循环引用导致ViewModel无法销毁; - ✨ 解药:所有闭包(Timer、URLSession、Combine)都加
[weak self],并在deinit里清理资源。
坑4:监听了个寂寞(onChange没开initial)
-
🕳️ 症状:首次加载的状态不触发
onChange,以为监听失效; -
🎯 原因:
onChange(of:)默认不监听初始值,只有状态变化时才触发; -
✨ 解药:需要监听初始值就加
initial: true:-
.onChange(of: 管家.今日买菜钱, initial: true) { 旧钱, 新钱 in print("初始/变化后的菜钱:(新钱)") // 首次加载也能触发 }
-
坑5:手动监听@Published,多此一举
-
🕳️ 症状:自己写
sink监听@Published,还忘了取消订阅; -
🎯 原因:SwiftUI会自动监听
@Published,手动监听反而容易漏取消,导致内存泄漏; -
✨ 解药:非必要不手动监听
@Published,真要监听就把订阅存到cancellables里:-
class 靠谱管家ViewModel: ObservableObject { @Published var 今日买菜钱: Int = 0 private var cancellables = Set<AnyCancellable>() init() { // 手动监听(非必要别写) $今日买菜钱 .sink { 新钱 in print("菜钱变了:(新钱)") } .store(in: &cancellables) // 必须存起来,不然订阅会丢 } }
-
三、完整避坑示例(靠谱管家+主人组合)
import SwiftUI
// 靠谱管家(ViewModel)
class ShoppingViewModel: ObservableObject {
// ✅ 只给UI需要的状态加@Published
@Published var money: Int = 0
@Published var isLoading: Bool = false
// ❌ 非UI状态:普通变量
private var timer: Timer?
private var cancellables = Set<AnyCancellable>()
// 初始化:干活前先立规矩
init() {
print("管家上岗了~")
}
// 异步买菜:走正门交差
func fetchMoney() {
isLoading = true
Task {
// 后台查价格
let newMoney = try await fetchFromAPI()
// ✅ 切主线程更新状态
await MainActor.run {
self.money = newMoney
self.isLoading = false
}
}
}
// 倒计时:弱引用不粘人
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.money -= 1
}
}
// 离职前清场子
func cleanUp() {
timer?.invalidate()
cancellables.removeAll()
print("管家清完场子了~")
}
// 销毁:体面离职
deinit {
cleanUp()
print("管家下班啦~")
}
// 模拟网络请求
private func fetchFromAPI() async throws -> Int {
try await Task.sleep(nanoseconds: 1_000_000_000)
return Int.random(in: 10...100)
}
}
// 靠谱主人(View)
struct ShoppingView: View {
// ✅ 专属管家:View生它生,View死它死
@StateObject private var vm = ShoppingViewModel()
var body: some View {
VStack(spacing: 20) {
Text("今日买菜钱:(vm.money)元")
.font(.title)
if vm.isLoading {
ProgressView()
}
Button("查菜价") {
vm.fetchMoney()
}
Button("开始倒计时") {
vm.startTimer()
}
}
// 监听状态:包含初始值
.onChange(of: vm.money, initial: true) { old, new in
print("菜钱更新:(old) → (new)")
}
// 页面消失:提醒管家清场子
.onDisappear {
vm.cleanUp()
}
}
}
四、总结(核心要点顺口溜)
- @Published要精简:只标UI需响应的状态,别瞎标;
- StateObject是本命:View内部 viewModel 用它,不重置;
- 闭包必加weak self:别绑死管家,内存不翻车;
- 异步更新切主线:管家走正门,UI才接得着;
- 销毁记得清资源:定时器/订阅要取消,体面离职。
说白了,SwiftUI ViewModel监听的核心就是“精准、不粘、合规”——管家该干啥干啥,不该干的别瞎掺和,你家的UI就能安安稳稳,再也不翻车~