UIAppearance漫談
前言
在一些app中會涉及到更改外觀設定的功能,最普遍的就是夜間模式和白天模式的切換,而對於外觀的更改必定是一個全域性的東西。在iOS5以前,想要實現這樣的效果是比較困難的,而再iOS5的時候Apple推出了UIAppearance
,使得外觀的自定義更加容易實現。
通常某個app都有自己的主題外觀,而在自定義導航欄的時候或許是使用到如下面的程式碼:
[UINavigationBar appearance].barTintColor = [UIColor redColor];
或者
[[UIBarButtonItem appearance] setTintColor:[UIColor redColor]];
這樣使用appearance
的好處就顯而易見了,因為這個設定是一個全域性的效果,一處設定之後在其他地方都無需再設定。實際上,appearance
的作用就是統一外觀設定。
那是否是所有的控制元件或者屬性都可以這樣設定尼?
實際上能使用appearance的地方是在方法或者屬性後面有UI_APPEARANCE_SELECTOR
巨集的地方
@property(nonatomic,assign) UIBarStyle barStyle UI_APPEARANCE_SELECTOR
- (void)setTitleTextAttributes:(nullable NSDictionary<NSString *,id> *)attributes forState:(UIControlState)state NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
簡單使用
如果我們自定義的檢視也想要一個全域性的外觀設定,那麼使用UIAppearancel來實現非常的方便,接下來就以一個小demo實現。
自定義一個繼承自UIView
的CardView,CardView
中新增兩個SubView
:leftView
和rightView
,高度和CardView一樣,寬度分別佔據一半。
然後在.h檔案中提供修改兩個子檢視顏色的API,並新增UI_APPEARANCE_SELECTOR巨集
@property (nonatomic, strong)UIColor * leftColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong)UIColor * rightColor UI_APPEARANCE_SELECTOR;
在.m檔案中重寫他們的setter方法設定兩個子檢視的顏色
- (void)setLeftColor:(UIColor *)leftColor {
_leftColor = leftColor;
self.leftView.backgroundColor = _leftColor;
}
- (void)setRightColor:(UIColor *)rightColor {
_rightColor = rightColor;
self.rightView.backgroundColor = _rightColor;
}
提供兩個VC,在第一個VC的viewDidLoad方法中進行全域性的顏色設定
- (void)viewDidLoad {
[super viewDidLoad];
[CardView appearance].leftColor = [UIColor redColor];
[CardView appearance].rightColor = [UIColor yellowColor];
}
分別在兩個VC的touchesBegan
方法中初始化和新增CardView檢視
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CardView * cardView = [[CardView alloc]initWithFrame:CGRectMake(20, 100, 200, 100)];
[self.view addSubview:cardView];
}
然後執行之後發現兩個VC中的CardView的顏色效果是相同的。
UIAppearance修改某一型別控制元件的全部例項和部分例項
當然UIAppearance不僅可以修改某一型別控制元件的全部例項,也可以修改部分例項,開發者只需要使用正確的 API 即可。
比如之前我們在demo中的第一個介面改變CardView
的leftColor
的全部例項的時候是這樣做的
[CardView appearance].leftColor = [UIColor redColor];
這是使用了這個API
+ (instancetype)appearance;
如果我只想在修改部分例項需要使用另外的API
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
比如如果第二個VC是以presentViewController
的方式跳轉的,只想修改第一個介面上的CardView
的leftColor
可以在上述程式碼後面增加如下程式碼:
[CardView appearanceWhenContainedInInstancesOfClasses:@[[UINavigationController class]]].leftColor = [UIColor greenColor];
執行之後第一個介面的效果為:
第二個介面不受影響。
深入剖析UIAppearance
會使用某個東西來達到效果只是一個初步的學習,接下來去看看UIAppearance究竟是一個什麼東西。
檢視API發現iOS5.0之後提供的不僅是UIAppearance
,還有另外一個叫做UIAppearanceContainer
的類,實際上他們都是protocol
@protocol UIAppearanceContainer <NSObject> @end
@protocol UIAppearance <NSObject>
...
...
@end
顯然蘋果的思路是:讓 UIAppearance 成為一個可以返回代理的協議,通過它可以把任何配置轉發給特定類的例項。
這樣做的好處是:UIAppearance 可以處理所有型別的UI控制元件,無論它是 UIView 的子類,還是包含了檢視例項的非 UIView 控制元件。
UIAppearance和UIAppearanceContainer的API
使用UIApearance 協議(Protocol)需實現這幾個方法:
// 返回接受外觀設定的代理
+ (instancetype)appearance;
// 當出現在某個類的出現時候才會改變
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 針對不同 trait 下的應用的 apperance 進行很簡單的設定
+ (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;
對於後面兩個appearanceForTraitCollection
方法是用於解決 Size Classes 的問題而誕生的,通過這兩個API,我們可以控制在不同螢幕尺寸下的樣式。
而沒有內容的UIAppearanceContainer
Protocol是什麼尼?
UIAppearanceContainer
協議並沒有任何約定方法。因為它只是作為一個容器。
比如 UIView 實現了 UIAppearance的協議,既可以獲取外觀代理,也可以作為外觀容器。而 UIViewController 則是僅實現了 UIAppearanceContainer 協議,很簡單,它本身是控制器而不是 view,作為容器,為UIView等服務。
事實上 所有的檢視類都繼承自 UIView,UIView 的容器也基本上是 UIView 或 UIViewController,基本不需要自己去實現這兩個協議。對於需要支援使用 appearance 來設定的屬性,在屬性後增加 UI_APPEARANCE_SELECTOR 巨集宣告即可。
UIAppearance深入挖掘
接下來去看看UIAppearance的呼叫過程。
繼續使用之前的demo,在兩個setter方法上加上斷點
執行的時候會發現viewDidLoad方法裡面的這兩句程式碼並沒有呼叫setter方法
[CardView appearance].leftColor = [UIColor redColor];
[CardView appearance].rightColor = [UIColor yellowColor];
而當CardView檢視被加到主檢視(容器)的時候才走了setter方法,這說明:
在通過appearance設定屬性的時候,並不會生成例項,立即賦值,而需要檢視被加到檢視tree中的時候才會生產例項。
所以使用 UIAppearance 只有在檢視新增到 window 時才會生效,對於已經在 window 中的檢視並不會生效。因此,對於已經在 window 裡的檢視,可以採用從檢視裡移除並再次新增回去的方法使得 UIAppearance 的設定生效。
方法的呼叫棧如下:
不難看出appearance 設定的屬性,都以 Invocation 的形式儲存到 _UIApperance 類中,等到檢視樹 performUpdates 的時候,會去檢查有沒有相關的屬性設定,有則 invoke。所以使用 UIAppearance 只有在檢視新增到 window 時才會生效。
總結如下:
每一個實現 UIAppearance 協議的類,都會有一個 _UIApperance 例項,儲存著這個類通過 appearance 設定屬性的 invocations,在該類被新增或應用到檢視樹上的時候,它會檢查並呼叫這些屬性設定。這樣就實現了讓所有該類的例項都自動統一屬性。appearance 只是起到一個代理作用,在特定的時機,讓代理替所有例項做同樣的事。
虛無縹緲的UI_APPEARANCE_SELECTOR
前面說到使用的時候需要在屬性後增加 UI_APPEARANCE_SELECTOR 巨集宣告支援使用 UIAppearance 來設定的屬性。但是會發現它其實什麼也沒幹:
#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))
既然它什麼多沒做,那麼我們在demo程式碼中將UI_APPEARANCE_SELECTOR
去掉試試。結果會發現效果是一樣的。但是蘋果官方說了這個是must be:
To support appearance customization, a class must conform to the UIAppearanceContainer protocol and relevant accessor methods must be marked with UI_APPEARANCE_SELECTOR.
所以還是加上比較號,或許在未來的iOS版本中,這些沒有被UI_APPEARANCE_SELECTOR所marked的屬性就不能使用UIAppearance了尼。
相關文章
- Flink漫談
- 漫談逆向工程
- 漫談全景分割
- 漫談負載均衡負載
- Hadoop Map Reduce 漫談Hadoop
- 隨機數漫談隨機
- 漫談CUDA優化優化
- 【MySQL】四、Insert buffer 漫談MySql
- 漫談 SLAM 技術(上)SLAM
- PHP安全性漫談PHP
- iOS APP 架構漫談iOSAPP架構
- GIS資料漫談(三)
- 新特性:postgresql的vacuum漫談SQL
- [前端漫談_1] 從 for of 聊到 Generator前端
- 漫談Web快取架構Web快取架構
- 效能優化漫談之一優化
- 效能優化漫談之二優化
- 漫談計算機架構計算機架構
- oracle數值型別漫談Oracle型別
- 漫談計算機編碼計算機
- 漫談Hadoop的思想之源:GoogleHadoopGo
- babel知識體系漫談Babel
- 漫談網站優化提速網站優化
- 漫談程式和執行緒執行緒
- [前端漫談_3] 從 filter 聊到 Promise前端FilterPromise
- 設計模式漫談之策略模式設計模式
- 漫談Android元件化及Web化Android元件化Web
- 設計模式漫談之模板方法設計模式
- 設計模式漫談之命令模式設計模式
- 漫談哲學與程式設計程式設計
- 設計模式漫談之代理模式設計模式
- 以前端視角,漫談「雲端」前端
- 漫談負載均衡演算法負載演算法
- 漫談對大資料的思考大資料
- 閱讀筆記——架構漫談筆記架構
- 【森城市】GIS資料漫談(二)
- iOS國際化(多語言)漫談iOS
- 區塊鏈2.0以太坊漫談(1)區塊鏈