本文做者:譚歆
做爲 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!