利用 UIAppearance 協議自定義控件外觀

文章開頭先援引一下Mattt Thompson大神在UIApearance裏的一句話吧:
ios

Users?will?pay?a?premium?for?good-looking?software.

就如同大多數人喜歡看帥哥美女同樣,一款App能不能被接受,長得怎樣很重要。雖然你們都明白「人不可貌相」這個理,但大多數人其實仍是視覺動物。用戶體驗用戶體驗,若是都讓用戶看得不爽了,又何談用戶體驗呢?因此…因此…哎,我也只能在這默默地碼字了。git

在iOS 5之前,咱們想去自定義系統控件的外觀是一件麻煩的事。若是想統一地改變系統控件的外觀,咱們可能會想各類辦法,如去繼承現有的控件類,並在子類中修改,或者甚至於動用method swizzling這樣高大上的方法。不過,蘋果在iOS 5以後爲咱們提供了一種新的方法:UIAppearance,讓這些事簡單了很多。在這裏,咱們就來總結一下吧。github

UIApearance是做用swift

UIApearance其實是一個協議,咱們能夠用它來獲取一個類的外觀代理(appearance proxy)。爲何說是一個類,而不明確說是一個視圖或控件呢?這是由於有些非視圖對象(如UIBarButtonItem)也能夠實現這個協議,來定義其所包含的視圖對象的外觀。咱們能夠給這個類的外觀代理髮送一個修改消息,來自定義一個類的實例的外觀。數組

咱們以系統定義的控件UIButton爲例,根據咱們的使用方式,能夠經過UIAppearance修改整個應用程序中全部UIButton的外觀,也能夠修改某一特定容器類中全部UIButton的外觀(如UIBarButtonItem)。不過須要注意的是,這種修改只會影響到那些執行UIAppearance操做以後添加到咱們的視圖層級架構中的視圖或控件,而不會影響到修改以前就已經添加的對象。所以,若是要修改特定的視圖,先確保該視圖在使用UIAppearance後才經過addSubview添加到視圖層級架構中。架構

UIAppearance的使用app

如上面所說,有兩種方式來自定義對象的外觀:針對某一類型的全部實例;針對包含在某一容器類的實例中的某一類型的實例。講得有點繞,我把文檔的原文貼出來吧。ide

for?all?instances,?and?for?instances?contained?within?an?instance?of?a?container?class.

爲此,UIAppearance聲明瞭兩個方法。若是咱們想自定義一個類全部實例的外觀,則可使用下面這個方法:測試

//?swift
static?func?appearance()?->?Self
//?Objective-C
+?(instancetype)appearance

例如,若是咱們想修改UINavigationBar的全部實例的背影顏色和標題外觀,則能夠以下實現:ui

UINavigationBar.appearance().barTintColor?=?UIColor(red:?104.0/255.0,?green:?224.0/255.0,?blue:?231.0/255.0,?alpha:?1.0)
UINavigationBar.appearance().titleTextAttributes?=?[
????NSFontAttributeName:?UIFont.systemFontOfSize(15.0),
????NSForegroundColorAttributeName:?UIColor.whiteColor()
]

咱們也能夠指定一類容器,在這個容器中,咱們能夠自定義一個類的全部實例的外觀。咱們可使用下面這個方法:

+?(instancetype)appearanceWhenContainedIn:(Class)ContainerClass,?...

如,咱們想修改導航欄中全部的按鈕的外面,則能夠以下處理:

[[UIBarButtonItem?appearanceWhenContainedIn:[UINavigationBar?class],?nil]
???setBackgroundImage:myNavBarButtonBackgroundImage?forState:state?barMetrics:metrics];
[[UIBarButtonItem?appearanceWhenContainedIn:[UINavigationBar?class],?[UIPopoverController?class],?nil]
????setBackgroundImage:myPopoverNavBarButtonBackgroundImage?forState:state?barMetrics:metrics];
[[UIBarButtonItem?appearanceWhenContainedIn:[UIToolbar?class],?nil]
????setBackgroundImage:myToolbarButtonBackgroundImage?forState:state?barMetrics:metrics];
[[UIBarButtonItem?appearanceWhenContainedIn:[UIToolbar?class],?[UIPopoverController?class],?nil]
????setBackgroundImage:myPopoverToolbarButtonBackgroundImage?forState:state?barMetrics:metrics];

