本文作者:谭歆

0x0 控件状态

作为 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 值即可,应用上比拟不便。

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 只有四种状态可用吗?实在开发中,控件的状态可能很多,四种是肯定不够用的。

0x1 状态组合

首先咱们留神到,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];

实践证明,

  • UIControlStateHighlightedUIControlStateHighlighted | UIControlStateDisabled
  • UIControlStateSelected | UIControlStateHighlightedUIControlStateSelected | UIControlStateHighlighted | UIControlStateDisabled

成果是一样的,互相笼罩掉。

其实也好了解,因为 UIControlStateDisabledUIControlStateHighlighted 原本语义上就不应该共存,所以剩下六种可用的状态组合。另外,在实践中发现,当某个状态没有设置款式时,它会以 Normal 状态的款式兜底,因而在日常开发中,咱们最好将所有用到的状态都设置上对应的款式。

0x2 自定义状态

有了以上组合后,咱们基本上能够笼罩 90% 的日常开发,然而如果须要用到更多状态呢?
咱们在开发 音街 的个人主页时就遇到了状态不够用的问题,对一个关注按钮,它有以下几种不同的状态(如下图):

  1. 以后登录用户没有关注该用户
  2. 以后登录用户正在关注该用户
  3. 以后登录用户曾经关注该用户
  4. 以后登录用户与该用户相互关注


这样一来用户能够操作的状态就有三种了,而且每种可操作的状态都有相应的高亮款式,于是咱们无奈仅仅用 selected 状态来示意是否曾经关注。对于这种需要,一个比拟容易想到的方法是在不同数据下,批改同一种状态下的款式:

[button setTitle:@"关注" forState:UIControlStateNormal];[button setTitle:@"已关注" forState:UIControlStateSelected];// 关注状态变动时button.selected = YES;if (/* 对方也关注了我 */) { [button setTitle:@"相互关注" forState:UIControlStateSelected];}

需要是实现了,但控件的应用上不再简略,咱们不能在初始化时设置完所有的状态,而后以数据驱动状态,状态驱动款式了,而要减少其余逻辑,并且这种减少很容易产生 Bug
有没有更好的方法来自定义状态,以实现==款式只设置一次==?
回头看一下 UIControlState 的定义,有一个 UIControlStateApplication 如同素来没有用过,是不是能够用来自定义呢?
咱们重用 selected 状态作为咱们的已关注 followed 状态,同时新增 loading 关注中状态,和 mutual 相互关注状态。

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 时同时处于不可点击状态。

@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 的值即可!

0x3 总结

本文从繁多状态,到组合状态,到自定义状态层层深刻了介绍了 UIButton 的状态在日常开发中的利用,只用状态来驱动 UI 始终是程序员开发中的美妙构想,本文算是从一个根本控件上给出了实现参考。另外,咱们在查看一些零碎提供的 API 时,肯定要多思考苹果这么设计的用意是什么?他们心愿咱们怎么应用,以及如何正确应用?

本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!