文章開頭先援引一下Mattt Thompson
大神在UIApearance裏的一句話吧:html
1
|
Users will pay a premium for good-looking software. |
就如同大多數人喜歡看帥哥美女同樣,一款App
能不能被接受,長得怎樣很重要。雖然你們都明白「人不可貌相」這個理,但大多數人其實仍是視覺動物。用戶體驗用戶體驗,若是都讓用戶看得不爽了,又何談用戶體驗呢?因此…因此…哎,我也只能在這默默地碼字了。ios
在iOS 5
之前,咱們想去自定義系統控件的外觀是一件麻煩的事。若是想統一地改變系統控件的外觀,咱們可能會想各類辦法,如去繼承現有的控件類,並在子類中修改,或者甚至於動用method swizzling
這樣高大上的方法。不過,蘋果在iOS 5
以後爲咱們提供了一種新的方法:UIAppearance
,讓這些事簡單了很多。在這裏,咱們就來總結一下吧。git
UIApearance
其實是一個協議,咱們能夠用它來獲取一個類的外觀代理(appearance proxy
)。爲何說是一個類,而不明確說是一個視圖或控件呢?這是由於有些非視圖對象(如UIBarButtonItem
)也能夠實現這個協議,來定義其所包含的視圖對象的外觀。咱們能夠給這個類的外觀代理髮送一個修改消息,來自定義一個類的實例的外觀。github
咱們以系統定義的控件UIButton
爲例,根據咱們的使用方式,能夠經過UIAppearance
修改整個應用程序中全部UIButton
的外觀,也能夠修改某一特定容器類中全部UIButton
的外觀(如UIBarButtonItem
)。不過須要注意的是,這種修改只會影響到那些執行UIAppearance
操做以後添加到咱們的視圖層級架構中的視圖或控件,而不會影響到修改以前就已經添加的對象。所以,若是要修改特定的視圖,先確保該視圖在使用UIAppearance
後才經過addSubview
添加到視圖層級架構中。objective-c
如上面所說,有兩種方式來自定義對象的外觀:針對某一類型的全部實例;針對包含在某一容器類的實例中的某一類型的實例。講得有點繞,我把文檔的原文貼出來吧。swift
1
|
for all instances, and for instances contained within an instance of a container class. |
爲此,UIAppearance
聲明瞭兩個方法。若是咱們想自定義一個類全部實例的外觀,則可使用下面這個方法:數組
1
2 3 4 5 |
// swift static func appearance() -> Self // Objective-C + (instancetype)appearance |
例如,若是咱們想修改UINavigationBar
的全部實例的背影顏色和標題外觀,則能夠以下實現:架構
1
2 3 4 5 6 |
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() ] |
咱們也能夠指定一類容器,在這個容器中,咱們能夠自定義一個類的全部實例的外觀。咱們可使用下面這個方法:app
1
|
+ (instancetype)appearanceWhenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, ... |
如,咱們想修改導航欄中全部的按鈕的外面,則能夠以下處理:ide
1
2 3 4 5 6 7 8 9 10 11 |
[[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
),又把這個方法給加上了,不過這回參數換成了數組,以下所示:
1
2 |
@available(iOS 9.0, *) static func appearanceWhenContainedInInstancesOfClasses(containerTypes: [AnyObject.Type]) -> Self |
嗯,這裏有個問題,我在Xcode 7.0 beta 3
版本上測試swift
版本的這個方法時,把將其放在啓動方法裏面,以下所示:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
,在其前面加了以下幾行代碼:
1
2 3 4 5 |
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
對應的方法試了一下,以下:
1
2 3 4 5 6 |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]]; return YES; } |
程序很愉快地跑起來了。
額,我能把這個歸結爲版本不穩定的緣故麼?等到穩定版出來後再研究一下吧。
從iOS 5.0
後,有不少iOS
的API
都已經支持UIAppearance
的代理方法了,Mattt Thompson
在UIApearance中,給咱們提供瞭如下兩行腳本代碼,能夠獲取全部支持UI_APPEARANCE_SELECTOR
的方法(咱們將在下面介紹UI_APPEARANCE_SELECTOR
):
1
2 3 |
$ 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;//' |
你們能夠試一下,我這裏列出部分輸出:
1
2 3 4 5 6 7 8 9 |
./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
。爲此,咱們須要作兩件事:
UIAppearanceContainer
協議Objective-C
中,則將相關的方法用UI_APPEARANCE_SELECTOR
來標記。而在Swift
中,須要在對應的屬性或方法前面加上dynamic
。固然,要讓咱們的類可使用appearance
(或appearanceWhenContainedInInstancesOfClasses
)來獲取本身的類,則還須要實現UIAppearance
協議。
在這裏,咱們來定義一個帶邊框的Label
,經過UIAppearance
來設置它的默認邊框。實際上,UIView
已經實現了UIAppearance
和UIAppearanceContainer
協議。所以,咱們在其子類中再也不須要顯式地去聲明實現這兩個接口。
咱們的Label的聲明以下:
1
2 3 4 5 6 7 8 9 |
// 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 |
具體的實現以下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@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
的默認配置,以下所示:
1
2 3 4 5 |
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
的實現就簡單多了,咱們只須要以下處理:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
的方法做格式限制,具體要求以下:
1
2 3 4 5 6 7 |
// 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
爲例,它定義瞭如下方法:
1
2 3 4 5 |
setTitlePositionAdjustment:forBarMetrics: backButtonBackgroundImageForState:barMetrics: setBackButtonBackgroundImage:forState:barMetrics: |
這些方法就是知足上面所提到的格式。
咱們查看UIAppearance的官方文檔,能夠看到在iOS 8
後,這個協議又新增了兩個方法:
1
2 3 4 5 6 7 8 |
// Swift static func appearanceForTraitCollection(_ trait: UITraitCollection) -> Self // Objective-C + (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait + (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, ... |
這兩個方法涉及到Trait Collection
,具體的內容咱們在此不過多的分析。
瞭解了怎麼去使用UIApearance
,如今咱們再來了解一下它是怎麼運做的。咱們跟着UIAppearance for Custom Views一文的思路來走。
咱們在如下實現中打一個斷點:
1
2 3 4 |
- (void)setBorderWidth:(CGFloat)borderWidth { _borderWidth = borderWidth; } |
而後運行程序。程序啓動時,咱們發現雖然在AppDelegate
中調用了
1
|
[RoundLabel appearance].borderWidth = 1.0f; |
但實際上,此時程序沒有到在此斷住。咱們再進到Label
所在的視圖控制器,這時程序在斷點處停住了。在這裏,咱們能夠看看方法的調用棧。
在調用棧裏面,咱們能夠看到_UIAppearance
這個東東,咱們從iOS-Runtime-Headers能夠找到這個類的定義:
1
2 3 4 5 6 7 |
@interface _UIAppearance : NSObject { NSMutableArray *_appearanceInvocations; NSArray *_containerList; _UIAppearanceCustomizableClassInfo *_customizableClassInfo; NSMapTable *_invocationSources; NSMutableDictionary *_resettableInvocations; } |
其中_UIAppearanceCustomizableClassInfo存儲的是外觀對應的類的信息。咱們能夠看看這個類的聲明:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@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
中嘗試用如下命令來打印出它的信息:
1
|
po [[NSClassFromString(@"_UIAppearance") _appearanceForClass:[RoundLabel class] withContainerList:nil] valueForKey:@"_appearanceInvocations"] |
咱們能夠獲得如下的信息:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<__NSArrayM 0x7fd44a5c1f80>( <NSInvocation: 0x7fd44a5c1d20> return value: {v} void target: {@} 0x10b545ae0 selector: {:} setCornerRadius: argument 2: {d} 0.000000 , <NSInvocation: 0x7fd44a5bf300> return value: {v} void target: {@} 0x10b545ae0 selector: {:} setBorderColor: argument 2: {@} 0x7fd44a5bbb80 , <NSInvocation: 0x7fd44a50b8c0> return value: {v} void target: {@} 0x10b545ae0 selector: {:} setBorderWidth: argument 2: {d} 0.000000 ) |
能夠看到這個數組中存儲的其實是NSInvocation
對象,每一個對象就是咱們在程序中設置的RoundLabel
外觀的方法信息。
在Peter Steinberger
的文章中,有提到當咱們設置了一個自定義的外觀時,_UIAppearanceRecorder會去保存並跟蹤這個設置。咱們能夠看看_UIAppearanceRecorder
的聲明:
1
2 3 4 5 6 7 |
@interface _UIAppearanceRecorder : NSObject { NSString *_classNameToRecord; NSArray *_containerClassNames; NSMutableArray *_customizations; Class _superclassToRecord; NSArray *_unarchivedCustomizations; } |
不過有點惋惜的是,我沒有從這裏找到太多的信息。我用runtime
檢查了一下這個類中的數據,貌似沒有太多東西。多是姿式不對,我把代碼和結果貼出來,你們幫我看看。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 < outCount; i++) { Ivar variable = variables[i]; id value = object_getIvar(recorder, variable); NSLog(@"variable's name: %s, value: %@", ivar_getName(variable), value); } free(variables); |
打印結果:
1
2 3 4 5 6 7 |
UIAppearanceExample2[7600:381708] _UIAppearanceRecorder instance : <_UIAppearanceRecorder: 0x7fa29a718960> 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
的外觀,可使用如下方式:
1
2 3 4 5 6 7 8 9 10 11 12 |
{ "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
如下能正常進行,嘿嘿)