注意這個方法的參數是一個可變參數,所以,它能夠同時設置多個容器。

咱們仔細看文檔,發現這個方法沒有swift版本,至少我在iOS 8.x的SDK中沒有找到對應的方法。呵呵,若是想在iOS 8.x如下的系統用swift來調用appearanceWhenContainedIn,那就乖乖地用混編吧。

不過在iOS 9的SDK中(記錄一下,今天是2015.07.18),又把這個方法給加上了,不過這回參數換成了數組,以下所示:

@available(iOS?9.0,?*)
static?func?appearanceWhenContainedInInstancesOfClasses(containerTypes:?[AnyObject.Type])?->?Self

嗯,這裏有個問題,我在Xcode 7.0 beta 3版本上測試swift版本的這個方法時,把將其放在啓動方法裏面,以下所示:

func?application(application:?UIApplication,?didFinishLaunchingWithOptions?launchOptions:?[NSObject:?AnyObject]?)?->?Bool?{
????//?此處會崩潰,提示EXC_BAD_ACCESS
????let?barButtonItemAppearance?=?UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses([UINavigationBar.self])
????let?attributes?=?[
????????NSFontAttributeName:?UIFont.systemFontOfSize(13.0),
????????NSForegroundColorAttributeName:?UIColor.whiteColor()
????]
????barButtonItemAppearance.setTitleTextAttributes(attributes,?forState:?.Normal)
????return?true
}

程序崩潰了,在appearanceWhenContainedInInstancesOfClasses這行提示EXC_BAD_ACCESS。既然是內存問題,那就找找吧。我作了以下幾個測試:

1.拆分UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses,在其前面加了以下幾行代碼:

let?appearance?=?UIBarButtonItem.appearance()
let?arr:?[AnyObject.Type]?=?[UINavigationBar.self,?UIToolbar.self]
print(arr)

能夠看到除了appearanceWhenContainedInInstancesOfClasses自身外,其它幾個元素都是沒問題的。

2.將這段拷貝到默認的ViewController中,運行。一樣崩潰了。

3.在相同環境下(Xcode 7.0 beta 3 + iOS 9.0),用Objective-C對應的方法試了一下,以下:

-?(BOOL)application:(UIApplication?*)application?didFinishLaunchingWithOptions:(NSDictionary?*)launchOptions?{
????[UIBarButtonItem?appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar?class]]];
????return?YES;
}

程序很愉快地跑起來了。

額,我能把這個歸結爲版本不穩定的緣故麼?等到穩定版出來後再研究一下吧。

支持UIAppearance的組件

從iOS 5.0後,有不少iOS的API都已經支持UIAppearance的代理方法了,Mattt Thompson在UIApearance中,給咱們提供瞭如下兩行腳本代碼,能夠獲取全部支持UI_APPEARANCE_SELECTOR的方法(咱們將在下面介紹UI_APPEARANCE_SELECTOR):

