最近几周的时间都在重构一个控件。仿照 AppStore 中的下载效果写的一个按钮。写篇文章记录一下心得体会。
文章分为重构前,为什么重构,以及重构后三部分去叙述代码层面的设计思想。这里规避了繁琐的业务逻辑,以及为了解决实际应用场景中 cell 的复用而做的一些处理,仅仅聚焦于类的职责设计。
先看一下效果图

重构前的代码
UPDownloadButtonDefines.h
UPDownloadButtonDefines.h 中用枚举定义了 DownloadButton 的 state,每种状态对应一种UI形态。
typedef NS_ENUM(NSUInteger, EUPDownloadButtonState) {
nUPDownloadButtonStateIdle, //闲置状态,等待下载
nUPDownloadButtonStatePending, //开始下载,还没有接收到数据
nUPDownloadButtonStateDownloading, //接收到数据
nUPDownloadButtonStateDownloaded, //下载完成
nUPDownloadButtonStateDefault = nUPDownloadButtonStateIdle,
};
OUPDownloadButton
OUPDownloadButton 继承自 UIView,它是整个库的核心。它负责:
- 初始化一些必要的元素: 比如 OUPDownloadButtonTitleView , 图片,UIControl,转圈图层,进度环图层等。
- 处理点击事件。
- 管理 state。
- 管理 state 相关的 UI 代码。
看一下 OUPDownloadButton 的头文件:
@protocol OUPDownloadButtonDelegate<NSObject>
@optional
- (void)downloadButtonDidStartDownloading:(OUPDownloadButton*)downloadButton;
- (void)downloadButtonDidCancelDownloading:(OUPDownloadButton*)downloadButton;
- (void)downloadButtonDidTapAfterDownload:(OUPDownloadButton*)downloadButton;
@end
@interface OUPDownloadButton : UIView
@property (nonatomic) OUPDownloadButtonConfiguration* configuration;
// idle -> pending -> downloading -> downloaded
@property (nonatomic, readonly) EUPDownloadButtonState state;
@property (nonatomic, weak) id<OUPDownloadButtonDelegate> delegate;
// 0.0 ~ 1.0
@property (nonatomic) CGFloat progress;
- (void)start;
- (void)cancel;
- (void)complete;
- (void)restoreWithState:(EUPDownloadButtonState)state progress:(CGFloat)progress;
@end
- DownloadButton 通过代理来建立与 Controller 的联系。通知任务下载开始,结束,以及取消时的事件。
- 持有 state 属性,因为不希望在 DownloadButton 以外的地方更改 state ,所以设计成了 readonly。
- 提供 start 等方法给外界调用,切换 DownloadButton 的状态。
OUPDownloadButtonConfiguration
OUPDownloadButtonConfiguration 用于配置 DownloadButton 在不同状态时的 UI 样式(比如 图片 字体 颜色等)
OUPDownloadButtonTitleView
OUPDownloadButtonTitleView 是按钮的标题视图,它包含了一个 Calyer 和 UILabel 。因为 OUPDownloadButton 的 UI 形态在切换的时候会有一些过度动画,这个过度动画主要由 OUPDownloadButtonTitleView 来负责,所以单独抽个类出来也易于管理和设计接口。
看一下 DownloadButton 的点击事件
- (void)onTap:(id)sender
{
switch (_state)
{
case nUPDownloadButtonStateIdle:
[self exitIdle];
[self enterPendingAndNotify:YES];
break;
case nUPDownloadButtonStatePending:
[self cancelAndNotify:YES];
break;
case nUPDownloadButtonStateDownloading:
[self cancelAndNotify:YES];
break;
case nUPDownloadButtonStateDownloaded:
[self tappedAfterDownload];
break;
default:
SCShouldNotReachAssert();
break;
}
}
当我们点击 DownloadButton ,代码的执行思路大致是这样:根据当前的不同 state执行不同的方法,并手动更新 state 的值。比如当按钮处于 idle 的时候,点击进入 pending。当按钮处于 pending 或是 downloading 的时候,点击取消下载,恢复到idle。
MVC下的实际使用:

