UIAppearance 使用指北
UIAppearance 的簡介
在 UIAppearance 出現之前,開發者如果想統一修改 app 內某一個控制元件的 UI 樣式時,只能通過去修改每個控制元件的例項屬性,對於只有幾個例項的 UI 控制元件來說,這樣的修改還可以接受,但如果整個 app 中有幾十個,甚至上百個例項的時候,這樣的修改就顯得相當笨拙了,當然你也可以考慮使用一些黑魔法來實現,不過這或多或少都給開發者帶來了不少麻煩。
除了上面提到的場景外,還有一種場景就是在 app 內提供多種多樣的主題來滿足使用者的需求,例如手淘在 app 內提供的主題切換功能。
上面這兩個實際開發中的應用場景都對映出這樣一個問題:如何在整個 app 中高效,統一,即時
的定製 UI 控制元件樣式。
在 iOS 5.0 之後,Apple 為開發者提供了名為 UIAppearance 和 UIAppearanceContainer 的類,它們就是蘋果為開發者提供的一種官方解決方案。
為了在現有的 UIKit 框架裡面去做這個事,蘋果的思路是: 讓 UIAppearance 成為一個可以返回代理的協議,通過它可以把任何配置轉發給特定類的例項。
但為什麼不直接在 UIView 裡面搞個屬性或者方法來做這件事兒呢?
因為諸如 UIBarButtonItem 這樣的控制元件並不是 UIView 的子類,它只是持有一個檢視例項而已。通過這種設計,UIAppearance 可以處理所有型別的 UI 控制元件,無論它是 UIView 的子類,還是包含了檢視例項的非 UIView 控制元件。
閱讀 UIAppearance 和 UIAppearanceContainer 的標頭檔案
我們剛才說過 UIApearance 實際上是一個協議,該協議需實現以下幾個方法:
+ (instancetype)appearance;
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait NS_AVAILABLE_IOS(8_0);
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 已經廢棄的兩個方法
+ (instancetype)appearanceWhenContainedIn:(nullable Class <UIAppearanceContainer>)ContainerClass, ... NS_REQUIRES_NIL_TERMINATION NS_DEPRECATED_IOS(5_0, 9_0, "Use +appearanceWhenContainedInInstancesOfClasses: instead") __TVOS_PROHIBITED;
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedIn:(nullable Class <UIAppearanceContainer>)ContainerClass, ... NS_REQUIRES_NIL_TERMINATION NS_DEPRECATED_IOS(8_0, 9_0, "Use +appearanceForTraitCollection:whenContainedInInstancesOfClasses: instead") __TVOS_PROHIBITED;
複製程式碼
在這個協議中,需要簡單解釋一下第 3 和第 4 個方法,這個兩個方法是用於解決 Size Classes 的問題而誕生的,通過這兩個 API,我們可以控制在不同螢幕尺寸下的樣式。
另外一個與之對應的協議是 UIAppearanceContainer,該協議並沒有任何約定方法。因為它只是作為一個容器。
常見的,如 UIView 實現了 UIAppearance 這兩種協議,既可以獲取外觀代理,也可以作為外觀容器。而 UIViewController 則是僅實現了 UIAppearanceContainer 協議,很簡單,它本身是控制器而不是 view,作為容器,為 UIView 等服務。
UIAppearance 的使用
哪些屬性可以被 UIAppearance 呼叫?
在 UIKit 中被 UI_APPEARANCE_SELECTOR
巨集標註的屬性可以被 UIAppearance 呼叫。 例如 UIBarButtonItem 裡的 tintColor 屬性。
@property(nullable, nonatomic,strong) UIColor *tintColor NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
複製程式碼
在 swift 中沒有巨集的概念,所以屬性無法被
UI_APPEARANCE_SELECTOR
標註,如果想讓某個屬性支援 UIAppearance 可以為該屬性使用 dynamic 關鍵字
何使用 UIAppearance 修改 UI 控制元件的屬性?
UIAppearance 不僅可以修改某一型別控制元件的全部例項,也可以修改部分例項,開發者只需要使用正確的 API 即可。還是以 UIBarButtonItem 為例。
當我們想改變所有 UIBarButtonItem 例項的 tintColor 時,程式碼如下:
[[UIBarButtonItem appearance] setTintColor:myColor];
複製程式碼
當我們想在某些指定容器類裡改變 UIBarButtonItem 的 tintColor 時,我們可以這麼做:
[[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]] setTintColor:myColor];
複製程式碼
UIAppearance 裡與 UITraitCollection 相關的兩個方法與上面兩個方法的使用規則相似,在這裡就不做贅述了
如何讓 UIAppearance 生效?
使用 UIAppearance 只有在檢視新增到 window 時才會生效,對於已經在 window 中的檢視並不會生效。所以,對於已經在 window 裡的檢視,可以採用從檢視裡移除並再次新增回去的方法使得 UIAppearance 的設定生效。
所以如果你發現自己的設定沒有生效的話,不妨參考下這個規則。
如何讓自定義 view 的屬性支援 UIAppearance
在實際使用過程中,我們絕大多數的檢視類都繼承自 UIView,UIView 的容器也基本上是 UIView 或 UIController,所以基本不需要開發者去實現這兩個協議。
對於需要支援使用 UIAppearance 來設定的屬性,在屬性後增加 UI_APPEARANCE_SELECTOR
巨集宣告即可。
需要說明的是,在遵循 UIAppearanceContainer 協議的類中,宣告與 UIAppearance 相關屬性的方法時,要遵循兩個程式碼規範:
- 在方法的最後用
UI_APPEARANCE_SELECTOR 標註
- 方法命名要遵守下面的格式
//You may have no axes or as many as you like for any property.
- (void)setProperty:(PropertyType)property forAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;
- (PropertyType)propertyForAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;
//Example
- (void)setBackgroundImage:(nullable UIImage *)backgroundImage forState:(UIControlState)state barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
- (nullable UIImage *)backgroundImageForState:(UIControlState)state barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
複製程式碼
UIAppearance 的探討
Apple 官方對於 UIAppearance 的介紹並不多,網上的文章也大多都停留在使用層面上,今天我們來深入討論一下 UIAppearance 內部的祕密。
沒有什麼卵用的 UI_APPEARANCE_SELECTOR
檢視 Apple 的官方文件後,你就會發現它其實什麼也沒幹…..
#define UI_APPEARANCE_SELECTOR
複製程式碼
既然它什麼都沒幹,那麼對於沒有被 UI_APPEARANCE_SELECTOR
標記的屬性,我們是否也能用 UIAppearance 進行統一設定呢?
答案是可以的。
此時你內心充滿了疑惑,
我們不妨將這個問題分成兩個部分:
- 第一部分就是為什麼會有這個巨集定義,如果真的沒有它會有什麼問題麼?
- 第二部分就是 UIAppearance 是如何實現統一修改控制元件樣式的呢?
真沒問題麼?
事實證明, UIAppearance 確實可以對一些沒有被 UI_APPEARANCE_SELECTOR
標記的屬性進行設定,甚至某些方法也是支援的 UIAppearance,例如 UISegmentedControl 的 setWidth:forSegmentAtIndex
方法。
但這種做法應該被避免,原因很簡單:這些沒有被標記的屬性不一定會在未來的 iOS 版本中適用。
在 Understanding UIAppearance 這篇文章裡也給出了同樣的觀點。
如何實現的?
那麼 UIAppearance 到底是如何實現的呢?國外的大神們已經對此有了研究結論,我直接給出連結,感興趣的朋友可以閱讀一下 UIAppearance and Custom Views。
友善的提醒!
在自定義 View 中使用 UIAppearance 還是有一些需要注意的事項,具體的內容可以參考 UIAppearance for Custom Views
參考文獻
- UIAppearance: nshipster.com/uiappearanc…
- UIAppearance for Custom Views: petersteinberger.com/blog/2013/u…
- Understanding UIAppearance: johnpetitto.com/understandi…
- UIAppearance and Custom Views: www.logicality.co/2012/10/08/…
- iOS UIAppearance 探祕: hyancat.com/posts/2016/…
我的部落格原文:sketchk.xyz/2018/01/25/…