返回 / Index

2026·06·21 · 阅读约 9 分钟 · Technical

从徒手刷 UI 到一个 @Observable:移动开发状态绑定的演进

更新于

动画版 · 把这一路的演进做成了可视化时间线,可以边看边读

「前情提要」

说起来惭愧,转行做软件开发,今年是第12个年头了。以前面试的时候回答不上技术问题,总以自己不是计算机科班出身为借口。但是,12年过去,大学都够读3回了,「不是计算机科班出身」,渐变成不太靠谱的借口。

如果硬是要甩锅,或许可以这样解释,我是一个「视觉学习者」,面对浩如烟海、每年更新的技术文档,我的大脑理所当然地拒绝,我是可以理解——甚至有点同情我的大脑。加之我们的教育并没有教我们「Learning How to Learn」,所以12年间「学习」过的技术知识,如水过鸭背。

不过到了AI时代,事情发生了微妙的变化,以NotebookLM为代表,AI可以轻易把「枯燥」的文字,转换为生动的音视频,让学习者轻松完成第一次的吸收;遇到问题,也可以随时随地「请教」大模型——一个集人类知识之大成,(理论上)全知全能的老师,当然,「幻觉」问题暂时按住不表,大方向已定——人类离不开大模型。

正因为「知识」触手可及、前所未有地廉价——一台手机+一个Wi-Fi,可以获取任何「知识」,「学习」「知识」变得不那么重要。所以,在AI时代,「学习」在某种程度上变成一种兴趣,可以演变为一种纯粹为了获得多巴胺的行为,就像健身一样——你健身的目的,并不是为了有朝一日可以像武松一样徒手打虎,纯粹是为了满足自己——获得多巴胺。


跑题了,言归正传,这次想小结一下接触「移动开发」以来的一些历史,从iOS OC时代的徒手写UI、数据更新后更新UI,到iOS最新的Observation,用一个@Observable宏就搞定一切,这一路下来,是如何演进的。

之前看objc的《App Architecture》,说App的本质,就是UI + Model,UI发出动作,触发Model的更新,Model更新后,又触发UI的更新,如此循环往复,App就是这样一直运行。

Applications Are a Feedback Loop

最近的项目,是用Flutter做的,用到Riverpod,第一次接触;而且Riverpod从1.0一路演进到现在的3.0,公司项目里新旧写法混在一起,一时之间无从下手。于是想重新捋一遍——又因为最熟悉的还是iOS,索性顺便做了对比。

命令式与手动观察者年代/The Imperative Era

2008年起,iPhone刚发布没多久,移动开发还处在原始的「命令式」与「手动观察」年代,写UI,需要明确(命令式)写出UI(的每个细节);触发动作后,也要手动更新UI,不可谓不原始。

- (void)didTapMultiply {
    // 命令式数据流:Controller 主动调用 Model,拿到结果后**亲手**刷新 View。
    // 对照Swift最新的 @Observable 版:那边只需 `viewModel.multiply()`,result 变化由框架自动驱动 UI。
    __weak typeof(self) weakSelf = self;
    [self.model multiplyWithCompletion:^(NSString *result) {
        weakSelf.resultLabel.text = result; // ← 手动刷新 UI,这一行是 MVC 的灵魂也是负担
    }];
}

