文章开头先援引一下Mattt Thompson
大神在UIApearance里的一句话吧:
Users will pay a premium for good-looking software.
就如同大多数人喜欢看帅哥美女一样,一款App
能不能被接受,长得怎样很重要。虽然大家都明白“人不可貌相”这个理,但大多数人其实还是视觉动物。用户体验用户体验,如果都让用户看得不爽了,又何谈用户体验呢?所以…所以…哎,我也只能在这默默地码字了。
在iOS 5
以前,我们想去自定义系统控件的外观是一件麻烦的事。如果想统一地改变系统控件的外观,我们可能会想各种办法,如去继承现有的控件类,并在子类中修改,或者甚至于动用method swizzling
这样高大上的方法。不过,苹果在iOS 5
之后为我们提供了一种新的方法:UIAppearance
,让这些事简单了不少。在这里,我们就来总结一下吧。
UIApearance是作用
UIApearance
实际上是一个协议,我们可以用它来获取一个类的外观代理(appearance proxy
)。为什么说是一个类,而不明确说是一个视图或控件呢?这是因为有些非视图对象(如UIBarButtonItem
)也可以实现这个协议,来定义其所包含的视图对象的外观。我们可以给这个类的外观代理发送一个修改消息,来自定义一个类的实例的外观。
我们以系统定义的控件UIButton
为例,根据我们的使用方式,可以通过UIAppearance
修改整个应用程序中所有UIButton
的外观,也可以修改某一特定容器类中所有UIButton
的外观(如UIBarButtonItem
)。不过需要注意的是,这种修改只会影响到那些执行UIAppearance
操作之后添加到我们的视图层级架构中的视图或控件,而不会影响到修改之前就已经添加的对象。因此,如果要修改特定的视图,先确保该视图在使用UIAppearance
后才通过addSubview
添加到视图层级架构中。
UIAppearance的使用
如上面所说,有两种方式来自定义对象的外观:针对某一类型的所有实例;针对包含在某一容器类的实例中的某一类型的实例。讲得有点绕,我把文档的原文贴出来吧。
for all instances, and for instances contained within an instance of a container class.
为此,UIAppearance
声明了两个方法。如果我们想自定义一个类所有实例的外观,则可以使用下面这个方法:
|
|
例如,如果我们想修改UINavigationBar
的所有实例的背影颜色和标题外观,则可以如下实现:
|
|
我们也可以指定一类容器,在这个容器中,我们可以自定义一个类的所有实例的外观。我们可以使用下面这个方法:
|
|
如,我们想修改导航栏中所有的按钮的外面,则可以如下处理:
|
|
注意这个方法的参数是一个可变参数,因此,它可以同时设置多个容器。
我们仔细看文档,发现这个方法没有swift
版本,至少我在iOS 8.x
的SDK
中没有找到对应的方法。呵呵,如果想在iOS 8.x
以下的系统用swift
来调用appearanceWhenContainedIn
,那就乖乖地用混编吧。
不过在iOS 9
的SDK
中(记录一下,今天是2015.07.18
),又把这个方法给加上了,不过这回参数换成了数组,如下所示:
|
|
嗯,这里有个问题,我在Xcode 7.0 beta 3
版本上测试swift
版本的这个方法时,把将其放在启动方法里面,如下所示:
|
|
程序崩溃了,在appearanceWhenContainedInInstancesOfClasses
这行提示EXC_BAD_ACCESS
。既然是内存问题,那就找找吧。我做了如下几个测试:
1.拆分UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses
,在其前面加了如下几行代码:
|
|
可以看到除了appearanceWhenContainedInInstancesOfClasses
自身外,其它几个元素都是没问题的。
2.将这段拷贝到默认的ViewController
中,运行。同样崩溃了。
3.在相同环境下(Xcode 7.0 beta 3 + iOS 9.0
),用Objective-C
对应的方法试了一下,如下:
|
|
程序很愉快地跑起来了。
额,我能把这个归结为版本不稳定的缘故么?等到稳定版出来后再研究一下吧。
支持UIAppearance的组件
从iOS 5.0
后,有很多iOS
的API
都已经支持UIAppearance
的代理方法了,Mattt Thompson
在UIApearance中,给我们提供了以下两行脚本代码,可以获取所有支持UI_APPEARANCE_SELECTOR
的方法(我们将在下面介绍UI_APPEARANCE_SELECTOR
):
|
|
大家可以试一下,我这里列出部分输出:
|
|
大家还可以在这里查看iOS 7.0
下的清单。
自定义类实现UIAppearance
我们可以自定义一个类,并让这个类支持UIAppearance
。为此,我们需要做两件事:
- 让我们的类实现
UIAppearanceContainer
协议 - 如果是在
Objective-C
中,则将相关的方法用UI_APPEARANCE_SELECTOR
来标记。而在Swift
中,需要在对应的属性或方法前面加上dynamic
。
当然,要让我们的类可以使用appearance
(或appearanceWhenContainedInInstancesOfClasses
)来获取自己的类,则还需要实现UIAppearance
协议。
在这里,我们来定义一个带边框的Label
,通过UIAppearance
来设置它的默认边框。实际上,UIView
已经实现了UIAppearance
和UIAppearanceContainer
协议。因此,我们在其子类中不再需要显式地去声明实现这两个接口。
我们的Label的声明如下:
|
|
具体的实现如下:
|
|
我们在drawRect:
设置Label
的边框,这样RoundLabel
的所有实例就可以使用默认的边框配置属性了。
然后,我们可以在AppDelegate
或者其它某个位置来设置RoundLabel
的默认配置,如下所示:
|
|
当然,我们在使用RoundLabel
时,可以根据实际需要再修改这几个属性的值。
Swift
的实现就简单多了,我们只需要如下处理:
|
|
在UIAppearanceContainer
的官方文档中,有对支持UIAppearance
的方法作格式限制,具体要求如下:
|
|
其中的属性类型可以是iOS
的任意类型,包括id
, NSInteger
, NSUInteger
, CGFloat
, CGPoint
, CGSize
, CGRect
, UIEdgeInsets
或UIOffset
。而IntegerType
必须是NSInteger
或者NSUInteger
。如果类型不对,则会抛出异常。
我们可以以UIBarButtonItem
为例,它定义了以下方法:
|
|
这些方法就是满足上面所提到的格式。
Trait Collection
我们查看UIAppearance的官方文档,可以看到在iOS 8
后,这个协议又新增了两个方法:
|
|
这两个方法涉及到Trait Collection
,具体的内容我们在此不过多的分析。
一些深入的东西
了解了怎么去使用UIApearance
,现在我们再来了解一下它是怎么运作的。我们跟着UIAppearance for Custom Views一文的思路来走。
我们在以下实现中打一个断点:
|
|
然后运行程序。程序启动时,我们发现虽然在AppDelegate
中调用了
|
|
但实际上,此时程序没有到在此断住。我们再进到Label
所在的视图控制器,这时程序在断点处停住了。在这里,我们可以看看方法的调用栈。
在调用栈里面,我们可以看到_UIAppearance
这个东东,我们从iOS-Runtime-Headers可以找到这个类的定义:
|
|
其中_UIAppearanceCustomizableClassInfo存储的是外观对应的类的信息。我们可以看看这个类的声明:
|
|
在_UIAppearance
中,还有一个_appearanceInvocations
变量,我们可以在Debug
中尝试用以下命令来打印出它的信息:
|
|
我们可以得到以下的信息:
|
|
可以看到这个数组中存储的实际上是NSInvocation
对象,每个对象就是我们在程序中设置的RoundLabel
外观的方法信息。
在Peter Steinberger
的文章中,有提到当我们设置了一个自定义的外观时,_UIAppearanceRecorder会去保存并跟踪这个设置。我们可以看看_UIAppearanceRecorder
的声明:
|
|
不过有点可惜的是,我没有从这里找到太多的信息。我用runtime
检查了一下这个类中的数据,貌似没有太多东西。可能是姿势不对,我把代码和结果贴出来,大家帮我看看。
|
|
打印结果:
|
|
我们回过头再来看看_UIAppearance的_appearanceInvocations
,我们是否可以这样猜测:UIAppearance
是否是通过类似于Swizzling Method
这种方式,在运行时去更新视图的默认显示呢?求解。
遗留问题
这一小篇遗留下了两个问题:
- 在
swift
中如何正确地使用appearanceWhenContainedInInstancesOfClasses
方法?我在stackoverflow
中没有找到答案。 iOS
内部是如何用UIAppearance
设置的信息来在运行时替换默认的设置的?
如果有答案,还请告知。
小结
使用UIAppearance
,可以让我们方便地去修改一些视图或控件的默认显示。同样,如果我们打算开发一个视图库,也可能会用到相关的内容。我们可以在库的内部自定义一些UIAppearance
的规则来代替手动去修改视图外观。这样,库外部就可以方便的通过UIAppearance
来整体修改一个类中视图的外观了。
我在github
中搜索UIAppearance
相关的实例时,找到了UISS这个开源库,它提供了一种便捷的方式来定义程序的样式。这个库也是基于UIAppearance
的。看其介绍,如果我们想自定义一个UIButton
的外观,可以使用以下方式:
|
|
看着像JSON
吧?
具体的我也还没有看,回头抽空再研究研究这个库。
补充:文章中的示例代码已放到github
中,可以在这里查看(不保证在iOS 9.0
以下能正常进行,嘿嘿)