快捷搜索:  as  test

iOS关于换肤和夜间模式的一些思考

先容

良久没写文章了,恰恰近来在钻研换肤,以是快要来的心得和体会与大年夜家分享一下。

iOS换肤的要领对照单一,查找了很多资料,发明主流的要领有如下两种:

要领一:经由过程给 Category 添加属性的要领实现换肤,有一个 Manager 用以治理颜色和图片,当主题改变时,经由过程发出看护奉告 UIKit 中的相关类,该改变视图颜色了,这时视图就会根据 Manager 中供给的不合主题的颜色来改变自己的颜色。

这种规划的优点在于:整体思路对照简单清楚明了,实现起来也不艰苦。

毛病在于:

对付每种控件,都已经将颜色固定逝世,没有法子设置比犹如一个父视图的两个子视图不合的颜色显示。

当我们的项目已经完成了,而且项目体积也对照大年夜时,这种要领的毛病就裸露的异常显着了:变动界面十分麻烦,由于我们的界面对照多时,必要给每个界面的每个控件都添加在Category 中增添的属性, 这种要领事情量伟大年夜。

要领二:应用系统供给的 UIAppearance 来变动主题,这种要领的优点在于,系统供给了异常简单方便的 API 供我们应用,最常用的便是 + (instancetype)appearance; 措施和 + (instancetype)appearanceWhenContainedIn:(Class)ContainerClass, …; 这两个措施。详细用法如下: [[UINavigationBar appearance] setBarTintColor:myNavBarBackgroundColor];可以设置全局的 UINavigationBar 的 barTintColor。而 [[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], nil] setBackgroundImage:myNavBarButtonBackgroundImage forState:state barMetrics:metrics]; 表示在指定视图中设置 color,在此示例中是设置 UINavigationBar 上的UIBarButtonItem 的背景图片。

这种要领的道理在于:应用 UI_APPEARANCE_SELECTOR 标记的要领会将当前对 UI 设置的外不雅保存起来,等到视图在添加到 window 之前会调用这个之前保存的外不雅,更新视图外不雅。以是并不是 UIKit 中所有的类的所有属性都可以应用这个措施来设置 UI,只有当属性上有标志UI_APPEARANCE_SELECTOR才可以用这个措施来设置。

这种要领的优点是可以十分便捷的设置一些全局的系统控件的外不雅。

然则毛病也十分显着:

当我们想要区分同一个父视图上方的子视图时,这种规划就会十分的未方便,与第一种措施一样,很难达到个性化定制的目的。

并且当我们想要设置 UILabel 等控件在不合视图上的字体颜色等时,常常会掉效,经由过程查看系统 API,可以发明 UILabel 的 setTextColor: 等措施并没有 UI_APPEARANCE_SELECTOR 标志位,以是这也是这个换肤要领并不是万能的缘故原由。Stack Overflow有一篇关于 UILabel 设置颜色掉效的缘故原由,他们说这是苹果系统的一个 bug。而办理这个问题的措施也对照简单,只要我们重写 setTextColor: 措施,给它加上一个UI_APPEARANCE_SELECTOR 标志位,那么就可以给它定制颜色。然则这种要领的毛病也十分显着,对代码的篡改并没有任何削减。反而当有很多控件都不能精确显示颜色时,还必要增添很大年夜的事情量。

总结:我觉得这种设置 UIAppearance 的要领照样对照适用于当全局的颜色已经固准时,设置主题,比如 UINavigationBar 和 UITabbar 这种控件,就对照得当应用这种要领来进行操作。当我们的换肤对照简单,不涉及类似夜间模式这种必要险些把所有的控件颜色都改变时,我感觉也可以应用这种措施来进行换肤操作。

别的:这个措施必要留意的一个点是,当我们改变主题颜色时,必要先将控件从 window 上移除,再从新添加才会触发这种要领。

- (void)p_updateSystemWindow {

NSArray *windowArray = [UIApplication sharedApplication].windows;

for (UIWindow *window in windowArray) {

for (UIView *subView in window.subviews) {

[subView removeFromSuperview];

[window addSubview:subView];

}

}

}

自己的设法主见

首先我们应该明确需求背景:

最基础的便是:能够实现换肤

项目已经完成,并且项目对照繁杂不得当一个节制器一个节制器的去改动

能够实现控件的个性化颜色定制,而并不是所有的一类控件都是同种颜色

孕育发生的问题:

是否可以结合上述两种要领,孕育发生自己的要领来进行简便的换肤?

若何做到只管即便少篡改代码,就能实现换肤的效果?

若何实现控件的个性化颜色定制?

若何办理:

既然全部项目都已经完成,那么假如我想只管即便少篡改代码,是否可以应用 methodSwizzling 的要领来 hook 系统的 setXXXColor: 措施实现不必要或只管即便少对原项目代码进行篡改。

既然必要对控件进行个性化定制,是否可以应用 tag 的要领,对必要个性化的控件添加 tag 从而根据不合的 tag 来应用不合的颜色,而不必要个性化的颜色维持蓝本状态不进行改动。

我的实践

