歡迎登錄個人我的網站: www.sketchk.xyzswift
在 UIAppearance 出現以前,開發者若是想統一修改 app 內某一個控件的 UI 樣式時,只能經過去修改每一個控件的實例屬性,對於只有幾個實例的 UI 控件來講,這樣的修改還能夠接受,但若是整個 app 中有幾十個,甚至上百個實例的時候,這樣的修改就顯得至關笨拙了,固然你也能夠考慮使用一些黑魔法來實現,不過這或多或少都給開發者帶來了很多麻煩。bash
除了上面提到的場景外,還有一種場景就是在 app 內提供多種多樣的主題來知足用戶的需求,例如手淘在 app 內提供的主題切換功能。app
上面這兩個實際開發中的應用場景都映射出這樣一個問題:如何在整個 app 中高效,統一,即時
的定製 UI 控件樣式。框架
在 iOS 5.0 以後,Apple 爲開發者提供了名爲 UIAppearance 和 UIAppearanceContainer 的類,它們就是蘋果爲開發者提供的一種官方解決方案。post
爲了在現有的 UIKit 框架裏面去作這個事,蘋果的思路是: 讓 UIAppearance 成爲一個能夠返回代理的協議,經過它能夠把任何配置轉發給特定類的實例。網站
但爲何不直接在 UIView 裏面搞個屬性或者方法來作這件事兒呢?ui
由於諸如 UIBarButtonItem 這樣的控件並非 UIView 的子類,它只是持有一個視圖實例而已。經過這種設計,UIAppearance 能夠處理全部類型的 UI 控件,不管它是 UIView 的子類,仍是包含了視圖實例的非 UIView 控件。atom
咱們剛纔說過 UIApearance 其實是一個協議,該協議需實現如下幾個方法:spa
+ (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 等服務。
在 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 不只能夠修改某一類型控件的所有實例,也能夠修改部分實例,開發者只須要使用正確的 API 便可。仍是以 UIBarButtonItem 爲例。
當咱們想改變全部 UIBarButtonItem 實例的 tintColor 時,代碼以下:
[[UIBarButtonItem appearance] setTintColor:myColor];
複製代碼
當咱們想在某些指定容器類裏改變 UIBarButtonItem 的 tintColor 時,咱們能夠這麼作:
[[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]] setTintColor:myColor];
複製代碼
UIAppearance 裏與 UITraitCollection 相關的兩個方法與上面兩個方法的使用規則類似,在這裏就不作贅述了
使用 UIAppearance 只有在視圖添加到 window 時纔會生效,對於已經在 window 中的視圖並不會生效。因此,對於已經在 window 裏的視圖,能夠採用從視圖裏移除並再次添加回去的方法使得 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;
複製代碼
Apple 官方對於 UIAppearance 的介紹並很少,網上的文章也大多都停留在使用層面上,今天咱們來深刻討論一下 UIAppearance 內部的祕密。
UI_APPEARANCE_SELECTOR
查看 Apple 的官方文檔後,你就會發現它其實什麼也沒幹…..
#define UI_APPEARANCE_SELECTOR
複製代碼
既然它什麼都沒幹,那麼對於沒有被 UI_APPEARANCE_SELECTOR
標記的屬性,咱們是否也能用 UIAppearance 進行統一設置呢?
答案是能夠的。
此時你心裏充滿了疑惑,
咱們不妨將這個問題分紅兩個部分:
事實證實, UIAppearance 確實能夠對一些沒有被 UI_APPEARANCE_SELECTOR
標記的屬性進行設置,甚至某些方法也是支持的 UIAppearance,例如 UISegmentedControl 的 setWidth:forSegmentAtIndex
方法。
但這種作法應該被避免,緣由很簡單:這些沒有被標記的屬性不必定會在將來的 iOS 版本中適用。
在 Understanding UIAppearance 這篇文章裏也給出了一樣的觀點。
那麼 UIAppearance 究竟是如何實現的呢?國外的大神們已經對此有了研究結論,我直接給出連接,感興趣的朋友能夠閱讀一下 UIAppearance and Custom Views。
在自定義 View 中使用 UIAppearance 仍是有一些須要注意的事項,具體的內容能夠參考 UIAppearance for Custom Views
個人博客原文:sketchk.xyz/2018/01/25/…