前言
在一些app中會(huì)涉及到更改外觀設(shè)置的功能瓮下,最普遍的就是夜間模式和白天模式的切換记劈,而對(duì)于外觀的更改必定是一個(gè)全局的東西盖腕。在iOS5以前赫冬,想要實(shí)現(xiàn)這樣的效果是比較困難的,而再iOS5的時(shí)候Apple推出了UIAppearance
溃列,使得外觀的自定義更加容易實(shí)現(xiàn)劲厌。
通常某個(gè)app都有自己的主題外觀,而在自定義導(dǎo)航欄的時(shí)候或許是使用到如下面的代碼:
[UINavigationBar appearance].barTintColor = [UIColor redColor];
或者
[[UIBarButtonItem appearance] setTintColor:[UIColor redColor]];
這樣使用appearance
的好處就顯而易見了哭廉,因?yàn)檫@個(gè)設(shè)置是一個(gè)全局的效果脊僚,一處設(shè)置之后在其他地方都無需再設(shè)置。實(shí)際上遵绰,appearance
的作用就是統(tǒng)一外觀設(shè)置辽幌。
那是否是所有的控件或者屬性都可以這樣設(shè)置尼?
實(shí)際上能使用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;
簡(jiǎn)單使用
如果我們自定義的視圖也想要一個(gè)全局的外觀設(shè)置椿访,那么使用UIAppearancel來實(shí)現(xiàn)非常的方便乌企,接下來就以一個(gè)小demo實(shí)現(xiàn)。
自定義一個(gè)繼承自UIView
的CardView成玫,CardView
中添加兩個(gè)SubView
:leftView
和rightView
加酵,高度和CardView一樣,寬度分別占據(jù)一半哭当。
然后在.h文件中提供修改兩個(gè)子視圖顏色的API,并添加UI_APPEARANCE_SELECTOR宏
@property (nonatomic, strong)UIColor * leftColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong)UIColor * rightColor UI_APPEARANCE_SELECTOR;
在.m文件中重寫他們的setter方法設(shè)置兩個(gè)子視圖的顏色
- (void)setLeftColor:(UIColor *)leftColor {
_leftColor = leftColor;
self.leftView.backgroundColor = _leftColor;
}
- (void)setRightColor:(UIColor *)rightColor {
_rightColor = rightColor;
self.rightView.backgroundColor = _rightColor;
}
提供兩個(gè)VC猪腕,在第一個(gè)VC的viewDidLoad方法中進(jìn)行全局的顏色設(shè)置
- (void)viewDidLoad {
[super viewDidLoad];
[CardView appearance].leftColor = [UIColor redColor];
[CardView appearance].rightColor = [UIColor yellowColor];
}
分別在兩個(gè)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];
}
然后運(yùn)行之后發(fā)現(xiàn)兩個(gè)VC中的CardView的顏色效果是相同的。
UIAppearance修改某一類型控件的全部實(shí)例和部分實(shí)例
當(dāng)然UIAppearance不僅可以修改某一類型控件的全部實(shí)例钦勘,也可以修改部分實(shí)例陋葡,開發(fā)者只需要使用正確的 API 即可。
比如之前我們?cè)赿emo中的第一個(gè)界面改變CardView
的leftColor
的全部實(shí)例的時(shí)候是這樣做的
[CardView appearance].leftColor = [UIColor redColor];
這是使用了這個(gè)API
+ (instancetype)appearance;
如果我只想在修改部分實(shí)例需要使用另外的API
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
比如如果第二個(gè)VC是以presentViewController
的方式跳轉(zhuǎn)的彻采,只想修改第一個(gè)界面上的CardView
的leftColor
可以在上述代碼后面增加如下代碼:
[CardView appearanceWhenContainedInInstancesOfClasses:@[[UINavigationController class]]].leftColor = [UIColor greenColor];
運(yùn)行之后第一個(gè)界面的效果為:
第二個(gè)界面不受影響腐缤。
深入剖析UIAppearance
會(huì)使用某個(gè)東西來達(dá)到效果只是一個(gè)初步的學(xué)習(xí),接下來去看看UIAppearance究竟是一個(gè)什么東西肛响。
查看API發(fā)現(xiàn)iOS5.0之后提供的不僅是UIAppearance
岭粤,還有另外一個(gè)叫做UIAppearanceContainer
的類,實(shí)際上他們都是protocol
@protocol UIAppearanceContainer <NSObject> @end
@protocol UIAppearance <NSObject>
...
...
@end
顯然蘋果的思路是:讓 UIAppearance 成為一個(gè)可以返回代理的協(xié)議特笋,通過它可以把任何配置轉(zhuǎn)發(fā)給特定類的實(shí)例剃浇。
這樣做的好處是:UIAppearance 可以處理所有類型的UI控件,無論它是 UIView 的子類,還是包含了視圖實(shí)例的非 UIView 控件偿渡。
UIAppearance和UIAppearanceContainer的API
使用UIApearance 協(xié)議(Protocol)需實(shí)現(xiàn)這幾個(gè)方法:
// 返回接受外觀設(shè)置的代理
+ (instancetype)appearance;
// 當(dāng)出現(xiàn)在某個(gè)類的出現(xiàn)時(shí)候才會(huì)改變
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 針對(duì)不同 trait 下的應(yīng)用的 apperance 進(jìn)行很簡(jiǎn)單的設(shè)定
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait NS_AVAILABLE_IOS(8_0);
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 已經(jīng)廢棄的方法
+ (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;
對(duì)于后面兩個(gè)appearanceForTraitCollection
方法是用于解決 Size Classes 的問題而誕生的臼寄,通過這兩個(gè)API霸奕,我們可以控制在不同屏幕尺寸下的樣式溜宽。
而沒有內(nèi)容的UIAppearanceContainer
Protocol是什么尼?
UIAppearanceContainer
協(xié)議并沒有任何約定方法质帅。因?yàn)樗皇亲鳛橐粋€(gè)容器适揉。
比如 UIView 實(shí)現(xiàn)了 UIAppearance的協(xié)議,既可以獲取外觀代理煤惩,也可以作為外觀容器嫉嘀。而 UIViewController 則是僅實(shí)現(xiàn)了 UIAppearanceContainer 協(xié)議,很簡(jiǎn)單魄揉,它本身是控制器而不是 view剪侮,作為容器,為UIView等服務(wù)洛退。
事實(shí)上 所有的視圖類都繼承自 UIView瓣俯,UIView 的容器也基本上是 UIView 或 UIViewController,基本不需要自己去實(shí)現(xiàn)這兩個(gè)協(xié)議兵怯。對(duì)于需要支持使用 appearance 來設(shè)置的屬性彩匕,在屬性后增加 UI_APPEARANCE_SELECTOR 宏聲明即可。
UIAppearance深入挖掘
接下來去看看UIAppearance的調(diào)用過程媒区。
繼續(xù)使用之前的demo驼仪,在兩個(gè)setter方法上加上斷點(diǎn)
運(yùn)行的時(shí)候會(huì)發(fā)現(xiàn)viewDidLoad方法里面的這兩句代碼并沒有調(diào)用setter方法
[CardView appearance].leftColor = [UIColor redColor];
[CardView appearance].rightColor = [UIColor yellowColor];
而當(dāng)CardView視圖被加到主視圖(容器)的時(shí)候才走了setter方法,這說明:
在通過appearance設(shè)置屬性的時(shí)候袜漩,并不會(huì)生成實(shí)例绪爸,立即賦值,而需要視圖被加到視圖tree中的時(shí)候才會(huì)生產(chǎn)實(shí)例宙攻。
所以使用 UIAppearance 只有在視圖添加到 window 時(shí)才會(huì)生效奠货,對(duì)于已經(jīng)在 window 中的視圖并不會(huì)生效。因此粘优,對(duì)于已經(jīng)在 window 里的視圖仇味,可以采用從視圖里移除并再次添加回去的方法使得 UIAppearance 的設(shè)置生效。
方法的調(diào)用棧如下:
不難看出appearance 設(shè)置的屬性雹顺,都以 Invocation 的形式存儲(chǔ)到 _UIApperance 類中丹墨,等到視圖樹 performUpdates 的時(shí)候,會(huì)去檢查有沒有相關(guān)的屬性設(shè)置嬉愧,有則 invoke贩挣。所以使用 UIAppearance 只有在視圖添加到 window 時(shí)才會(huì)生效。
總結(jié)如下:
每一個(gè)實(shí)現(xiàn) UIAppearance 協(xié)議的類,都會(huì)有一個(gè) _UIApperance 實(shí)例王财,保存著這個(gè)類通過 appearance 設(shè)置屬性的 invocations卵迂,在該類被添加或應(yīng)用到視圖樹上的時(shí)候,它會(huì)檢查并調(diào)用這些屬性設(shè)置绒净。這樣就實(shí)現(xiàn)了讓所有該類的實(shí)例都自動(dòng)統(tǒng)一屬性见咒。appearance 只是起到一個(gè)代理作用,在特定的時(shí)機(jī)挂疆,讓代理替所有實(shí)例做同樣的事改览。
虛無縹緲的UI_APPEARANCE_SELECTOR
前面說到使用的時(shí)候需要在屬性后增加 UI_APPEARANCE_SELECTOR 宏聲明支持使用 UIAppearance 來設(shè)置的屬性。但是會(huì)發(fā)現(xiàn)它其實(shí)什么也沒干:
#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))
既然它什么多沒做缤言,那么我們?cè)赿emo代碼中將UI_APPEARANCE_SELECTOR
去掉試試宝当。結(jié)果會(huì)發(fā)現(xiàn)效果是一樣的。但是蘋果官方說了這個(gè)是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.
所以還是加上比較號(hào)胆萧,或許在未來的iOS版本中庆揩,這些沒有被UI_APPEARANCE_SELECTOR所marked的屬性就不能使用UIAppearance了尼。