背景
对Swift的函数式编程一直都只停留在《函数式Swift》案例中,长久以来命令式编程思想也让我很难真正的理解相应内容,更没有勇气在项目运用函数式编程相关的内容。但是最近阅读了王巍大神的单向数据流动的函数式 View Controller,通过对文章内容的理解和实践,让我对Swift函数式编程思想有深刻的理解,也切实体会到它的好处。为了加深记忆和方便理解,对整个理解和实践的过程进行梳理。
1. 函数式 View Controller设计的原理。
2. POCT项目Mine页面的重设计。
3. 函数式 View Controller设计的利弊权衡。
函数式 View Controller设计的原理
函数式的核心就是输入的值确定,那输出的结果是唯一的。
关于函数式View Controller的设计,可以理解为用户**操作行为**改变VC**状态**,订阅VC的状态变化,通过变化内容实现UI更新。单个行为对应唯一的页面显示。
所以vc显示最基本的流程图应该如图1.1所示:
如果是没有复杂交互逻辑的页面,比如单行为,单显示结果的页面这样设计结构也可以。但是一旦遇到复杂逻辑的页面,显然无法满足。对于一般页面而言,用户行为->VC显示的逻辑按照以下3个步骤进行。
1. 用户行为(网络请求或者点击事件)。
2. 判断之前VC的显示内容。
3. 再根据相应的内容变化,对具体的变化内容进行VC的UI更新。
现阶段大多数的vc实现类似于下面的流程进行设计:
这么实践从逻辑上而言没有任何问题,但是根本没有关于用户行为的管理,也就是“用户行为”这一层是很乱,随着页面的逻辑越来越复杂,用户的行为越来越多,导致“数据判断”部分的内容会杂乱的散落在vc层各个角落,这对于vc的维护者或者接盘手而言简直是灾难,可能还不如重新设计来的划算。这工作时间浪费得毫无意义。
幸好函数式的VC设计,给我们提供了新的实践思路,大致可以将其流程理解为:
这个流程跟之前的比,还是有很大的改进,将网络所有用户的本地操作以改变的vc状态进行统一管理,但是针对于网络请求的行为这一行为,完全和用户操作和vc的状态剥离开,但事实上,vc显示的主要操作源还是网络请求,将如此大的模块从vc设计剥离开,对代码的质量和可读性的提升并不大。所以为了使vc设计的架构更加饱满,需要对结构进行再设计。
注: 整个流程估计比较难理解的就是“逻辑判断”再返回到“用户操作”的部分。如果是产生了网络指令,那么会在逻辑判断的阶段进行网络请求,通过请求的数据,重新引起用户操作,再改变vc状态。核心还是把所有的状态内容都进行统一的存储。
最终引起UI发生变化的因子只有vc状态发生改变。通过这样的设计可以清晰梳理vc的业务逻辑,设计VC的阶段也是对交互逻辑梳理的阶段,可以从vc设计的角度了解功能,这样一来可以保证对开发的代码负责,同时提高可读性和可维护性。即便遇到“事而多”的产品,提出各种各样异想天开的行为,只要想得到,保证完成起来轻松愉快,一口气实现增加10个功能,不费劲儿~关于函数式vc结合对王巍大神的文章和自己的理解,总结出的核心内容只有三点:单操作行为对应唯一的UI显示。单操作行为对应唯一的UI显示。单操作行为对应唯一的UI显示。
现阶段根据我的理解,上述的函数式vc设计流程是比较完整的方案,如果在之后有更好的方案,会进行更新。
POCT项目Mine页面的重设计
Mine页面核心业务,一些基本操作的入口、通过某些状态的改变对某些入口进行提示。在之前的设计中已经对Mine页面所涉及到的所有数据内容进行封装,所以对其进行重新设计的过程中只需要关注业务即可。
根据图1.4的流程,设计函数式Mine页面。可以把网络请求和用户操作放在统一的Action模块进行管理,而把产生的网络指令和vc状态放在State模块进行管理,而在逻辑判断阶段只需要区分State改变的是vc状态还是网络指令即可,最后进行相关的UI更新即可。
具体实现
首先是对Mine页面的***业务梳理*:
**进行梳理,其主要的业务如下所示:
1. 关于系统消息和推送消息红点提醒。
2. 关于七鱼消息的推送的红点提醒。
3. 设备连接的提示。
4. 用户处于怀孕状态还是备孕状态的提示。
5. 用户名修改的显示。
6. 如果处于检测值已经是怀孕了,但是状态还处于备孕状态的,有一个tips的提示用户切换状态。
7. 网络请求。
针对Mine页面涉及的以上七种业务,在Action模块中需要管理这七种Action:
enum PersonCenterAction: ActionType {
// with cell
case messageBadge(isShowBadge: Bool, messageType: BadgeType)
case serviceBadge(isShowBadge: Bool, count: Int)
case deviceBadge(isShowBadge: Bool)
// with headview
case headTip(isShowTip: Bool)
case stateButton(stateTitle: String)
case setTitle(title: String)
// command
case loadData
}
针对于每一种Action,都会改变vc的State,所以需要对每一个Action所引发的State改变进行进行操作:
fileprivate lazy var reducer: (PersonCenterState, PersonCenterAction) -> (state: PersonCenterState, command: PersonCenterCommand?) = { (state: PersonCenterState, action: PersonCenterAction) in
var personState = state
var command: PersonCenterCommand? = nil
switch action {
case .messageBadge(let isShowBadge, let msssageType):
personState.messageState[msssageType] = isShowBadge
personState.staticData[1][0].isRed = false
for s in personState.messageState {
if s.value {
personState.staticData[1][0].isRed = true
}
}
case .serviceBadge(let isShowBadge, let count):
personState.staticData[2][1].isRed = isShowBadge
personState.staticData[2][1].unreadCount = count
case .deviceBadge(let isShowBadge):
personState.staticData[0][0].isShowTips = [false, false, isShowBadge]
case .headTip(let isShowTip):
personState.headData?.didShownBubbles = isShowTip
case .stateButton(let stateString):
personState.headData?.stateButtonTitle = stateString
case .setTitle(let title):
personState.headData?.title = title
case .loadData:
command = PersonCenterCommand.getFemaleData
}
return (personState, command)
}
在State发生改变之后,需要针对每一种情况进行分开处理,这里主要处理工作就是tableview的刷新和header内容的更新。
func stateDidChange(_ state: PersonCenterState, previousState: PersonCenterState?, command: PersonCenterCommand?) {
// tableview的状态变化 相对而言会增加内存消耗
guard let `command` = command else {
headerView.model = state.headData
tableView.reloadData()
return
}
// 关于消息的数据刷新
switch command {
case .getFemaleData: loadFemaleInfo()
} }
以上就是函数式Mine VC的核心代码。
函数式 View Controller设计的利弊权衡
存在的问题分析:
在stateDidChange方法中关于tableView.reloadData()的使用可能会有疑问,明明大多数行为只是修改了某一行的内容,而到了这里,一股脑全刷新了。当然可以细化将每次的修改针对具体的某个Cell进行刷新方案也很多:
1. 通过比对之前的State和之后的State.
2. 在State中增加一个关于修改了哪一个Cell或者header的属性,使每一个修改都有特定的标记.
首先来谈谈第一点,确定要比对State中的某一个属性的变化,会增加很多的逻辑操作,不建议使用;关于第二点,它确实可以使方法能够正常调用,但是问题就是这是一个完全和业务逻辑都无关的操作,作为State的某个属性耦合性太强。因此在方法中还是使用tableView.reloadData()。但是总感觉这个明显可以提升性能的地方犹如一根针插到心里。
对tableView.reloadData() 和 tableView.reloadRows从耗时和CPU消耗两个方面性能进行比对:
根据测试reloadData()和reloadRows()相比在性能上更有优势,所以使用reloadData()不用担心性能问题,只是跟reloadRows()相比在表意上会有所欠缺。
测试相关
函数式的VC设计有利于编写UnitTest,虽然现在我们工程的量级与QA的配备不需要开发自行编写UnitTest,但是如果可以这对于开发代码的质量提升还是有推进作用的。可以“干掉”天天给我们提Bug的QA,多开心~~~(/:X-)/:X-)
现阶段对函数式思考的内容还比较单薄,还需要再更多的实践中运用起来,才能真正的理解函数式。如有大神能给一些指导性的建议那是极好的。