为什么需要重构
看完第一节我们就很清楚 DownloadButton 这个类的臃肿性了。如果我们需要额外增加一种状态,那么我们需要在 DownloadButton 中增加额外的状态判断和切换,对应的UI更新。这样 DownloadButton 会变得越来越难以维护,代码的可读性也会越来越差。
DownloadButton 既然是一个 UIView,那么它就应该只负责 UI 层面的事情,state可以交给专业的类去管理,这样既可以减轻 DownloadButton 的负担,又可以让DownloadButton 遵守面向对象的单一原则。所以 DownloadButton 中实际上不应该出现和 state 有关的任何代码。
或许我们可以做的更多。UI层面的事情我们也可以用 Category 来进一步管理。将state 相关的 UI 更新抽到分类中来专门维护。
所以我们希望重构之后的 DownloadButton 既不用关心 state,又不用关心 state 相关的 UI 更新。
重构后的代码
现在来看下重构后的库结构:
首先将类的命名改了: DownloadButton -> OperationButton 因为项目中可能会在其他场景下用到此类的 button,比如内购的支付按钮。所以命名上我们希望这个按钮能够适应所有可能的业务场景而不局限于下载。
OUPOperationButtonImageView
将之前的图片抽了一个单独的类出来 便于管理 结构更加清晰 也方便于接口设计。
OUPOperationButtonProcessView
将之前的转圈图层和进度环图层抽了一个单独的类出来 便于管理 结构更加清晰 也方便于接口设计。
OUPOperationButtonTransitionManager
OUPOperationButtonTransitionManager 负责状态的维护。
OUPOperationButtonTransitionManager 里面维护着一个状态机,在OUPOperationButtonTransitionManager 中我们可以定义各种状态及事件。状态机将状态和事件关联起来。
比如按钮从 state A切换到了 state B。那么我们将 A 称为 SourceState 将 B 称为DestinationState。我们将这个转换状态的过程定义为事件。事件的初始化依赖于SourceState 和 DestinationState。
每个事件的 SourceState 可以有多个,但是 DestinationState 只能有一个。当我们想切换状态的时候,就可以让状态机执行一个以目标状态为 DestinationState 的一个事件。如果当前状态是 SourceState 中的一个,那么这个事件将被正常执行。
比如 按钮从点击下载开始的状态转换过程为:
StateOriginal -> StateReady -> StateExecuting -> StateFinished
当我们想取消下载的时候 状态机会去执行一个 cancel 事件。当按钮在 StateReady的时候你可以 cancel,当按钮在 StateExecuting 的时候你也可以 cancel。但是无论你是从哪种状态 cancel的,cancel 事件成功后状态都会变成 StateOriginal。这里的StateReady 和 StateExecuting 叫做 SourceState。StateOriginal 叫做DestinationState。
那么问题来了,状态的管理问题是解决了,Button 已经不再自己维护状态了。不过如何通知 Button 在状态切换的时候更新UI呢?
嗯... 说的直白一点。我们对 OUPOperationButtonTransitionManager 这个类的期望是:
- 帮我们管理好 Button 的状态 当外界的 view action 到来时 OUPOperationButtonTransitionManager 会帮我们切换对应的状态
- 状态切换时能够及时通知 Button 去更新 UI
我们来看一下 OUPOperationButtonTransitionManager 的头文件
@interface OUPOperationButtonTransitionManager : NSObject
@property (nonatomic, readonly) EUPOperationButtonState currentState;
- (instancetype)initWithHandlers:(NSDictionary*)handlers;
- (void)fireEventWithTargetState:(EUPOperationButtonState)state;
@end
可以看到OUPOperationButtonTransitionManager有一个初始化方法
- (instancetype)initWithHandlers:(NSDictionary*)handlers;
handlers 以 state 作为 key 对应的 UI 更新事件(闭包)作为 value。OUPOperationButtonTransitionManager 会在状态切换的时候根据一个状态的进入或退出去执行对应的闭包。
示例代码
- (void)setDidEnterStateActionForState:(TKState*)state
{
SC_WEAKIFY(self);
[state setDidEnterStateBlock:^(TKState* state, TKTransition* transition) {
SC_STRONGIFY(self);
//state的字符串作为key从handler中取出对应的闭包 然后执行。
self.didEnterStateHandlers[state.name](nil);
}];
}
OUPOperationButton+FSM
OUPOperationButton+FSM 的职责:
- 维护状态相关的 UI 代码
- 创建 OUPOperationButtonTransitionManager
- 给外界提供更改状态的接口
@interface OUPOperationButton (FSM)
- (void)setupFSM;
- (void)transitionToTargetState:(EUPOperationButtonState)state;
@end
在 OUPOperationButton+FSM.m 中我们定义了各种状态切换的 UI 更新代码,那么理所当然,OUPOperationButtonTransitionManager 应该在OUPOperationButton+FSM 中被初始化。
OUPOperationButton_Internal.h
OUPOperationButton 拥有了可以管理状态的OUPOperationButtonTransitionManager 以及管理 state 相关的 UI 代码的OUPOperationButton+FSM。不过当我们将 OUPOperationButton 封装成一个静态库的时候,我可不希望将这两个类的头文件暴露出去。因为调用者并不需要知道OUPOperationButton 是怎么实现的。所以这里还需要一个 internal.h 来帮助我们隐藏掉我们不想暴露给外界的头文件。我们将 Button 中的各个控件以及OUPOperationButtonTransitionManager 放到 Internal.h 中供 Button 模块内部调用。
#import "OUPOperationButton.h"
#import "OUPOperationButtonImageView.h"
#import "OUPOperationButtonProcessView.h"
#import "OUPOperationButtonTextView.h"
#import "OUPOperationButtonTransitionManager.h"
@interface OUPOperationButton ()
@property (nonatomic) UIControl* backingControl;
@property (nonatomic) OUPOperationButtonTextView* textView;
@property (nonatomic) OUPOperationButtonImageView* imageView;
@property (nonatomic) OUPOperationButtonProcessView* processView;
@property (nonatomic) OUPOperationButtonTransitionManager* transitionManager;
--- state相关的UI更新接口
@end
现在我们想在 Button 中额外增加一种情况的话,我们需要在OUPOperationButtonTransitionManager 中定义新的 state 和事件,在OUPOperationButton+FSM 中定义对应的UI更新。这样明显比一股脑的全塞在Button 中要清晰的多。
网友评论