MVVM + RxSwift
iOS 中的 MVVM 架構早就是個老生常談的問題,相比於傳統的 MVC 架構方式, MVVM 比較核心的地方在於雙向繫結的過程,即 View 和 ViewModel 之間的繫結,而建立繫結關係最優方案是通過響應式的方式構建,iOS 原生方面可以通過 KVO + KVC 的方式去搭建響應式,缺點是API相對複雜,操作不方便,純 Swift 物件需要標記為 dynamic
,需要手動管理 KVO 的生命週期。
RxSwift 屬於 ReactiveX 系列,目前存在多個語言版本,基本覆蓋全部主流程式語言,其專注於非同步程式設計與控制可觀察資料(或者事件)流的API,背後是微軟的團隊在開發維護,所以穩定性較高。RxSwift 是一種響應式的程式設計思想,故非常適合與 MVVM 架構配合使用。
ViewModel + ReactorKit
MVVM 架構最核心的部分無疑是 ViewModel ,其主要負責模組的邏輯處理、狀態的維護等。在 iOS 開發中,狀態一詞提及的相對較少,不像 React 中元件對狀態的依賴那麼強烈。其實每一個具有互動功能的控制元件都會依賴於狀態,狀態決定控制元件的表示方式,故狀態在 iOS 開發中同樣重要。傳統的 MVC 方式,狀態的管理主要是 Controller 負責,在 MVVM 中由 ViewModel 管理。
ReactorKit 是一個輕量級的響應式框架,其依賴於 RxSwift 並結合了 Flux 。Flux 是 faceBook 提出的一種架構思想,其核心概念是資料的單向流動, 同樣適用於 iOS,在 iOS 中主要表現為 Action 和 State 兩個部分:
View 發出的 Action,經由 Reactor 處理後,由 State 丟擲後繫結到 View 上,即每一個狀態的改變都要派發一個 Action 。也就是說 View 以怎樣的方式顯示是被動的,如想改變自身的渲染方式需要自己派發 Action。
-
View
ReactorKit 將 Controller 和 View 都歸類為
View
,使用方式是需要實現View
協議:class ReactorViewController: UIViewController, View { ... } 複製程式碼
-
Reactor + State
可以理解為 Reactor 就是 ViewModel 。Reactor 同樣是協議,其限定了 ViewModel 的行為(程式碼片段來自網路):
class ReactorViewModel: Reactor { /// - Action: View 派發的事件 enum Action { case refreshFollowingStatus(Int) case follow(Int) } /// - Mutation:Action 和 State 之間的過渡 enum Mutation { case setFollowing(Bool) } /// - State:狀態管理器 struct State { var isFollowing: Bool = false } /// - 狀態管理器初始化 let initialState: State = State() } 複製程式碼
func mutate(action: Action) -> Observable<Mutation> { switch action { /// - View 派發的 Action 會在這裡被響應 case let .refreshFollowingStatus(userID): return UserAPI.isFollowing(userID) // create an API stream .map { (isFollowing: Bool) -> Mutation in /// - 派發一個 Mutation return Mutation.setFollowing(isFollowing) } /// - 同理 case let .follow(userID): return UserAPI.follow() .map { _ -> Mutation in return Mutation.setFollowing(true) } } func reduce(state: State, mutation: Mutation) -> State { /// - 對 State 做一份拷貝,因為 State 是 let 宣告的結構體 var state = state switch mutation { /// - 將 Mutation 所關聯的資料對映到 State 上 case let .setFollowing(isFollowing): /// - 改變 State state.isFollowing = isFollowing /// - 返回一個新的 State return state } 複製程式碼
以上是 ViewModel 的大致工作流程:Action -> Mutatuin -> State,還有一些可選的API,如 transform(),可翻閱官方文件檢視。通過以上程式碼可知,ReactorKit 符合 Flux 程式設計思想,簡單來說 State 改變需通過 Action。
-
完善 View
眾所周知,MVVM 中 Controller 需持有 ViewModel,同樣 ReactorKit 中的 View 協議規定需顯示的指定 Reactor 的型別,並提供了
bind()
方法,可以在這個方法中建立 View 和 ViewModel 之間的繫結關係。class ReactorViewController: UIViewController, View { func bind(reactor: ReactorViewModel) { /// - View 發出的 Action 繫結到了 ViewModel(Reactor) 的 action 上 refreshButton.rx.tap.map { Reactor.Action.refresh } .bindTo(reactor.action) .addDisposableTo(self.disposeBag) /// - ViewModel(Reactor) 的 State 繫結到了 View 上,並立即根據 State 渲染 reactor.state.map { $0.isFollowing } .bindTo(followButton.rx.isSelected) .addDisposableTo(self.disposeBag) } } 複製程式碼
有了 ReactorKit 的協助,ViewModel 的行為更清晰明瞭。
Coordinator
Coordinator 導航層。傳統的開發模式下,頁面間的跳轉是通過 navigationController
的 push()
方法,這種方法固然便捷,但是實現跳轉存在頁面間耦合。Coordinator 的誕生就是為了解決這一問題,當然 路由 或者引入 中間管理層 也可以實現解耦,但 Coordintaor 更輕量。
引入 Coordinator 後跳轉邏輯對頁面不可見,由 Coordinator 管理,其提供了 navigationController
的介面並持有 Controller,跳轉邏輯隱藏在了 Coordinator 中。Coordinator 獨立與 MVVM 之外,是一個附加層,不依賴於 MVVM 中的任何一部分。可以理解為 Coordinator 是每個元件對外暴露的介面,固然頁面間的互動,只能通過 Coordinator,同樣依賴於 RxSwift。
- 實現 Coordinator
/// - 頁面 Pop 後觸發的事件
enum PopResult {
case reload
case cancel
}
final class ExampleCoordinator: BaseCoordinator<PopResult> {
override func start() -> Observable<PopResult> {
let controller = ReactorViewController()
let reactor = ReactorViewModel()
let cancel = controller.popedAction
.asObserver()
.map { PopResult.cancel }
let delete = reactor.state
.map { $0.isDelete }
.filter { (bool) -> Bool in
return bool
}
.map { _ in PopResult.reload }
return Observable
.merge(cancel, delete)
.take(1).do(onNext: { [weak self] (result) in
if let `self` = self {
switch result {
case .reload:
self.navigationController.popViewController(animated: true)
break
default:
break
}
}
})
}
}
複製程式碼
- Coordinator 互動(頁面跳轉)
self.coordinate(to: ExampleCoordinator())
.subscribe(onNext: { result in
switch result {
case reload:
...
case cancel:
...
}
})
.disposed(by: self.disposeBag)
複製程式碼
-
Coordinator 原始碼同樣很簡單
public protocol CoordinatorType: NSObjectProtocol { var identifier: UUID { get } var childCoordinators: [UUID: Any] { get set } var navigationController: RTRootNavigationController! { get set } } public extension CoordinatorType { func store<T>(coordinator: BaseCoordinator<T>) { coordinator.navigationController = navigationController childCoordinators[coordinator.identifier] = coordinator } func free<T>(coordinator: BaseCoordinator<T>) { childCoordinators[coordinator.identifier] = nil } @discardableResult public func coordinate<T>(to coordinator: BaseCoordinator<T>) -> Observable<T> { store(coordinator: coordinator) return coordinator.start() .do(onNext: { [weak self] _ in if let `self` = self { self.free(coordinator: coordinator) } }) } } public class BaseCoordinator<ResultType>: NSObject, UINavigationControllerDelegate, CoordinatorType { /// Typealias which will allows to access a ResultType of the Coordainator by `CoordinatorName.CoordinationResult`. typealias CoordinationResult = ResultType public var navigationController: RTRootNavigationController! func start() -> Observable<ResultType> { fatalError("Coordinator Start method should be implemented by subclass.") } func noResultStart() { fatalError("Coordinator noResultStart method should be implemented by subclass.") } /// UINavigationControllerDelegate public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { // ensure the view controller is popping if let transitionCoordinator = navigationController.transitionCoordinator, let fromViewController = transitionCoordinator.viewController(forKey: .from), !navigationController.viewControllers.contains(fromViewController) { fromViewController.popedAction.onNext(()) fromViewController.popedAction.onCompleted() } } let disposeBag = DisposeBag() public let identifier = UUID() public var childCoordinators = [UUID: Any]() } 複製程式碼