文章开头先援引一下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
如下能正常进行,嘿嘿)