UIAppearance漫談

前言

在一些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è)SubViewleftViewrightView加酵,高度和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的顏色效果是相同的。


image

image
UIAppearance修改某一類型控件的全部實(shí)例和部分實(shí)例

當(dāng)然UIAppearance不僅可以修改某一類型控件的全部實(shí)例钦勘,也可以修改部分實(shí)例陋葡,開發(fā)者只需要使用正確的 API 即可

比如之前我們?cè)赿emo中的第一個(gè)界面改變CardViewleftColor的全部實(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è)界面上的CardViewleftColor可以在上述代碼后面增加如下代碼:

[CardView appearanceWhenContainedInInstancesOfClasses:@[[UINavigationController class]]].leftColor = [UIColor greenColor];

運(yùn)行之后第一個(gè)界面的效果為:

image

第二個(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)容的UIAppearanceContainerProtocol是什么尼?

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)


image

運(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)用棧如下:

image

不難看出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了尼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末跌穗,一起剝皮案震驚了整個(gè)濱河市订晌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瞻离,老刑警劉巖腾仅,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異套利,居然都是意外死亡推励,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門肉迫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來验辞,“玉大人,你說我怎么就攤上這事喊衫〉欤” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵族购,是天一觀的道長(zhǎng)壳贪。 經(jīng)常有香客問我,道長(zhǎng)寝杖,這世上最難降的妖魔是什么违施? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮瑟幕,結(jié)果婚禮上磕蒲,老公的妹妹穿的比我還像新娘留潦。我一直安慰自己,他們只是感情好辣往,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布兔院。 她就那樣靜靜地躺著,像睡著了一般站削。 火紅的嫁衣襯著肌膚如雪坊萝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天钻哩,我揣著相機(jī)與錄音屹堰,去河邊找鬼。 笑死街氢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的睦袖。 我是一名探鬼主播珊肃,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼馅笙!你這毒婦竟也來了伦乔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤董习,失蹤者是張志新(化名)和其女友劉穎烈和,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皿淋,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡招刹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窝趣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疯暑。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哑舒,靈堂內(nèi)的尸體忽然破棺而出妇拯,到底是詐尸還是另有隱情,我是刑警寧澤洗鸵,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布越锈,位于F島的核電站,受9級(jí)特大地震影響膘滨,放射性物質(zhì)發(fā)生泄漏甘凭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一吏祸、第九天 我趴在偏房一處隱蔽的房頂上張望对蒲。 院中可真熱鬧钩蚊,春花似錦、人聲如沸蹈矮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泛鸟。三九已至蝠咆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間北滥,已是汗流浹背刚操。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留再芋,地道東北人菊霜。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像济赎,于是被迫代替她去往敵國(guó)和親鉴逞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容