UIButton 狀態新解

題圖

本文做者:譚歆

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 值便可,使用上比較方便。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

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];

實踐證實,github

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

效果是同樣的,相互覆蓋掉。
ControlState
其實也好理解,由於 UIControlStateDisabledUIControlStateHighlighted 原本語義上就不該該共存,因此剩下六種可用的狀態組合。另外,在實踐中發現,當某個狀態沒有設置樣式時,它會以 Normal 狀態的樣式兜底,所以在平常開發中,咱們最好將全部用到的狀態都設置上對應的樣式。objective-c

0x2 自定義狀態

有了以上組合後,咱們基本上能夠覆蓋 90% 的平常開發,可是若是須要用到更多狀態呢?
咱們在開發 音街 的我的主頁時就遇到了狀態不夠用的問題,對一個關注按鈕,它有如下幾種不一樣的狀態(以下圖):app

  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 互相關注狀態。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 的值便可!

0x3 總結

本文從單一狀態,到組合狀態,到自定義狀態層層深刻了介紹了 UIButton 的狀態在平常開發中的應用,只用狀態來驅動 UI 一直是程序員開發中的美好設想,本文算是從一個基本控件上給出了實現參考。另外,咱們在查看一些系統提供的 API 時,必定要多思考蘋果這麼設計的意圖是什麼?他們但願咱們怎麼使用,以及如何正確使用?

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!
相關文章
相關標籤/搜索