使用ReSwift管理应用状态

使用ReSwift管理应用状态

前面提到引入App Coordinator之后,ViewController的剩下的职责之一就是“接收数据并把数据绑定到对应的子UIView展示”,这儿的数据来源就是应用的状态。它山之石,可以攻玉,不只是iOS应用有复杂状态管理的问题,在越来越多的逻辑往前端迁移的时代,所有的前端都面临着类似的问题,而目前Web前端最火的Redux就是为了解决这个问题诞生的状态管理机制,而ReSwift则把这套机制带入了iOS的世界。这套机制中主要有一下几个概念:
App State: 在一个时间点上,应用的所有状态. 只要App State一样,应用的展现就是一样的。

Store: 保存App State的对象,其还负责发送Action更新App State.

Action: 表示一次改变应用状态的行为,其本身可以携带用以改变App State的数据。

Reducer: 一个接收当前App State和Action,返回新的App State的小函数。

在这个机制下, 一个App的状态转换如下:
启动初始化App State -> 初始化UI,并把它绑定到对应的App State的属性上

业务操作 -> 产生Action -> Reducer接收Action和当前App State产生新的AppState -> 更新当前State -> 通知UI AppState有更新 -> UI显示新的状态 -> 下一个业务操作......

在这个状态转换的过程中,需要注意,业务操作会有两类:
无异步调用的操作,如点击界面把界面数据存储到App State上;这类操作处理起来非常简单,按照上面提到的状态转换流程走一圈即可。

有异步调用的操作。如点击查询,调用API,数据返回之后再存储到App State上。这类操作就需要引入一个新的逻辑概念(Action Creators)来处理,通过Action Creators来处理异步调用并分发新的Action。

整个App的状态变换过程如下:



无异步调用操作的状态流转



有异步调用操作的状态流转
经过ReSwift统一管理应用状态之后,App开发可以得到如下好处:

统一管理应用状态,包括统一的机制和唯一的状态容器,这让应用状态的改变更容易预测,也更容易调试。

清晰的逻辑拆分,清晰的代码组织方式,让团队的协作更加容易。

函数式的编程方式,每个组件都只做一件小事并且是独立的小函数,这增加了应用的可测试性。

单向数据流,数据驱动UI的编程方式。

整理后的iOS架构

经过上面的大篇幅介绍,下面我们就来归纳下结合了App Coordinator和ReSwift的一个iOS App的整体架构图:


架构实战

上面已经讲解了整体的架构原理,"Talk is cheap", 接下来就以Raywendlich上面的这个App为例来看看如何实践这个架构。


第一步:构建UI组件
在构建UI组件时,因为每个组件都是独立的,所以团队可以并发的做多个UI页面,在做页面时,需要考虑:
该ViewController包含多少子UIView?子UIView是如何组织在一起的?

该ViewController需要的数据及该数据的格式?

该ViewController需要支持哪些业务操作?

以第一个页面为例:



class SearchSceneViewController: BaseViewController {      //定义业务操作的接口    
    var searchSceneCoordinator:SearchSceneCoordinatorProtocol?      //子组件      var searchView:SearchView?  //该UI接收的数据结构      private func update(state: AppState) {            if let searchCriteria = state.property.searchCriter   {                searchView?.update(searchCriteria: searchCriteria)        }    }      //支持的业务操作      func searchByCity(searchCriteria:SearchCriteria) {            searchSceneCoordinator?.searchByCity(searchCriteria: searchCriteria)        }      func searchByCurrentLocation() {              searchSceneCoordinator?.searchByCurrentLocation()      }       //子组件的组织      override func viewDidLoad() {            super.viewDidLoad()            searchView = SearchView(frame: self.view.bounds)            searchView?.goButtonOnClick = self.searchByCity            searchView?.locationButtonOnClick = self.searchByCurrentLocation            self.view.addSubview(searchView!)      }}


注:子组件支持的操作都以property的形式从外部注入,组件内命名更组件化,不应包含业务含义。
其它的几个ViewController也依法炮制,完成所有UI组件,这步完成之后,我们就有了App的所有UI组件,以及UI支持的所有操作接口。下一步就是把他们串联起来,根据业务逻辑完成User Journey。
第二步:构建App Coordinators串联所有的ViewController
首先,在AppDelegate中加入AppCoordinator,把路由跳转的逻辑转移到AppCoordinator中。
var appCoordinator: AppCoordinator! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow() let rootVC = UINavigationController() window?.rootViewController = rootVC appCoordinator = AppCoordinator(rootVC) appCoordinator.start() window?.makeKeyAndVisible() return true }

然后,在AppCoordinator中实现首页SeachSceneViewController的加载
class AppCoordinator { var rootVC: UINavigationController init(_ rootVC: UINavigationController){ self.rootVC = rootVC } func start() { let searchVC = SearchSceneViewController(); let searchSceneCoordinator = SearchSceneCoordinator(self.rootVC) searchVC.searchSceneCoordinator = searchSceneCoordinator self.rootVC.pushViewController(searchVC, animated: true) }}

