项目实践
下面是 ViewModel 构造时候的最佳实践(仅供参考), 主要是将 VM 的代码分成3个类别, 分别是:
- Init: 即所有的构造方法分为一类, 在它们里面进行各类的依赖注入.
- Input: 在这部分包含公共属性(不一定是 public, 只需要保证 VC 可以正常访问这些属性.), 比如 subject, 或是普通属性, VC 通过它们传入(input)数据到 VM.
- Output: 这部分中也是包含的公共属性(不一定是 public, 只需要保证 VC 可以正常访问这些属性.), 但通常都是 Observable. VM 通过它们来向外界提供输出(Output), 一般来说都是 driver(也是一种特殊的 Observable) 或者是其他 observable. VC 利用这些属性来驱动 UI.
一般来说, 项目架构是否清晰, 很简单的衡量方式就是去看 UI, 业务逻辑, 以及支撑业务逻辑的若干服务是否拥有良好的封装.
根据这样的标准, 应用内的元素可以这样组织:
- Scene: 用于表示一个 VC 管理的界面, 包含该界面对应的 VC 和 View Model, View.
- View Model: 视图模型, 包含提供给 VC 使用的业务逻辑和数据.
- VC: 控制器, 其中仅包含视图控制逻辑
- View: 视图, 即包含的是 UI 的具体实现.
- Service: 服务, 其中包含的是提供给业务逻辑代码使用的各种支撑功能, 比如数据库访问服务, 网络 API 访问服务等.
- Models: 模型, 里面包含的是最最基本的数据结构, VM 和 Service 都是在操作和交换 Model 里面的对象.
在绑定 VC 和对应的 VM 时, 有一个好的办法, 就是像插入两个可插拔设备那样, 给 VM 一个接口, 或是给 VC 一个接口.
例如可以构造一个协议如下所示:
protocol BindableType {
associatedtype ViewModelType
var viewModel: ViewModelType! { get set }
func bindViewModel()
}
associatedtype 指定和协议相关的类型名称占位符. 但该协议并非是泛型协议. 在使用的时候只需要在协议的实现类中指定该类型的实际类型即可:
typealias ViewModelType = Int
这样所有需要绑定 VM 的 VC 都需要实现这个协议, 在这里就可以让持有 vm, 并且在 bindViewModel
方法中对 UI 和 observable 或 action 进行绑定.
而绑定时机需要注意, 一般来说都希望在视图已经建立成功后才会进行绑定. 故在 viewdidload 中去绑定, 而为了让绑定能够安全进行, 可以添加一个帮助方法, 在 ViewDidLoad 中去调用这个方法:
extension BindableType where Self: UIViewController {
mutating func bindViewModel(to model: Self.ViewModelType) {
viewModel = model
loadViewIfNeeded()
bindViewModel()
}
}
这个帮助方法看起来很怪异, 但主要作用就是将 model 赋值给 VC, 并且保证视图加载完成后再调用 bindViewModel()
方法.
构造 Model 中的基础对象
比如 Todo List 中的 Item, 如果使用 Realm 存储的话, 需要像下面这样构造:
class TaskItem: Object {
dynamic var uid: Int = 0
dynamic var title: String = ""
dynamic var added: Date = Date()
dynamic var checked: Date? = nil
override class func primaryKey() -> String? {
return "uid"
}
}
在使用 Realm 的时候需要注意如下事项:
- realm 的对象不能跨线程使用, 如果要在其他线程使用某个对象, 需要重新进行查询, 或者是使用 realm 提供的
ThreadSafeReference
. - 从 realm 里面查询出来的对象都是自动更新的, 即如果数据库中对象变化了, 则之前查询出来的对象的相应属性也会同样进行变化.
- 但上述的特性也有副作用, 若一个对象被从数据库删除, 则它在内存中的所有对象拷贝都将失效. 就是当你去访问一个被删除的对象的属性, 则会出现异常.
构造 Task Store 服务
下面就可以利用 realm 来构造对象的存储服务了.
构造服务的时候, 最佳实践是: 构造一个 protocol 用于暴露服务的接口, 构造一个服务的实现, 构造一个服务的 mock 实现用于单元测试.
首先构造 protocol:
protocol TaskServiceType {
@discardableResult
func createTask(title: String) -> Observable<TaskItem>
@discardableResult
func delete(task: TaskItem) -> Observable<Void>
@discardableResult
func update(task: TaskItem, title: String) -> Observable<TaskItem>
@discardableResult
func toggle(task: TaskItem) -> Observable<TaskItem>
func tasks() -> Observable<Results<TaskItem>>
}
下面是一个 方法的实现示例:
@discardableResult
func update(task: TaskItem, title: String) -> Observable<TaskItem> {
let result = withRealm("updating title") { realm -> Observable<TaskItem> in
try realm.write {
task.title = title
}
return .just(task)
}
return result ?? .error(TaskServiceError.updateFailed(task))
}
其中 withRealm 是一个帮助方法, 用于获取当前的 realm 数据库对象, 并且进行相应操作.
提供服务的实现对象:
struct TaskService: TaskServiceType {
再看 Scene 如何构造
再次强调:
- Scene 由一个 VC 和一个 VM 构成, 相当于一个场景.
- 其中 VM 包含业务逻辑, 在 VM 中实现场景切换, 并且和 VC 实现双向通信. 但 VM 不知道实际和它沟通的具体 VC, 只是通过接口来交流.
- VC 只包含视图控制逻辑, VM 和 View 不能直接通信. 在 VC 中不能进行场景切换, 场景切换是 VM 中的业务逻辑驱动的.
At this stage, a view model can instantiate another view model and assign it to its scene, ready for transition.
新建一个类似 Scene 管理器的实体(Scene 枚举), 添加如下代码:
enum Scene {
case tasks(TasksViewModel)
case editTask(EditTaskViewModel)
}
表明 APP 里面有两个 Scene, tasks 和 editTask, 并且各自对应有不同的 VM.
下面的代码演示了 Scene 管理器如何管理 VC 和 VM 以及它们的关系:
extension Scene {
func viewController() -> UIViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
switch self {
case .tasks(let viewModel):
let nc = storyboard.instantiateViewController(withIdentifier:
"Tasks") as! UINavigationController
var vc = nc.viewControllers.first as! TasksViewController
vc.bindViewModel(to: viewModel)
return nc
case .editTask(let viewModel):
let nc = storyboard.instantiateViewController(withIdentifier:
"EditTask") as! UINavigationController
var vc = nc.viewControllers.first as! EditTaskViewController
vc.bindViewModel(to: viewModel)
return nc
}
}
}
不过在大型项目中可能有若干的 Scene, 这样就会导致这样的方法十分庞大, 故可以对 Scene 进行分层, 即分离为多个 Domain, 然后每个 Domain 对应有若干的 Scene, 然后对其中的 Scene 再进行类似管理.
之后就可以使用一个 Scene Coordinator 来管理 Scene 的切换了.
Scene 的切换: 使用 Scene Coordinator
关于 Scene 的切换, 有很多的方法, 有直接在 VC 进行的, 有使用 route 进行的. 这里使用一种比较简单的方式, 这样的方式在若干 app 的构建中经受住了实践的检验.
下面的图说明了这样切换过程:
Scene 切换- Scene A 中的 VM1 实例化 Scene B 关联的 VM2
- VM1 调用 Scene Coordinator 中的方法(比如 transition), 利用它来完成之后的步骤
- transition 会调用之前的 Scene 管理器中的
func viewController() -> UIViewController
方法, 这样就得到了 VM2 对应的 VC - 将对应 VC 和 VM2 进行绑定
- 最后将 VM2 对应的 VC 显示出来.(push, pop, present/modal, and dismiss.)
这样的架构下, 就将 VM 和它们对应的 VC 完全隔离开来了.
实现 Scene Coordinator
同样地, 构造一个 protocol, 一个协议实现, 一个 mock 实现用于测试.
协议如下所示:
protocol SceneCoordinatorType {
init(window: UIWindow)
@discardableResult
func transition(to scene: Scene, type: SceneTransitionType) -> Observable<Void>
@discardableResult
func pop(animated: Bool) -> Observable<Void>
}
其中的 SceneTransitionType 就可以指定是何种切换方式, 比如 push 或者 present, dismiss 等.
返回值中的 Observable 表示没有任何数据返回, 当切换完成的时候输出 complete.
待续.
网友评论