本文做者:谭歆
做为 iOS 开发者,一提到控件,就不得不提到 UIButton
,它作为 iOS 系统最经常使用的响应用户点击操做的控件,为咱们提供了至关丰富的功能以及可定制性。而咱们的平常工做的 80% ~ 90% 作是在与 UI 打交道,处理控件在用户的不一样操做下的不一样状态,最简单的,好比用户没有登陆时,按钮置灰不可点击,用户点击时出现一个反色效果反馈到用户等等。对经常使用状态的定义,系统在很早的时候就给出了:前端
typedef NS_OPTIONS(NSUInteger, UIControlState) { UIControlStateNormal = 0, UIControlStateHighlighted = 1 << 0, // used when UIControl isHighlighted is set UIControlStateDisabled = 1 << 1, UIControlStateSelected = 1 << 2, // flag usable by app (see below) UIControlStateFocused API_AVAILABLE(ios(9.0)) = 1 << 3, // Applicable only when the screen supports focus UIControlStateApplication = 0x00FF0000, // additional flags available for application use UIControlStateReserved = 0xFF000000 // flags reserved for internal framework use };
咱们通常预先设置好 UIButton
在不一样状态下的样式,而后直接改对应状态的 bool
值便可,使用上比较方便。ios
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; // 正常状态 [button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal]; // 点击高亮 [button setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; [button setBackgroundImage:[UIImage imageNamed:@"btn_highlighted"] forState:UIControlStateHighlighted]; // 不可用 [button setTitleColor:[UIColor grayColor] forState:UIControlStateDisabled]; // 用户登陆状态变化时,修改属性值 if (/* 用户未登陆 */) { button.enabled = NO; } else { button.enabled = YES; }
那么 UIButton
只有四种状态可用吗?真实开发中,控件的状态可能不少,四种是必定不够用的。git
首先咱们注意到,UIControlState
的定义是一个 NS_OPTIONS,而不是 NS_ENUM,三个有效的 bit 两两组合应该有 8 种状态。正好咱们能够写个 Demo 测试一下:程序员
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; [btn setTitle:@"Normal" forState:UIControlStateNormal]; [btn setTitle:@"Selected" forState:UIControlStateSelected]; [btn setTitle:@"Highlighted" forState:UIControlStateHighlighted]; [btn setTitle:@"Highlighted & Disabled" forState:UIControlStateHighlighted | UIControlStateDisabled]; [btn setTitle:@"Disabled" forState:UIControlStateDisabled]; [btn setTitle:@"Selected & Disabled" forState:UIControlStateSelected | UIControlStateDisabled]; [btn setTitle:@"Selected & Highlighted & Disabled" forState:UIControlStateSelected | UIControlStateHighlighted | UIControlStateDisabled]; [btn setTitle:@"Selected & Highlighted" forState:UIControlStateSelected | UIControlStateHighlighted];
实践证实,github
UIControlStateHighlighted
跟 UIControlStateHighlighted | UIControlStateDisabled
UIControlStateSelected | UIControlStateHighlighted
跟 UIControlStateSelected | UIControlStateHighlighted | UIControlStateDisabled
效果是同样的,相互覆盖掉。
其实也好理解,由于 UIControlStateDisabled
与 UIControlStateHighlighted
原本语义上就不该该共存,因此剩下六种可用的状态组合。另外,在实践中发现,当某个状态没有设置样式时,它会以 Normal
状态的样式兜底,所以在平常开发中,咱们最好将全部用到的状态都设置上对应的样式。objective-c
有了以上组合后,咱们基本上能够覆盖 90% 的平常开发,可是若是须要用到更多状态呢?
咱们在开发 音街 的我的主页时就遇到了状态不够用的问题,对一个关注按钮,它有如下几种不一样的状态(以下图):app
这样一来用户能够操做的状态就有三种了,并且每种可操做的状态都有相应的高亮样式,因而咱们没法仅仅用 selected
状态来表示是否已经关注。对于这种需求,一个比较容易想到的办法是在不一样数据下,修改同一种状态下的样式:测试
[button setTitle:@"关注" forState:UIControlStateNormal]; [button setTitle:@"已关注" forState:UIControlStateSelected]; // 关注状态变化时 button.selected = YES; if (/* 对方也关注了我 */) { [button setTitle:@"互相关注" forState:UIControlStateSelected]; }
需求是实现了,但控件的使用上再也不简单,咱们不能在初始化时设置完全部的状态,而后以数据驱动状态,状态驱动样式了,而要增长其余逻辑,而且这种增长很容易产生 Bug。
有没有更好的办法来自定义状态,以实现==样式只设置一次==?
回头看一下 UIControlState
的定义,有一个 UIControlStateApplication
好像历来没有用过,是否是能够用来自定义呢?
咱们重用 selected
状态做为咱们的已关注 followed
状态,同时新增 loading
关注中状态,和 mutual
互相关注状态。atom
enum { NKControlStateFollowed = UIControlStateSelected, NKControlStateMutual = 1 << 16 | UIControlStateSelected, NKControlStateLoading = 1 << 17 | UIControlStateDisabled, }; @interface NKLoadingButton : UIButton @property (nonatomic, getter=isLoading) BOOL loading; @property (nonatomic) UIActivityIndicatorView *spinnerView; @end @interface NKFollowButton : NKLoadingButton @property (nonatomic, getter=isMutual) BOOL mutual; @end
这里的定义须要做如下说明:
首先,为何作移位 16 的操做?由于 UIControlStateApplication
的值是 0x00FF0000,移位 16 (16 到 23 均为合法值)正好让状态位落在它的区间内。
其次,loading
时用户应该是不能点击操做的,因此它要 或 上 disabled
状态,mutual
时必定是已经 followed
的了(即 selected
),因此它要 或 上 selected
。
最后,loading
状态应该其余地方也能复用,所以在继承关系上单独又拆了一层 NKLoadingButton
。NKLoadingButton
的实现比较简单,须要注意的是,咱们要重写 -setEnabled:
方法让它在 loading
时同时处于不可点击状态。spa
@implementation NKLoadingButton - (UIControlState)state { UIControlState state = [super state]; if (self.isLoading) { state |= NKControlStateLoading; } return state; } - (void)setEnabled:(BOOL)enabled { super.enabled = !_loading && enabled; } - (void)setLoading:(BOOL)loading { if (_loading != loading) { _loading = loading; super.enabled = !loading; if (loading) { [self.spinnerView startAnimating]; } else { [self.spinnerView stopAnimating]; } [self setNeedsLayout]; [self invalidateIntrinsicContentSize]; } } @end
NKFollowButton
的实现以下:
@implementation NKFollowButton - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setTitle:@"关注" forState:UIControlStateNormal]; [self setTitle:@"已关注" forState:UIControlStateSelected]; [self setTitle:@"已关注" forState:UIControlStateSelected | UIControlStateHighlighted]; [self setTitle:@"互相关注" forState:NKControlStateMutual]; [self setTitle:@"互相关注" forState:NKControlStateMutual | UIControlStateHighlighted]; [self setTitle:@"" forState:NKControlStateLoading]; [self setTitle:@"" forState:NKControlStateLoading | UIControlStateSelected]; [self setTitle:@"" forState:NKControlStateMutual | NKControlStateLoading]; // 如下省略颜色相关设置 } return self; } - (UIControlState)state { UIControlState state = [super state]; if (self.isMutual) { state |= NKControlStateMutual; } return state; } - (void)setSelected:(BOOL)selected { super.selected = selected; if (!selected) { self.mutual = NO; } } - (void)setMutual:(BOOL)mutual { if (_mutual != mutual) { _mutual = mutual; if (mutual) { self.selected = YES; } [self setNeedsLayout]; [self invalidateIntrinsicContentSize]; } } @end
咱们须要重写 -state
方法让外界拿到完整、正确的值,重写 -setSelected:
方法和 -setMutual:
方法,让它们在某些条件下互斥,某些条件下统一。
如此,咱们实现了只在 -init
中设置一次样式,后续仅仅依据服务端返回的数据修改 .selected
.loading
.mutual
的值便可!
本文从单一状态,到组合状态,到自定义状态层层深刻了介绍了 UIButton
的状态在平常开发中的应用,只用状态来驱动 UI 一直是程序员开发中的美好设想,本文算是从一个基本控件上给出了实现参考。另外,咱们在查看一些系统提供的 API 时,必定要多思考苹果这么设计的意图是什么?他们但愿咱们怎么使用,以及如何正确使用?
本文发布自 网易云音乐大前端团队,文章未经受权禁止任何形式的转载。咱们常年招收前端、iOS、Android,若是你准备换工做,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!