采用如下图所示的MVVM架构。
Model 部分的实现细节详见 model-detail.md。
Model 部分主要进行缓存的管理,以及向服务器请求和发送数据。如上图所示,Request
进行具体的请求(查询缓存/进行HTTP请求),Request Publisher
对 Request
进行包装,并向 View Model
提供声明式的接口。
第三方依赖库:
我们为与树洞后端进行通信的类型定义了一个协议 Request
以规定接口,同时实现代码的高效复用。所有请求均可抽象成遵循此协议的类型。对于大部分请求而言,其请求过程均为请求数据 - 解析JSON - 数据转换 - 返回数据。针对这些请求,我们在 Request
的基础上规定了一个更高级的协议 DefaultRequest
,并为其请求过程进行了默认的实现,大大减少了重复代码的编写。
针对声明式代码,利用原生 Combine 框架,我们使用 Publisher 对 Request 进行包装,降低管理异步事件的难度,同时更好地与 SwiftUI 的声明式风格融合。所有 Request 均可获取其 publisher,并在收到订阅时进行请求并返回结果。
View 部分的实现细节详见 view-detail.md。
View 部分的主要难点是数据流的实现。根据设计,本应用的数据流有以下几类:
- 整个应用的数据共享
- 父 View 与子 View 绑定数据
- 父 View 作为唯一数据源
- 子 View 作为(暂时的)唯一数据源
- 父 View 向子 View 传递只读数据
由于采用了 SwiftUI 的生命周期(App Essentials in SwiftUI),我们定义一个表示应用状态的类型 AppModel
,在 HollowApp
中监测其变化,在其他位置通过修改单例实现数据共享。
当子 View 需要修改数据源(而本身并不拥有数据)时,用 @Binding
进行数据绑定;否则直接进行值的传递,此时父 View 中的数据对于子 View 来说是只读的。
有些情况下,子 View 需要更新父 View 中的数据,然而父 View 本身在这个过程中也可能会更新数据,从而导致很多问题。在某些情况下,我们需要在子 View 存在的时间里,将子 View 的数据作为唯一的数据源:子 View 中的数据不受父 View 的影响,并在自身的数据更新后,对父 View 的数据进行更新。
在本应用的实现中,有三种使用 UIKit 框架的情况:
- UIKit 的
UIView
或UIViewController
作为View
,通过UIViewRepresentable
和UIViewControllerRepresentable
实现。 - 先将
View
用UIHostingController
包装为 UIKit 的UIViewController
,在对其内部进行一定修改后,再用UIViewControllerRepresentable
包装为View
。 - 直接使用 UIKit 的命令式代码实现视图的导航等操作。