如果要实现监听Model变化,自动更新UI,需要手动实现,通过KVO或者notification:

    // 手动实现监听
    // ★ 订阅:监听 model.result 的变化(对照 SwiftUI body 里读 viewModel.result 自动建立的订阅)。
    [self.model addObserver:self
                 forKeyPath:@"result"
                    options:NSKeyValueObservingOptionNew
                    context:kResultContext];

    // Model有变化,自动回调,从而(自动)更新UI
    // ★ KVO 回调:result 一变就自动进到这里,我们刷新 UI。
    - (void)observeValueForKeyPath:(NSString *)keyPath
                        ofObject:(id)object
                            change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                        context:(void *)context {
        if (context == kResultContext) {
            self.resultLabel.text = change[NSKeyValueChangeNewKey];
        } else {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }

    // 发出「动作」,触发Model更新
    - (void)didTapMultiply {
        [self.model multiply]; // 只触发;结果回来后由 KVO 自动刷新(无需手动设 label)
    }

    - (void)dealloc {
    // 需要手动管理的年代,管理不当,会内存泄漏
    // ★ 必须退订,否则 model 通知一个已释放的 observer 会崩溃(KVO 的经典坑)。
    //   对照 @Observable / SwiftUI:订阅生命周期由框架自动管理,根本无需手写这步。
    [self.model removeObserver:self forKeyPath:@"result" context:kResultContext];
}

后来演进到2012年的ReactiveCocoa——一个开创性第三方框架,把函数式响应式编程(FRP)带入iOS,启发了后来的一切。再演进到2015年的RxSwift,因为是第三方框架,这里掠过不表。

小结:这一时期,UI需要用代码一步步”指挥”出来。状态变化靠手动订阅 / 移除监听器、委托(delegate)和回调传递。最大隐患:忘记移除监听导致内存泄漏。第三方库开始引入”响应式”概念探路。

生命周期感知与响应式框架/Reactive & Lifecycle-Aware

到2019年,Apple推出自家的声明式UI框架SwiftUI,还有响应式框架Combine。人们可以用「ObservableObject / @Published」来实现「属性变化自动驱动视图重建」。


final class MultiplyViewModel: ObservableObject {
    // ObservableObject 搭配 @Published,实现「属性变化自动驱动视图重建」/「属性可观察」
    @Published private(set) var result = "-"
    private let useCase: MultiplyUseCase

    init(useCase: MultiplyUseCase) {
        self.useCase = useCase
    }

    func multiply() async {
        do {
            result = try await useCase.multiply() // 赋值 → objectWillChange → 观察者重绘
        } catch {
            debugPrint("Error: \(error)")
        }
    }
}

// ViewModel的「注入」方式有三种
// 1 当前视图自己init
// 传递: 这一屏是根/拥有者
@StateObject private var viewModel = MultiplyViewModel(useCase: 实例化参数)

// 2 由父视图传入(在父视图init)
// 传递: 显式逐层传递
@ObservedObject var viewModel: MultiplyViewModel

// 3 上游某处init(通过.environmentObject(...) 注入)
// 传递: 跨多层共享(优势)
@EnvironmentObject private var viewModel: MultiplyViewModel

// UI直接使用即可
Text(viewModel.result)

BTW,真的忍不住吐槽写这个的Apple Engineer,「ObservableObject」、「@Published」、「@StateObject」、「@State」、「@ObservedObject」、「@EnvironmentObject」、「@Bindable」,用的人心智负担得有多大?这绝对是失败的设计,后面的「Observation Framework」出现便是例证。 冷知识:SwiftUI一开始只有「@ObservedObject」,没有「@StateObject」,后者是为了填坑(视图重建,「@ObservedObject」会让状态丢失)在2020年才加的。

而同时期,Flutter那边发生的事情:

小结:这一时期,官方开始提供「会自动管理订阅生命周期」的状态容器,彻底缓解了泄漏问题。

细粒度与现代化

时间先跳到2023,Apple演进出Observation Framework

// 「@Observable」 is all you need, 不再需要「ObservableObject」「@Published」
@Observable
final class MultiplyViewModel {
    private(set) var result = "-"
    private let useCase: MultiplyUseCase

    init(useCase: MultiplyUseCase) {
        self.useCase = useCase
    }

    func multiply() async {
        do {
            result = try await useCase.multiply()
        } catch {
            debugPrint("Error: \(error)")
        }
    }
}

// 「注入」端也改为
// 1 自己拥有
@State private var viewModel = MultiplyViewModel(useCase: 实例化参数)

// 2 外部传入,直接 let 
let viewModel: MultiplyViewModel

// 3 
@Environment(MultiplyViewModel.self) private var viewModel

// 另外,需要双向 binding 时 → @Bindable

Observation Framework让开发者的心智负担大大降低。

这样的演进,是为了解决几个痛点

class UserModel: ObservableObject {
    @Published var name = ""
    @Published var age = 0
}

struct NameView: View {
    @ObservedObject var user: UserModel
    var body: some View {
        Text(user.name)   // 只用了 name,不过如果改user的age,也会触发不必要的重绘
    }
}

// @Observable 改成了属性级追踪:SwiftUI 在求值 body 的过程中,记录下这次到底访问了哪些属性,然后只在这些属性变化时才让这个 view 失效。
// 上面的例子换成 @Observable 后,age 变化对 NameView 完全没有影响。

而最新的2026年WWDC,没有关于Observation Framework的大更新,唯一相关的是「@State」从原来的「property wrapper」变成「macro」

而同时期——甚至早于Swift,Flutter那边发生的事情:

小结:这一时期,目标是只重建真正依赖了变化数据的那一小块UI,减少样板代码。

附上Riverpod3个版本的变迁简介

// 1.0版本写法: 手写,没有代码生成
final randomNumberServiceProvider = Provider<RandomNumberService>(
  (ref) => RandomNumberGrpcService(),
);

// 依赖靠 ref.watch 串起来
final randomNumberRepositoryProvider = Provider<RandomNumberRepository>(
  (ref) => RandomNumberRepositoryImpl(
    service: ref.watch(randomNumberServiceProvider),
  ),
);
// 用的时候
final result = ref.watch(multiplyViewModelProvider);
final viewModel = ref.read(multiplyViewModelProvider.notifier);


// 2.0版本写法: 自动生成代码
@riverpod
RandomNumberService randomNumberService(RandomNumberServiceRef ref) =>
    RandomNumberGrpcService();

@riverpod
RandomNumberRepository randomNumberRepository(RandomNumberRepositoryRef ref) =>
    RandomNumberRepositoryImpl(service: ref.watch(randomNumberServiceProvider));

// 用的时候,和1.0一样


// 3.0版本写法
// 「Ref ref」写法和1.0版本的一样(改回去了)
@riverpod
RandomNumberService randomNumberService(Ref ref) => RandomNumberGrpcService();

@riverpod
RandomNumberRepository randomNumberRepository(Ref ref) =>
    RandomNumberRepositoryImpl(service: ref.watch(randomNumberServiceProvider));

一张图,看完这一路

iOS 与 Flutter 两条赛道,起点不同、节奏也不同,却不约而同地朝着同一个方向演进——从「命令式 / 手动观察」,到「生命周期感知 / 响应式」,再到「细粒度 / 声明式」。中间那几条虚线串起的,正是两边各自跨过同一道门槛的时刻。

flowchart LR
    classDef ios fill:#E3F2FD,stroke:#1565C0,color:#0D2A4A;
    classDef flutter fill:#E8F5E9,stroke:#2E7D32,color:#16401A;
    classDef era fill:#FFF8E1,stroke:#C0392B,color:#7A2418,font-weight:bold;

    E1["命令式 / 手动观察<br/>The Imperative Era"]:::era
    E2["生命周期感知 / 响应式<br/>Reactive & Lifecycle-Aware"]:::era
    E3["细粒度 / 声明式<br/>Fine-grained & Declarative"]:::era
    E1 --> E2 --> E3

    subgraph IOS ["iOS 轨道"]
        direction LR
        I1["2008<br/>命令式 UI<br/>手动 KVO / Notification"]:::ios
        I2["2012<br/>ReactiveCocoa<br/>FRP 函数式响应式"]:::ios
        I3["2015<br/>RxSwift"]:::ios
        I4["2019<br/>SwiftUI + Combine<br/>ObservableObject / @Published"]:::ios
        I5["2023<br/>Observation<br/>@Observable 属性级细粒度追踪"]:::ios
        I6["2026<br/>@State 由 property wrapper<br/>升级为 macro"]:::ios
        I1 --> I2 --> I3 --> I4 --> I5 --> I6
    end

    subgraph FLT ["Flutter 轨道"]
        direction LR
        F1["2018<br/>BLoC 模式<br/>基于 Stream 单向数据流"]:::flutter
        F2["2018 末<br/>Flutter 1.0 内置原语<br/>setState / InheritedWidget / ChangeNotifier"]:::flutter
        F3["2019<br/>Provider<br/>官方推荐"]:::flutter
        F4["2020<br/>Riverpod 诞生<br/>摆脱 BuildContext / 编译期安全"]:::flutter
        F5["2021.09<br/>Riverpod 1.0"]:::flutter
        F6["2022<br/>Riverpod 2.0<br/>Notifier / codegen / AsyncValue"]:::flutter
        F7["2025<br/>Riverpod 3.0<br/>接口统一 / ref.select 细粒度"]:::flutter
        F1 --> F2 --> F3 --> F4 --> F5 --> F6 --> F7
    end

    E1 -.-> I1
    E1 -.-> F1
    E2 -.-> I4
    E2 -.-> F3
    E3 -.-> I5
    E3 -.-> F7
Antony