原文:UIButton 状态新解 – 网易云音乐大前端

控件状态

作为 iOS 开发者,一提到控件,就不得不提到 UIButton,它做为 iOS 系统最常用的响应用户点击操作的控件,为我们提供了相当丰富的功能以及可定制性。而我们的日常工作的 80% ~ 90% 做是在与 UI打交道,处理控件在用户的不同操作下的不同状态,最简单的,比如用户没有登录时,按钮置灰不可点击,用户点击时出现一个反色效果反馈到用户等等。对常用状态的定义,系统在很早的时候就给出了:

typedef NS_OPTIONS(NSUInteger, UIControlState) {UIControlStateNormal       = 0,UIControlStateHighlighted  = 1 << 0,                  // used when UIControl isHighlighted is setUIControlStateDisabled     = 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 focusUIControlStateApplication  = 0x00FF0000,              // additional flags available for application useUIControlStateReserved     = 0xFF000000               // flags reserved for internal framework use
};

NS_OPTIONS 类型枚举
我们一般预先设置好 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 只有四种状态可用吗?真实开发中,控件的状态可能很多,四种是一定不够用的。

状态组合

首先我们注意到,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 状态的样式兜底,因此在日常开发中,我们最好将所有用到的状态都设置上对应的样式。

自定义状态

有了以上组合后,我们基本上可以覆盖 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 均为合法值)正好让状态位落在它的区间内。

所以自定义应该从16开始。
其次,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 的值即可!

总结

本文从单一状态,到组合状态,到自定义状态层层深入了介绍了 UIButton 的状态在日常开发中的应用,只用状态来驱动 UI 一直是程序员开发中的美好设想,本文算是从一个基本控件上给出了实现参考。另外,我们在查看一些系统提供的 API 时,一定要多思考苹果这么设计的意图是什么?他们希望我们怎么使用,以及如何正确使用?

UIButton状态探索和自定义相关推荐

  1. 文化袁探索专栏——自定义View实现细节

    文化袁探索专栏--Activity.Window和View三者间关系 文化袁探索专栏--View三大流程#Measure 文化袁探索专栏--View三大流程#Layout 文化袁探索专栏--消息分发机 ...

  2. 切换不同的数据状态布局,包含加载中、空数据和出错状态,可自定义状态布局

    代码地址如下: http://www.demodashi.com/demo/12318.html StatusLayoutManager 切换不同的数据状态布局,包含加载中.空数据和出错状态. git ...

  3. 《Android开发艺术探索》自定义View中关于“HorizontalScrollViewEx”的改进

    在<Android开发艺术探索>一书中自定义View一节中提到了关于一个类似横向滑动List的自定义ViewGroup:HorizontalScrollViewEx.如果你使用过的话就会发 ...

  4. php自定义返回状态码,Thinkphp6自定义状态码

    下面由thinkphp框架教程栏目给大家介绍TP6 自定义状态码的方法,希望对需要的朋友有所帮助! config 目录下新建code.php<?php return [ 'success'=&g ...

  5. promise的状态以及api介绍_Promise从入门到自定义 | 尚硅谷Promise新版视频发布!

    尚硅谷发布全新升级版前端课程,推出"5+100+3"人才培养新模式,5.5个月系统学习+100课时进阶课程+3年谷粒学院VIP课程,为技术成长持续赋能,打造前端架构师!预知详情,猛 ...

  6. iOS UIButton(按钮)

    UIButton属性 1.UIButton状态: UIControlStateNormal // 正常状态 UIControlStateHighlighted // 高亮状态 UIControlSta ...

  7. 【iOS 开发】基本 UI 控件详解 (UIButton | UITextField | UITextView | UISwitch)

    博客地址 : http://blog.csdn.net/shulianghan/article/details/50051499 ; 一. UI 控件简介 1. UI 控件分类 UI 控件分类 : 活 ...

  8. iOS新闻类App内容页技术探索

    为了更好的阅读体验,建议阅读原文 据相关数据显示,截至2017年底,中国手机新闻客户端用户规模达到6.36亿人,移动App已经成为新闻和内容传播的最重要途径之一.而伴随着行业的竞争和发展,App中的内 ...

  9. 文化袁探索专栏——消息分发机制

    文化袁探索专栏--Activity.Window和View三者间关系 文化袁探索专栏--View三大流程#Measure 文化袁探索专栏--View三大流程#Layout 文化袁探索专栏--消息分发机 ...

最新文章

  1. ReactNative ViewPageAndroid组件详解
  2. linux下软件安装与yum源码库的设置
  3. linux获取cpu核数(线程数)
  4. 互联网汽车迎新成员 Alibaba YunOS Auto冠名2016世俱杯
  5. php输出json到表格,PHP中把数据库查询结果输出为json格式
  6. 一般拦截器 serviceImpl部分
  7. mongodb 怎样检测 安装成功 以及mongodb的一些增删改查命令
  8. 破解centos7root口令
  9. VS中生成、清理项目、调试、開始运行(不调试)、Debug 和 Release等之间的差别...
  10. van-cell 取消点击_消息传来!转告父母:2021年起,取消60岁以上老年卡?
  11. vitual dom实现(转)
  12. 敏捷开发案例:用白板解决项目管理和团队沟通
  13. java代码无限弹窗制作_vbs无限弹窗制作方法
  14. mysql ocp考试准备多久_MySQL 5.7OCP考试经验分享。
  15. 软件需求分析是什么?
  16. python 回归方程及回归系数的显著性检验_回归方程及回归系数的显著性检验_stata显著性检验...
  17. 猜拳java,猜拳小游戏(Java代码实现)
  18. 测试四则运算2:Right-BICEP
  19. 苹果手机升级13无法开机_苹果11更新ios13.7卡在开机页面
  20. 用openCV取出图片中的四边形

热门文章

  1. GridView 实现LinkButton下载文件/附件
  2. 单选框-复选框重置的方法
  3. Installation error: INSTALL_FAILED_UPDATE_INCOMPATIBLE解决方法
  4. CentOS上如何把Web服务器从Apache换到nginx
  5. Task中的异常处理
  6. Android自己定义组件系列【2】——Scroller类
  7. 深入理解HTML协议
  8. 使用FoundationDB高效地将SQL数据映射到NoSQL存储系统中
  9. 网络配置命令优先级和元字符
  10. android AVD运行chrome,contentshell,chromeshell失败解决方法