欢迎登录个人我的网站: www.sketchk.xyzswift
在 UIAppearance 出现以前,开发者若是想统一修改 app 内某一个控件的 UI 样式时,只能经过去修改每一个控件的实例属性,对于只有几个实例的 UI 控件来讲,这样的修改还能够接受,但若是整个 app 中有几十个,甚至上百个实例的时候,这样的修改就显得至关笨拙了,固然你也能够考虑使用一些黑魔法来实现,不过这或多或少都给开发者带来了很多麻烦。bash
除了上面提到的场景外,还有一种场景就是在 app 内提供多种多样的主题来知足用户的需求,例如手淘在 app 内提供的主题切换功能。app
上面这两个实际开发中的应用场景都映射出这样一个问题:如何在整个 app 中高效,统一,即时
的定制 UI 控件样式。框架
在 iOS 5.0 以后,Apple 为开发者提供了名为 UIAppearance 和 UIAppearanceContainer 的类,它们就是苹果为开发者提供的一种官方解决方案。post
为了在现有的 UIKit 框架里面去作这个事,苹果的思路是: 让 UIAppearance 成为一个能够返回代理的协议,经过它能够把任何配置转发给特定类的实例。网站
但为何不直接在 UIView 里面搞个属性或者方法来作这件事儿呢?ui
由于诸如 UIBarButtonItem 这样的控件并非 UIView 的子类,它只是持有一个视图实例而已。经过这种设计,UIAppearance 能够处理全部类型的 UI 控件,不管它是 UIView 的子类,仍是包含了视图实例的非 UIView 控件。atom
咱们刚才说过 UIApearance 其实是一个协议,该协议需实现如下几个方法:spa
+ (instancetype)appearance;
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait NS_AVAILABLE_IOS(8_0);
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 已经废弃的两个方法
+ (instancetype)appearanceWhenContainedIn:(nullable Class <UIAppearanceContainer>)ContainerClass, ... NS_REQUIRES_NIL_TERMINATION NS_DEPRECATED_IOS(5_0, 9_0, "Use +appearanceWhenContainedInInstancesOfClasses: instead") __TVOS_PROHIBITED;
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedIn:(nullable Class <UIAppearanceContainer>)ContainerClass, ... NS_REQUIRES_NIL_TERMINATION NS_DEPRECATED_IOS(8_0, 9_0, "Use +appearanceForTraitCollection:whenContainedInInstancesOfClasses: instead") __TVOS_PROHIBITED;
复制代码
在这个协议中,须要简单解释一下第 3 和第 4 个方法,这个两个方法是用于解决 Size Classes 的问题而诞生的,经过这两个 API,咱们能够控制在不一样屏幕尺寸下的样式。设计
另一个与之对应的协议是 UIAppearanceContainer,该协议并无任何约定方法。由于它只是做为一个容器。
常见的,如 UIView 实现了 UIAppearance 这两种协议,既能够获取外观代理,也能够做为外观容器。而 UIViewController 则是仅实现了 UIAppearanceContainer 协议,很简单,它自己是控制器而不是 view,做为容器,为 UIView 等服务。
在 UIKit 中被 UI_APPEARANCE_SELECTOR
宏标注的属性能够被 UIAppearance 调用。 例如 UIBarButtonItem 里的 tintColor 属性。
@property(nullable, nonatomic,strong) UIColor *tintColor NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
复制代码
在 swift 中没有宏的概念,因此属性没法被
UI_APPEARANCE_SELECTOR
标注,若是想让某个属性支持 UIAppearance 能够为该属性使用 dynamic 关键字
UIAppearance 不只能够修改某一类型控件的所有实例,也能够修改部分实例,开发者只须要使用正确的 API 便可。仍是以 UIBarButtonItem 为例。
当咱们想改变全部 UIBarButtonItem 实例的 tintColor 时,代码以下:
[[UIBarButtonItem appearance] setTintColor:myColor];
复制代码
当咱们想在某些指定容器类里改变 UIBarButtonItem 的 tintColor 时,咱们能够这么作:
[[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]] setTintColor:myColor];
复制代码
UIAppearance 里与 UITraitCollection 相关的两个方法与上面两个方法的使用规则类似,在这里就不作赘述了
使用 UIAppearance 只有在视图添加到 window 时才会生效,对于已经在 window 中的视图并不会生效。因此,对于已经在 window 里的视图,能够采用从视图里移除并再次添加回去的方法使得 UIAppearance 的设置生效。
因此若是你发现本身的设置没有生效的话,不妨参考下这个规则。
在实际使用过程当中,咱们绝大多数的视图类都继承自 UIView,UIView 的容器也基本上是 UIView 或 UIController,因此基本不须要开发者去实现这两个协议。
对于须要支持使用 UIAppearance 来设置的属性,在属性后增长 UI_APPEARANCE_SELECTOR
宏声明便可。
须要说明的是,在遵循 UIAppearanceContainer 协议的类中,声明与 UIAppearance 相关属性的方法时,要遵循两个代码规范:
UI_APPEARANCE_SELECTOR 标注
//You may have no axes or as many as you like for any property.
- (void)setProperty:(PropertyType)property forAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;
- (PropertyType)propertyForAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;
//Example
- (void)setBackgroundImage:(nullable UIImage *)backgroundImage forState:(UIControlState)state barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
- (nullable UIImage *)backgroundImageForState:(UIControlState)state barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
复制代码
Apple 官方对于 UIAppearance 的介绍并很少,网上的文章也大多都停留在使用层面上,今天咱们来深刻讨论一下 UIAppearance 内部的秘密。
UI_APPEARANCE_SELECTOR
查看 Apple 的官方文档后,你就会发现它其实什么也没干…..
#define UI_APPEARANCE_SELECTOR
复制代码
既然它什么都没干,那么对于没有被 UI_APPEARANCE_SELECTOR
标记的属性,咱们是否也能用 UIAppearance 进行统一设置呢?
答案是能够的。
此时你心里充满了疑惑,
咱们不妨将这个问题分红两个部分:
事实证实, UIAppearance 确实能够对一些没有被 UI_APPEARANCE_SELECTOR
标记的属性进行设置,甚至某些方法也是支持的 UIAppearance,例如 UISegmentedControl 的 setWidth:forSegmentAtIndex
方法。
但这种作法应该被避免,缘由很简单:这些没有被标记的属性不必定会在将来的 iOS 版本中适用。
在 Understanding UIAppearance 这篇文章里也给出了一样的观点。
那么 UIAppearance 究竟是如何实现的呢?国外的大神们已经对此有了研究结论,我直接给出连接,感兴趣的朋友能够阅读一下 UIAppearance and Custom Views。
在自定义 View 中使用 UIAppearance 仍是有一些须要注意的事项,具体的内容能够参考 UIAppearance for Custom Views
个人博客原文:sketchk.xyz/2018/01/25/…