唠唠SwiftUI ViewModel监听:别让你的“管家”瞎忙活!

7 阅读6分钟

咱先把事儿说透:你想知道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()
        }
    }
}

四、总结(核心要点顺口溜)

  1. @Published要精简:只标UI需响应的状态,别瞎标;
  2. StateObject是本命:View内部 viewModel 用它,不重置;
  3. 闭包必加weak self:别绑死管家,内存不翻车;
  4. 异步更新切主线:管家走正门,UI才接得着;
  5. 销毁记得清资源:定时器/订阅要取消,体面离职。

说白了,SwiftUI ViewModel监听的核心就是“精准、不粘、合规”——管家该干啥干啥,不该干的别瞎掺和,你家的UI就能安安稳稳,再也不翻车~