在上一步中我们已经为每个ViewController定义好对应的CoordinatorProtocol,也会在这一步中实现
protocol SearchSceneCoordinatorProtocol { func searchByCity(searchCriteria:SearchCriteria) func searchByCurrentLocation()}class SearchSceneCoordinator: AppCoordinator, SearchSceneCoordinatorProtocol { func searchByCity(searchCriteria:SearchCriteria) { self.pushSearchResultViewController() } func searchByCurrentLocation() { self.pushSearchResultViewController() } private func pushSearchResultViewController() { let searchResultVC = SearchResultSceneViewController(); let searchResultCoordinator = SearchResultsSceneCoordinator(self.rootVC) searchResultVC.searchResultCoordinator = searchResultCoordinator self.rootVC.pushViewController(searchResultVC, animated: true) }}

以同样的方式完成SearchResultSceneCoordinator. 从上面的的代码中可以看出,我们跳转逻辑中只做了两件事:初始化ViewController和装配该ViewController对应的Coordinator。这步完成之后,所有UI之间就已经按照业务逻辑串联起来了。下一步就是根据业务逻辑,让用App State在UI之间流转起来。
第三步:引入ReSwift架构构建Redux风格的应用状态管理机制
首先,跟着ReSwift官方指导选取你喜欢的方式引入ReSwift框架,笔者使用的是Carthage。
定义App State

然后,需要根据业务定义出整个App的State,定义State的方式可以从业务上建模,也可以根据UI需求来建模,笔者偏向于从UI需求建模,这样的State更容易和UI进行绑定。在本例中主要的State有:
struct AppState: StateType { var property:PropertyState ...}struct PropertyState { var searchCriteria:SearchCriteria? var properties:[PropertyDetail]? var selectedProperty:Int = -1}struct SearchCriteria { let placeName:String? let centerPoint:String?}struct PropertyDetail { var title:String ...}

定义好State的模型之后,接着就需要把AppState绑定到Store上,然后直接把Store以全局变量的形式添加到AppDelegate中。
let mainStore = Store<AppState>( reducer: AppReducer(), state: nil )

把App State绑定到对应的UI上

注入之后,就可以把AppState中的属性绑定到对应的UI上了,注意,接收数据绑定应该是每个页面的顶层ViewController,其它的子View都应该只是以property的形式接收ViewController传递的值。绑定AppState需要做两件事:订阅AppState
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) mainStore.subscribe(self) { state in state } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) mainStore.unsubscribe(self) }和实现StoreSubscriber的newState方法class SearchSceneViewController: StoreSubscriber { ...... override func newState(state: AppState) { self.update(state: state) super.newState(state: state) } ......}

经过绑定之后,每一次的AppState修改都会通知到ViewController,ViewController就可以根据AppState中的内容更新自己的UI了。
定义Actions和Reducers实现App State更新机制

绑定好UI和AppState之后,接下来就应该实现改变AppState的机制了,首先需要定义会改变AppState的Action们
struct UpdateSearchCriteria: Action { let searchCriteria:SearchCriteria}......

然后,在AppCoordinator中根据业务逻辑把对应的Action分发出去, 如果有异步请求,还需要使用ActionCreator来请求数据,然后再生成Action发送出去
func searchProperties(searchCriteria: SearchCriteria, _ callback:(() -> Void)?) -> ActionCreator { return { state, store in store.dispatch(UpdateSearchCriteria(searchCriteria: searchCriteria)) self.propertyApi.findProperties( searchCriteria: searchCriteria, success: { (response) in store.dispatch(UpdateProperties(response: response)) store.dispatch(EndLoading()) callback?() }, failure: { (error) in store.dispatch(EndLoading()) store.dispatch(SaveErrorMessage(errorMessage: (error?.localizedDescription)!)) } ) return StartLoading() } }

Action分发出去之后,初始化Store时注入的Reducer就会接收到相应的Action,并根据自己的业务逻辑和当前App State的状态生成一个新的App State
func propertyReducer(_ state: PropertyState?, action: Action) -> PropertyState { var state = state ?? PropertyState() switch action { case let action as UpdateSearchCriteria: state.searchCriteria = action.searchCriteria ... default: break } return state }

最终Store以Reducer生成的新App State替换掉老的App State完成了应用状态的更新。
以上三步就是一个完整的架构实践步骤,该示例的所有源代码可以在笔者的Github上找到

总结

以解决掉Massive ViewController的iOS应用架构之争持续多年,笔者也参与了公司内外的多场讨论,架构本无好坏,只是各自适应不同的上下文而已。本文中提到的架构方式使用了多种模式,它们各自解决了架构上的一些问题,但并不是一定要捆绑在一起使用,大家完全可以根据需要裁剪出自己需要的模式,希望本文中提到的架构模式能够给你带来一些启迪。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容