单数据流的VC设计

背景


对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设计_第1张图片

如果是没有复杂交互逻辑的页面,比如单行为,单显示结果的页面这样设计结构也可以。但是一旦遇到复杂逻辑的页面,显然无法满足。对于一般页面而言,用户行为->VC显示的逻辑按照以下3个步骤进行。

1. 用户行为(网络请求或者点击事件)。

2. 判断之前VC的显示内容

3. 再根据相应的内容变化,对具体的变化内容进行VC的UI更新。

现阶段大多数的vc实现类似于下面的流程进行设计:


单数据流的VC设计_第2张图片

这么实践从逻辑上而言没有任何问题,但是根本没有关于用户行为的管理,也就是“用户行为”这一层是很乱,随着页面的逻辑越来越复杂,用户的行为越来越多,导致“数据判断”部分的内容会杂乱的散落在vc层各个角落,这对于vc的维护者或者接盘手而言简直是灾难,可能还不如重新设计来的划算。这工作时间浪费得毫无意义。

幸好函数式的VC设计,给我们提供了新的实践思路,大致可以将其流程理解为:


单数据流的VC设计_第3张图片

这个流程跟之前的比,还是有很大的改进,将网络所有用户的本地操作以改变的vc状态进行统一管理,但是针对于网络请求的行为这一行为,完全和用户操作和vc的状态剥离开,但事实上,vc显示的主要操作源还是网络请求,将如此大的模块从vc设计剥离开,对代码的质量和可读性的提升并不大。所以为了使vc设计的架构更加饱满,需要对结构进行再设计。


单数据流的VC设计_第4张图片

注: 整个流程估计比较难理解的就是“逻辑判断”再返回到“用户操作”的部分。如果是产生了网络指令,那么会在逻辑判断的阶段进行网络请求,通过请求的数据,重新引起用户操作,再改变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消耗两个方面性能进行比对:


单数据流的VC设计_第5张图片

根据测试reloadData()和reloadRows()相比在性能上更有优势,所以使用reloadData()不用担心性能问题,只是跟reloadRows()相比在表意上会有所欠缺。

测试相关

函数式的VC设计有利于编写UnitTest,虽然现在我们工程的量级与QA的配备不需要开发自行编写UnitTest,但是如果可以这对于开发代码的质量提升还是有推进作用的。可以“干掉”天天给我们提Bug的QA,多开心~~~(/:X-)/:X-)


现阶段对函数式思考的内容还比较单薄,还需要再更多的实践中运用起来,才能真正的理解函数式。如有大神能给一些指导性的建议那是极好的。


你可能感兴趣的:(单数据流的VC设计)