$?cd?/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks/UIKit.framework/Headers
$?grep?-H?UI_APPEARANCE_SELECTOR?./*?|?sed?'s/?__OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0)?UI_APPEARANCE_SELECTOR;//'

你們能夠試一下,我這裏列出部分輸出:

./UIActivityIndicatorView.h:@property?(readwrite,?nonatomic,?retain)?UIColor?*color?NS_AVAILABLE_IOS(5_0)?UI_APPEARANCE_SELECTOR;
./UIAppearance.h:/*?To?participate?in?the?appearance?proxy?API,?tag?your?appearance?property?selectors?in?your?header?with?UI_APPEARANCE_SELECTOR.
./UIAppearance.h:#define?UI_APPEARANCE_SELECTOR?__attribute__((annotate("ui_appearance_selector")))
./UIBarButtonItem.h:-?(void)setBackgroundImage:(UIImage?*)backgroundImage?forState:(UIControlState)state?barMetrics:(UIBarMetrics)barMetrics?NS_AVAILABLE_IOS(5_0)?UI_APPEARANCE_SELECTOR;
./UIBarButtonItem.h:-?(UIImage?*)backgroundImageForState:(UIControlState)state?barMetrics:(UIBarMetrics)barMetrics?NS_AVAILABLE_IOS(5_0)?UI_APPEARANCE_SELECTOR;
./UIBarButtonItem.h:-?(void)setBackgroundImage:(UIImage?*)backgroundImage?forState:(UIControlState)state?style:(UIBarButtonItemStyle)style?barMetrics:(UIBarMetrics)barMetrics?NS_AVAILABLE_IOS(6_0)?UI_APPEARANCE_SELECTOR;
./UIBarButtonItem.h:-?(UIImage?*)backgroundImageForState:(UIControlState)state?style:(UIBarButtonItemStyle)style?barMetrics:(UIBarMetrics)barMetrics?NS_AVAILABLE_IOS(6_0)?UI_APPEARANCE_SELECTOR;
./UIBarButtonItem.h:-?(void)setBackgroundVerticalPositionAdjustment:(CGFloat)adjustment?forBarMetrics:(UIBarMetrics)barMetrics?NS_AVAILABLE_IOS(5_0)?UI_APPEARANCE_SELECTOR;?
......

你們還能夠在這裏查看iOS 7.0下的清單。

自定義類實現UIAppearance

咱們能夠自定義一個類,並讓這個類支持UIAppearance。爲此,咱們須要作兩件事:

  1. 讓咱們的類實現UIAppearanceContainer協議

  2. 若是是在Objective-C中,則將相關的方法用UI_APPEARANCE_SELECTOR來標記。而在Swift中,須要在對應的屬性或方法前面加上dynamic。

固然,要讓咱們的類可使用appearance(或appearanceWhenContainedInInstancesOfClasses)來獲取本身的類,則還須要實現UIAppearance協議。

在這裏,咱們來定義一個帶邊框的Label,經過UIAppearance來設置它的默認邊框。實際上,UIView已經實現了UIAppearance和UIAppearanceContainer協議。所以,咱們在其子類中再也不須要顯式地去聲明實現這兩個接口。

咱們的Label的聲明以下:

//?RoundLabel.h
@interface?RoundLabel?:?UILabel
@property?(nonatomic,?assign)?CGFloat?borderWidth?UI_APPEARANCE_SELECTOR;
@property?(nonatomic,?assign)?CGFloat?cornerRadius?UI_APPEARANCE_SELECTOR;
@property?(nonatomic,?assign)?UIColor?*borderColor?UI_APPEARANCE_SELECTOR;
@end

具體的實現以下:

@implementation?RoundLabel
-?(void)drawRect:(CGRect)rect?{
????[super?drawRect:rect];
????self.layer.borderColor?=?_borderColor.CGColor;
????self.layer.cornerRadius?=?_cornerRadius;
????self.layer.borderWidth?=?_borderWidth;
}
-?(void)setBorderWidth:(CGFloat)borderWidth?{
????_borderWidth?=?borderWidth;
}
-?(void)setCornerRadius:(CGFloat)cornerRadius?{
????_cornerRadius?=?cornerRadius;
}
-?(void)setRectColor:(UIColor?*)rectColor?{
????_borderColor?=?rectColor;
}
@end

咱們在drawRect:設置Label的邊框,這樣RoundLabel的全部實例就可使用默認的邊框配置屬性了。

而後,咱們能夠在AppDelegate或者其它某個位置來設置RoundLabel的默認配置,以下所示:

UIColor?*color?=?[UIColor?colorWithRed:104.0/255.0?green:224.0/255.0?blue:231.0/255.0?alpha:1.0f];
[RoundLabel?appearance].cornerRadius?=?5.0f;
[RoundLabel?appearance].borderColor?=?color;
[RoundLabel?appearance].borderWidth?=?1.0f;

固然,咱們在使用RoundLabel時,能夠根據實際須要再修改這幾個屬性的值。

Swift的實現就簡單多了,咱們只須要以下處理:

class?RoundLabel:?UILabel?{
????dynamic?func?setBorderColor(color:?UIColor)?{
????????layer.borderColor?=?color.CGColor
????}
????dynamic?func?setBorderWidth(width:?CGFloat)?{
????????layer.borderWidth?=?width
????}
????dynamic?func?setCornerRadius(radius:?CGFloat)?{
????????layer.cornerRadius?=?radius
????}
}

在UIAppearanceContainer的官方文檔中,有對支持UIAppearance的方法做格式限制,具體要求以下:

//?Swift
func?propertyForAxis1(axis1:?IntegerType,?axis2:?IntegerType,?axisN:?IntegerType)?->?PropertyType
func?setProperty(property:?PropertyType,?forAxis1?axis1:?IntegerType,?axis2:?IntegerType)
//?OBJECTIVE-C
-?(PropertyType)propertyForAxis1:(IntegerType)axis1?axis2:(IntegerType)axis2?…?axisN:(IntegerType)axisN;
-?(void)setProperty:(PropertyType)property?forAxis1:(IntegerType)axis1?axis2:(IntegerType)axis2?…?axisN:(IntegerType)axisN;

其中的屬性類型能夠是iOS的任意類型,包括id, NSInteger, NSUInteger, CGFloat, CGPoint, CGSize, CGRect, UIEdgeInsets或UIOffset。而IntegerType必須是NSInteger或者NSUInteger。若是類型不對,則會拋出異常。

咱們能夠以UIBarButtonItem爲例,它定義瞭如下方法:

setTitlePositionAdjustment:forBarMetrics:
backButtonBackgroundImageForState:barMetrics:
setBackButtonBackgroundImage:forState:barMetrics:

這些方法就是知足上面所提到的格式。

Trait Collection

咱們查看UIAppearance的官方文檔,能夠看到在iOS 8後,這個協議又新增了兩個方法:

//?Swift
static?func?appearanceForTraitCollection(_?trait:?UITraitCollection)?->?Self
//?Objective-C
+?(instancetype)appearanceForTraitCollection:(UITraitCollection?*)trait
+?(instancetype)appearanceForTraitCollection:(UITraitCollection?*)trait
?????????????????????????whenContainedIn:(Class)ContainerClass,?...

這兩個方法涉及到Trait Collection,具體的內容咱們在此不過多的分析。

一些深刻的東西

瞭解了怎麼去使用UIApearance,如今咱們再來了解一下它是怎麼運做的。咱們跟着UIAppearance for Custom Views一文的思路來走。

咱們在如下實現中打一個斷點:

-?(void)setBorderWidth:(CGFloat)borderWidth?{
????_borderWidth?=?borderWidth;
}

而後運行程序。程序啓動時,咱們發現雖然在AppDelegate中調用了

[RoundLabel?appearance].borderWidth?=?1.0f;

但實際上,此時程序沒有到在此斷住。咱們再進到Label所在的視圖控制器,這時程序在斷點處停住了。在這裏,咱們能夠看看方法的調用棧。

在調用棧裏面,咱們能夠看到_UIAppearance這個東東,咱們從iOS-Runtime-Headers能夠找到這個類的定義:

@interface?_UIAppearance?:?NSObject?{
????NSMutableArray?*_appearanceInvocations;
????NSArray?*_containerList;
????_UIAppearanceCustomizableClassInfo?*_customizableClassInfo;
????NSMapTable?*_invocationSources;
????NSMutableDictionary?*_resettableInvocations;
}

其中_UIAppearanceCustomizableClassInfo存儲的是外觀對應的類的信息。咱們能夠看看這個類的聲明:

@interface?_UIAppearanceCustomizableClassInfo?:?NSObject?{
????NSString?*_appearanceNodeKey;
????Class?_customizableViewClass;
????Class?_guideClass;
????unsigned?int?_hash;
????BOOL?_isCustomizableViewClassRoot;
????BOOL?_isGuideClassRoot;
}
@property?(nonatomic,?readonly)?NSString?*_appearanceNodeKey;
@property?(nonatomic,?readonly)?Class?_customizableViewClass;
@property?(nonatomic,?readonly)?Class?_guideClass;
@property?(nonatomic,?readonly)?unsigned?int?_hash;
+?(id)_customizableClassInfoForViewClass:(Class)arg1?withGuideClass:(Class)arg2;
-?(id)_appearanceNodeKey;
-?(Class)_customizableViewClass;
-?(Class)_guideClass;
-?(unsigned?int)_hash;
-?(id)_superClassInfo;
-?(void)dealloc;
-?(id)description;
-?(unsigned?int)hash;
-?(BOOL)isEqual:(id)arg1;
@end

在_UIAppearance中,還有一個_appearanceInvocations變量,咱們能夠在Debug中嘗試用如下命令來打印出它的信息:

po?[[NSClassFromString(@"_UIAppearance")?_appearanceForClass:[RoundLabel?class]?withContainerList:nil]?valueForKey:@"_appearanceInvocations"]

咱們能夠獲得如下的信息:

(return?value:?{v}?void
target:?{@}?0x10b545ae0
selector:?{:}?setCornerRadius:
argument?2:?{d}?0.000000
,return?value:?{v}?void
target:?{@}?0x10b545ae0
selector:?{:}?setBorderColor:
argument?2:?{@}?0x7fd44a5bbb80
,return?value:?{v}?void
target:?{@}?0x10b545ae0
selector:?{:}?setBorderWidth:
argument?2:?{d}?0.000000
)

能夠看到這個數組中存儲的其實是NSInvocation對象,每一個對象就是咱們在程序中設置的RoundLabel外觀的方法信息。

在Peter Steinberger的文章中,有提到當咱們設置了一個自定義的外觀時,_UIAppearanceRecorder會去保存並跟蹤這個設置。咱們能夠看看_UIAppearanceRecorder的聲明:

@interface?_UIAppearanceRecorder?:?NSObject?{
????NSString?*_classNameToRecord;
????NSArray?*_containerClassNames;
????NSMutableArray?*_customizations;
????Class?_superclassToRecord;
????NSArray?*_unarchivedCustomizations;
}

不過有點惋惜的是,我沒有從這裏找到太多的信息。我用runtime檢查了一下這個類中的數據,貌似沒有太多東西。多是姿式不對,我把代碼和結果貼出來,你們幫我看看。

unsigned?int?outCount?=?0;
Class?recorderClass?=?NSClassFromString(@"_UIAppearanceRecorder");
id?recorder?=?[recorderClass?performSelector:NSSelectorFromString(@"_sharedAppearanceRecorderForClass::whenContainedIn:")?withObject:[RoundLabel?class]?withObject:nil];
NSLog(@"_UIAppearanceRecorder?instance?:?%@",?recorder);
Ivar?*variables?=?class_copyIvarList(recorderClass,?&outCount);
for?(int?i?=?0;?i?打印結果:UIAppearanceExample2[7600:381708]?_UIAppearanceRecorder?instance?:?UIAppearanceExample2[7600:381708]?variable's?name:?_classNameToRecord,?value:?RoundLabel
UIAppearanceExample2[7600:381708]?variable's?name:?_superclassToRecord,?value:?(null)
UIAppearanceExample2[7600:381708]?variable's?name:?_containerClassNames,?value:?(null)
UIAppearanceExample2[7600:381708]?variable's?name:?_customizations,?value:?(
)
UIAppearanceExample2[7600:381708]?variable's?name:?_unarchivedCustomizations,?value:?(null)咱們回過頭再來看看_UIAppearance的_appearanceInvocations,咱們是否能夠這樣猜想:UIAppearance是不是經過相似於Swizzling Method這種方式,在運行時去更新視圖的默認顯示呢?求解。遺留問題這一小篇遺留下了兩個問題:在swift中如何正確地使用appearanceWhenContainedInInstancesOfClasses方法?我在stackoverflow中沒有找到答案。iOS內部是如何用UIAppearance設置的信息來在運行時替換默認的設置的?若是有答案,還請告知。小結使用UIAppearance,可讓咱們方便地去修改一些視圖或控件的默認顯示。一樣,若是咱們打算開發一個視圖庫,也可能會用到相關的內容。咱們能夠在庫的內部自定義一些UIAppearance的規則來代替手動去修改視圖外觀。這樣,庫外部就能夠方便的經過UIAppearance來總體修改一個類中視圖的外觀了。我在github中搜索UIAppearance相關的實例時,找到了UISS這個開源庫,它提供了一種便捷的方式來定義程序的樣式。這個庫也是基於UIAppearance的。看其介紹,若是咱們想自定義一個UIButton的外觀,可使用如下方式:{
????"UIButton":{
????????"titleColor:normal":["white",?0.8],
????????"titleColor:highlighted":"white",
????????"backgroundImage:normal":?["button-background-normal",?[0,10,0,10]],
????????"backgroundImage:highlighted":?["button-background-highlighted",?[0,10,0,10]],
????????"titleEdgeInsets":?[1,0,0,0],
????????"UILabel":{
????????????"font":["Copperplate-Bold",?18]
????????}
????}
}看着像JSON吧?具體的我也尚未看,回頭抽空再研究研究這個庫。補充:文章中的示例代碼已放到github中,能夠在這裏查看(不保證在iOS 9.0如下能正常進行,嘿嘿)
相關文章
相關標籤/搜索