首先必要供给一个 Manager 来进行主题的节制,在我的项目中,它叫做 LYThemeManager , 这个 Manager 的感化是节制切换不合的主题,当主题进行改变时,可以发出看护,见告 UI 控件该改变自己的颜色了。并且它所供给的 (UIColor *)colorWithReceiver:(id)receiver selString:(NSString *)selector; 和 (UIColor *)colorWithReceiver:(id)receiver withTag:(NSInteger)tag selString:(NSString *)selector; 分手是实现全局控件 UI 的设置以及 个性化控件 UI 的设置。

在 LYThemeManager 内部有两个字典,分手是读取不合的 plist , colorInfoDic 用于读取全局 UI 的颜色设置,而 specialColorInfoDic 用于读取个性化控件的颜色设置,详细的 plist 中的内容如下:

image

image

在 specialPlist 中前面的数字表示 tag 值,后面表示设置的属性意义。

以 UIView 的 category 为例,首先在这个类中,应用了 methodSwizzle 来实现 hook 系统措施,在这里我 hook 了系统的 setBackgroundColor: 措施和 setTintColor: 措施。

+ (void)load {

[self swizzleViewColor];

}

#pragma mark - MethodSwizzling

+ (void)swizzleViewColor {

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

[LYMethodSwizzleUtils swizzleInstanceMethodWithClass:[self class] OriginMethod:@selector(setBackgroundColor:) swappedMethod:@selector(ly_setBackgroundColor:)];

[LYMethodSwizzleUtils swizzleInstanceMethodWithClass:[self class] OriginMethod:@selector(setTintColor:) swappedMethod:@selector(ly_setTintColor:)];

});

}

以 setBackgroundColor: 措施为例:

- (void)ly_setBackgroundColor:(UIColor *)color {

// 使用 selector 来选措施,留意子类和父类不要应用同名措施,否则会导致符号纷乱孕育发生轮回引用。

UIColor *bgColor = [[LYThemeManager shareManager] colorWithReceiver:self withTag:self.tag selString:[NSString stringWithFormat:@"%ld:viewBackgroundColor", self.tag]];

if (bgColor) {

[self.pickers setObject:bgColor forKey:@"setBackgroundColor:"];

[self ly_setBackgroundColor:bgColor];

} else {

[self ly_setBackgroundColor:color];

}

}

在这里为什么我要应用个性化颜色设置的措施:(UIColor *)colorWithReceiver:(id)receiver withTag:(NSInteger)tag selString:(NSString *)selector; ,这是由于险些所有 UIKit 中的控件都承袭自 UIView,当我们直接将所有的 setBackgroundColor: 措施都设置为同一颜色时,达到的效果是劫难性的所有控件都是同一颜色。无法进行区分。以是这里应用个性化的,只对 controller 中的 view 改变颜色。

添加了一个字典属性 pickers, 这个属性用以将我们 hook 的措施添加进来,它的 key 是措施名, value是它应该被设置的 color,当收到改变颜色的看护时,必要遍历这个属性中所有的数据,来实现颜色更新。

@interface UIView ()

@property (nonatomic, strong) NSMutableDictionary*pickers;

@end

#pragma mark - Add Property

- (NSMutableDictionary *)pickers {

NSMutableDictionary*pickers = objc_getAssociatedObject(self, @selector(pickers));

if (!pickers) {

pickers = @{}.mutableCopy;

objc_setAssociatedObject(self, @selector(pickers), pickers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

[[NSNotificationCenter defaultCenter] removeObserver:self];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateTheme) name:LYThemeChangeNotification object:nil];

}

return pickers;

}

着末便是对看护的相应:

#pragma mark - Response Notification

- (void)updateTheme {

[self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, UIColor * _Nonnull obj, BOOL * _Nonnull stop) {

SEL selector = NSSelectorFromString(key);

[UIView animateWithDuration:0.3 animations:^{

#pragma clang diagnostic push

#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

[self performSelector:selector withObject:obj];

#pragma clang diagnostic pop

}];

}];

}

因为险些所有的 UIKit 中的控件都承袭自 UIView,并且相应要领都同于 UIView ,以是在其他的 category 中省去了对属性 picker 的 Add Property 步骤以及对看护的相应。

在 UILabel 中的 setTextColor: 措施也应用了个性化的设置,对付不必要特殊设置的 UILabel 的 textColor 则蓝本默认是什么颜色,便是什么颜色。

所有的 tag 值,我都以宏定义的要领存储在 ThemeConfig.pch 中了,当必要个性定义的控件对照多时,经由过程 tag 治理也是一个毛病。

整体上思路便是如斯,这个规划只是一个初步规划,还有很多很多不够之处。

毛病在于:

比如说经由过程 tag 来治理颜色,实际上也会改动原项目的代码,由于我们必要设置不合控件的 tag 值。

hook 系统的措施或许会带来意想不到的bug。不过在我 hook 的这种要领下,当在颜色匹配表中找不到对应字段时,会直接应用原本的颜色进行设置,感到也没有什么分外大年夜的问题。

这种要领的上风在于:

可以尽可能削减对原项目的篡改

并且可以实现对不合要求的控件进行个性化定制。基础上完成了对一开始提出的问题的办理。

总结

这种规划照样一种对照不成熟的规划,没有颠末真正项目的认证,当项目对照大年夜时,这种规划可能照样不能够很好的办理问题。不过这也是一次新的考试测验。今后我会就这方面继承进行改动和考试测验。也迎接有设法主见的大年夜家来与我进行评论争论,盼望能不吝见示!

项目的代码在:这个地址

您可能还会对下面的文章感兴趣: