💡这是字节面试出的题目,当时连UIAppearance怎么使用的都不知道,没回答出来,感觉愧对江东父老,特反省一下,写一个 demo 大家看看对不对
代码地址可查看:https://github.com/irobbin1024/UIAppearance-demo
一. UIAppearance是什么
UIAppearance是一个可以给 App 进行定制主题的工具
传统上,如果我们要全局改变按钮的样式,例如背景色改为灰色,还是比较麻烦的,可以采取的方式,例如写一个 UIButton 的子类,在子类中进行变更。使用UIAppearance就比较简单了。看一下它的 API
1
| [[UIButton appearance] setBackgroundColor:[UIColor grayColor] forState:UIControlStateNormal];
|
如上所示,一行代码就搞定了,我们要做的就是这个效果。
接下来看一下我的 API
1
| [[MyView xy_appearance] setBackgroundColor:[UIColor redColor]];
|
二. 分析要点
要实现UIAppearance的效果,有两点要注意的
第一就是针对目标 API 的调用,例如setBackgroundColor,我们可以换成任意的一个属于UIButton的方法,例如setAlpha。
第二就是我们在配置好效果之后,到底怎么应用到 UI 上,这里我们不讨论系统到底是怎么实现的,根据我的思路就是hook UIView 的 addSubView:方法,在添加之后调用刚才配置的方法即可
三. 具体的实现
1. API 调用
这里我们采用欺骗的方式来实现,xy_appearance方法会返回一个 instanceType,对于编译器来说,他会认为返回的是一个MyView的实例,我们也就可以调用任何 MyView 的方法了,顺便说一下,MyView 是一个 UIView 子类
首先,我们定义一个协议XYUIAppearance,里面是xy_appearance方法,MyView 实现这个协议
在外部调用 MyView 的方法,例如setBackgroundColor的时候,我们将方法保存起来,在添加到父类的时候进行调用。
为了让这一步更简单,我使用了 NSProxy,NSProxy是一个代理类,可以实现对其方法调用的捕获,再转发给被代理的对象。我们可以在捕获之后,保存起来
接下来是具体的代码
XYUIAppearance.h 👇
1 2 3 4 5
| @protocol XYUIAppearance <NSObject>
+ (instancetype)xy_appearance;
@end
|
MyView.h 👇
1 2 3
| @interface MyView : UIView<XYUIAppearance>
@end
|
MyView.m👇
1 2 3 4 5 6 7
| @implementation MyView
+ (instancetype)xy_appearance { return (MyView *)[XYProxy proxyWithTarget:self]; }
@end
|
XYProxy.h👇
1 2 3 4 5 6 7
| @interface XYProxy : NSProxy
@property (nullable, nonatomic, strong) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
|
XYProxy.m👇
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 28 29 30 31 32
| @interface XYProxy ()
@property (nonatomic, copy) NSString * className;
@end
@implementation XYProxy
- (instancetype)initWithTarget:(id)target { _target = target; self.className = NSStringFromClass([(NSObject *)target class]); return self; }
+ (instancetype)proxyWithTarget:(id)target { return [[XYProxy alloc] initWithTarget:target]; }
- (id)forwardingTargetForSelector:(SEL)selector { return nil; }
- (void)forwardInvocation:(NSInvocation *)invocation { void *null = NULL;
[[XYAppearanceManager instance] appendWithClassName:self.className invocation:invocation];
}
@end
|
2. 应用效果
这里就比较简单了,首先 hook UIView 的 addSubView 方法,然后找到对应的 NSInvocation 进行调用。
我这里写了一个类XYAppearanceManager完成NSInvocation保存任务,一个 UIView 的分类来完成 hook 的任务
可以看一下具体的代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| - (void)hook_addSubview:(UIView *)view { NSLog(@"Hooked addSubview: %@", view); [self hook_addSubview:view]; NSInvocation * invocation = [[XYAppearanceManager instance].invocationMap objectForKey:NSStringFromClass(view.class)]; if (invocation) { invocation.target = view; [invocation invoke]; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @implementation XYAppearanceManager
+ (instancetype)instance { static XYAppearanceManager * obj = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ obj = [[self alloc] init]; }); return obj; }
- (NSMutableDictionary<NSString *,NSInvocation *> *)invocationMap { if (_invocationMap == nil) { _invocationMap = [NSMutableDictionary dictionary]; } return _invocationMap; }
- (void)appendWithClassName:(NSString *)calssName invocation:(NSInvocation *)invocation { self.invocationMap[calssName] = invocation; }
@end
|