一、前言
iOS 上提供了一個比較強大的工具UIAppearance,我們通過UIAppearance設置一些UI的全局效果,這樣就可以很方便的實現UI的自定義效果又能最簡單的實現統一界面風格。
+ (id)appearance ; 這個是這個協議里最重要的方法了 .
這個方法是統一全部改,比如你可以設置UIView.appearance().backgroundColor = UIColor.orange, 這樣所有View默認顏色就都是橘色。
二、主題設置的前提
為什么我們可以給它們設置主題屬性呢?哪些對象 哪些屬性 可以設置主題屬性呢?
1. 那些控件和類,可以設置主題呢?
回答:只要遵守了UIAppearance協議
的類,都可以設置主題
查看UIView的頭文件,可得,UIView可以設置主題,那么不是所有繼承UIView的控件就都可以設置主題了嗎?是的
觀看可得,不僅,只是控件可以設置主題,UIBarItem等只要遵守了UIAppearance協議的類,都可以設置主題
這里列舉了一部分:
- UIView
- UIActivitiIndicatorView
- UIBarButtonItem
- UIBarItem
- UINavgationBar
- UIPopoverControll
- UIProgressView
- UISearchBar
- UISegmentControll
- UISlider
- UISwitch
- UITabBar
- UITabBarItem
- UIToolBar
- UIViewController
2.遵守UIAppearance協議的類的,那些屬性可以設置主題呢?
通過主題對象設置屬性的前提:?屬性后面是否帶有UI_APPEARANCE_SELECTOR
的方法
- (void)setTitleTextAttributes:(NSDictionary *)attributes forState:(UIControlState)state
NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
因為并不是所以屬性可以設置主題,設置主題屬性是有前提的。
查看頭文件可得,可以設置UITabBarItem類,發現setTitleTextAttributes:可以設置UITabBarItem文字主題
- 案例:設置所有的UITabBarItem,普通與選中狀態下的文字顏色
三、運用主題appearance,是否會生效,何時會生效
1、主題會生效的場景
:先設置控件主題,后添加控件到視圖上
- 添加控件時,添加的那一刻會檢查主題,會根據主題設置控件 =》主題會生效
2、主題不會生效的場景
:先添加控件,后設置主題
- 控件已經添加,后設置主題,對以前的添加的控件不起作用了
如果先添加控件,后設置主題,主題失效,我們該如何解決呢?
最優方案:當然是改下調用順序,先設置appearance,在添加控件。
當然作為腦洞或者探究原理,也可以重新把View移除,然后在添加一次,觸發一次渲染,實測是生效的。(但是這樣的代碼,誰看誰撓頭,還是不要在上線版本中寫。)
?
三、實現原理
UIApearance
?實際上是一個協議(Protocol),我們可以用它來獲取一個類的外觀代理(Appearance Proxy)。該協議需實現這幾個方法:
+ (instancetype)appearance;
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 詳細方法見 UIKit/UIAppearance.h
另外一個與之對應的協議是?UIAppearanceContainer
,該協議并沒有任何約定方法。因為它只是作為一個容器。
常見的,如 UIView 實現了?UIAppearance
?這兩種協議,既可以獲取外觀代理,也可以作為外觀容器。 而 UIViewController 則是僅實現了?UIAppearanceContainer
?協議,很簡單,它本身是控制器而不是 view,作為容器,為 UIView 等服務。
事實上,在使用中,我們所有的視圖類都繼承自 UIView,UIView 的容器也基本上是 UIView 或 UIController,基本不需要自己去實現這兩個協議。對于需要支持使用 appearance 來設置的屬性,在屬性后增加?UI_APPEARANCE_SELECTOR
?宏聲明即可。 文檔中也有解釋?UI_APPEARANCE_SELECTOR
?用來標記屬性用于外觀代理,支持哪些類型等等。
To participate in the appearance proxy API, tag your appearance property selectors in your header with UI_APPEARANCE_SELECTOR.Appearance property selectors must be of the form:- (void)setProperty:(PropertyType)property forAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;- (PropertyType)propertyForAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;You may have no axes or as many as you like for any property. PropertyType may be any standard iOS type: id, NSInteger, NSUInteger, CGFloat, CGPoint, CGSize, CGRect, UIEdgeInsets or UIOffset. IntegerType must be either NSInteger or NSUInteger; we will throw an exception if other types are used in the axes.
翻譯一下:
要參與外觀代理 API,請在頭文件中用UI_APPEARANCE_SELECTOR標記您的外觀屬性選擇器。
外觀屬性選擇器必須采用以下形式:
- (void) setProperty:(屬性類型) property forAxis1:(整數類型) axis1 axis2:(整數類型) axis2 axisN:(整數類型) axisN;
- (屬性類型) propertyForAxis1:(整數類型) axis1 axis2:(整數類型) axis2 axisN:(整數類型) axisN;對于任何屬性,您可以沒有參數,也可以有任意多個參數。屬性類型可以是任何標準的 iOS 類型:id、NSInteger、NSUInteger、CGFloat、CGPoint、CGSize、CGRect、UIEdgeInsets 或 UIOffset。整數類型必須是 NSInteger 或 NSUInteger;如果在參數中使用其他類型,我們將拋出異常。
demo驗證原理
寫一個簡單的小 Demo,自定義 CardView,有兩個 subview: headerView 和 footerView,聲明 2 個屬性:
@property (nonatomic, strong) UIColor *headerColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) UIColor *bodyColor UI_APPEARANCE_SELECTOR;
Setter 方法都加斷點調試:
- (void)setHeaderColor:(UIColor *)headerColor
{_headerColor = headerColor;self.headerView.backgroundColor = _headerColor;
}- (void)setBodyColor:(UIColor *)bodyColor
{_bodyColor = bodyColor;self.bodyView.backgroundColor = _bodyColor;
}
在 ViewController 的 view 中加一個按鈕,點擊則創建并添加 CardView,每行代碼均加斷點:
- (IBAction)createButtonTouched:(id)senderCardView *cardView = [[CardView alloc] initWithFrame:CGRectMake(20, 100, 80, 120)];[self.view addSubview:cardView];cardView.headerColor = [UIColor greenColor];
}
另外,在較早的時候,添加 appearance 設置:
[CardView appearance].headerColor = [UIColor redColor];
[CardView appearance].bodyColor = [UIColor orangeColor];
運行發現,在通過 appearance 設置屬性的時候,并沒有調用 setter 方法,由此可知 appearance 并不會生成實例,立即賦值。當 cardView 被添加到主視圖(即視圖樹)中去的時候,才依次調用兩個 setter 方法,調用棧如下
從 15 至 11 可以看出確實是加入到視圖樹中才觸發的,從 7 至 2 可以基本猜測出,appearance 設置的屬性,都以 Invocation 的形式存儲到 _UIApperance 類中(事實上 _UIApperance 類中就有一個 _appearanceInvocations 數組),等到視圖樹 performUpdates 的時候,會去檢查有沒有相關的屬性設置,有則 invoke。(這里可以看看 NSInvocation)
緊接著,它進入了 bodyColor 的 setter
然后,當手動設置屬性的時候,它是直接進入 setter 的。
到這里,基本清晰了。
每一個實現 UIAppearance 協議的類,都會有一個 _UIApperance 實例,保存著這個類通過 appearance 設置屬性的 invocations,在該類被添加或應用到視圖樹上的時候,它會檢查并調用這些屬性設置。這樣就實現了讓所有該類的實例都自動統一屬性。
當然,如果后面又手動設置了屬性,肯定會覆蓋了。從上面可以知道,appearance 生效是在被添加到視圖樹時,所以,在此之后設置 appearance,則不會起作用,而在手動設置屬性之后被添加到視圖樹上,手動設置的會被覆蓋。appearance 只是起到一個代理作用,在特定的時機,讓代理替所有實例做同樣的事。
嘗試一下,去掉?UI_APPEARANCE_SELECTOR
?宏聲明,然后通過 appearance 設置屬性,會怎么樣呢? 測試后發現,結果是一樣的。也就是說?UI_APPEARANCE_SELECTOR
?并沒有干什么事,正如文檔所說,只是 tag 一下。看?UI_APPEARANCE_SELECTOR
?宏定義如下
#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))
由此可見,UI_APPEARANCE_SELECTOR
?真的啥都沒干。。只是為了代碼可讀性,方便開發者使用,還是在需要的地方加上它。
原理小結:
1. 調用 +appearance
獲得 proxy 對象,這個 appearance
對象并不是真正的?UIView
?的實例,而是 UIKit 給你的一個 proxy,它不會馬上設置屬性,而是記錄這個調用。
2.?UIKit 使用 NSInvocation 保存調用信息,包括方法名和參數值。UIAppearance
會創建一個 NSInvocation
,記錄你設置了 selector: 和
?value, 在合適的時機重新調用設置。
3.在控件顯示時統一“回放”設置,當后續創建某個 UIView
?實例時,UIKit 會:
-
檢查當前
UIView
類型是否有 appearance 設置; -
遍歷已記錄的 selector;
-
創建
NSInvocation
,將目標值更新為這個實例的屬性