UIAppearcance 使用指北

SketchK發表於2018-10-27

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

參考文獻

我的部落格原文:sketchk.xyz/2018/01/25/…

相關文章