成熟的夜间模式
从开始写DKNightVersion这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。
其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的DKNightVersion已经很好的解决了这个问题。
在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。
我们会以对backgroundColor为例说明整个框架的工作原理。
方法调剂的版本
如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。
其核心思路就是使用方法调剂修改backgroundColor的存取方法。
使用 nightBackgroundColor
在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在分类中添加nightBackgroundColor属性,并且使用方法调剂改变backgroundColor的 setter 方法。
-(void)hook_setBackgroundColor:(UIColor*)backgroundColor{if([DKNightVersionManager currentThemeVersion]==DKThemeVersionNormal){[selfsetNormalBackgroundColor:backgroundColor];}[selfhook_setBackgroundColor:backgroundColor];}
在当前主题为DKThemeVersionNormal时,将颜色保存至normalBackgroundColor中,然后再调用原backgroundColor的 setter 方法,更新视图的颜色。
DKNightVersionManager
这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。
整个 DKNightVersion 都是由一个DKNightVersionManager的单例来管理的,而它的主要工作就是负责改变应用的主题、并在主题改变时通知其它视图更新颜色:
-(void)changeColor:(id)object{if([object respondsToSelector:@selector(changeColor)]){[object changeColor];}if([object respondsToSelector:@selector(subviews)]){if(![object subviews]){// Basic case, do nothing.return;}else{for(id subviewin[object subviews]){// recursive darken all the subviews of current view.[selfchangeColor:subview];if([subview respondsToSelector:@selector(changeColor)]){[subview changeColor];}}}}}
如果主题更新,那么就会递归地调用changeColor方法,刷新全部的视图颜色,而这个方法的实现比较简单:
-(void)changeColor{if([DKNightVersionManager currentThemeVersion]==DKThemeVersionNormal){self.backgroundColor=self.normalBackgroundColor;}else{self.backgroundColor=self.nightBackgroundColor;}}
上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:
在高速滚动的scrollView上面来回切换夜间模式,会出现颜色错乱的问题
由于对backgroundColor属性进行不合适的方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同
无法适配第三方 UI 控件
使用色表的版本
为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对nightBackgroundColor的使用,并且重新设计底层的实现,转而使用更为稳定、安全的方法实现夜间模式,先看一下效果图:
新的实现不仅能够支持夜间模式,而且能够支持多主题。
DKColorPicker
与上一个版本实现上的不同,在 2.0 中删除了全部的nightBackgroundColor,使用一个名为dk_backgroundColorPicker的属性取代它。
@property(nonatomic,copy)DKColorPicker dk_backgroundColorPicker;
这个属性其实就是一个 block,它接收参数DKThemeVersion *themeVersion,但是会返回一个UIColor *:
在第一次传入 picker 或者每次主题改变时,都会将当前主题DKThemeVersion传入 picker 并执行,然后,将得到的UIColor赋值给对应的属性backgroundColor更新视图颜色。
typedefUIColor*(^DKColorPicker)(DKThemeVersion*themeVersion);
比如下面使用DKColorPickerWithRGB创建一个临时的DKColorPicker:
在DKThemeVersionNormal时返回0xffffff
在DKThemeVersionNight时返回0x343434
在自定义的主题下返回0xfafafa(这里的顺序与色表中主题的顺序有关)
cell.dk_backgroundColorPicker=DKColorPickerWithRGB(0xffffff,0x343434,0xfafafa);
同时,每一个对象还持有一个pickers数组,来存储自己的全部DKColorPicker:
@interfaceNSObject()@property(nonatomic,strong)NSMutableDictionary*pickers;@end
在第一次使用这个属性时,当前对象注册为DKNightVersionThemeChangingNotificaiton通知的观察者。
在每次收到通知时,都会调用night_update方法,将当前主题传入DKColorPicker,并再次执行,并将结果传入对应的属性[self performSelector:sel withObject:result]。
-(void)night_updateColor{[self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString*_Nonnull selector,DKColorPicker _Nonnull picker,BOOL*_Nonnull stop){SEL sel=NSSelectorFromString(selector);id result=picker(self.dk_manager.themeVersion);[UIView animateWithDuration:DKNightVersionAnimationDuration animations:^{#pragma clang diagnostic push#pragma clang diagnostic ignored "-Warc-performSelector-leaks"[selfperformSelector:sel withObject:result];#pragma clang diagnostic pop}];}];}
也就是说,在每次改变主题的时候,都会发出通知。
DKColorTable
虽然我们在上面临时创建了一些DKColorPicker。不过在DKNightVersion中,我更推荐使用色表,来减少相同的DKColorPicker的创建,并且能够更好地管理整个应用中的颜色:
NORMAL NIGHT RED#ffffff #343434 #fafafa BG#aaaaaa #313131 #aaaaaa SEP#0000ff #ffffff #fa0000 TINT#000000#ffffff #000000TEXT#ffffff #444444 #ffffff BAR
上面就是默认色表文件DKColorTable.txt中的内容,其中,第一行表示主题,NORMAL主题必须存在,而且必须为第一列,而最右面的BG、SEP就是对应DKColorPicker的 key。
self.tableView.dk_backgroundColorPicker=DKColorPickerWithKey(BG);
在使用时,上面的代码就相当于返回了一个在NORMAL时返回#ffffff、NIGHT时返回#343434以及RED时返回#fafafa的DKColorPicker。
pickerify
虽然说,我们使用色表以及DKColorPicker解决了,但是,到目前为止我们还没有解决第三方框架的问题。
比如我们使用了某个第三方框架,或者自己添加了某个color属性,比如说:
@interfaceDKView()@property(nonatomic,strong)UIColor*weirdColor;@end
weirdColor并没有对应的DKColorPicker,但是,我们可以通过pickerify在想要使用dk_weirdColorPicker的地方生成这个对应的 picker:
@pickerify(DKView,weirdColor);
然后,我们就可以使用dk_weirdColorPicker属性了:
view.dk_weirdColorPicker=DKColorPickerWithKey(BG);
pickerify其实是一个宏:
#define pickerify(KLASS, PROPERTY) interface \
KLASS (Night) \
@property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \
@end \
@interface \
KLASS () \
@property (nonatomic, strong) NSMutableDictionary *pickers; \
@end \
@implementation \
KLASS (Night) \
- (DKColorPicker)dk_ ## PROPERTY ## Picker { \
return objc_getAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker)); \
} \
- (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \
objc_setAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \
[self setValue:picker(self.dk_manager.themeVersion) forKeyPath:@keypath(self, PROPERTY)];\
[self.pickers setValue:[picker copy] forKey:_DKSetterWithPROPERTYerty(@#PROPERTY)]; \
} \
@end
这个宏根据传入的类和属性名,为我们生成了对应picker的存取方法,它也可以说是一种元编程的手段。
这里生成的 setter 方法不是标准意义上的驼峰命名法dk_setweirdColorPicker:,因为我不知道怎么才能让大写首字母之后的属性添加到这里(如果各位读者有解决方案,欢迎提 PR 或者 issue)。
嵌入式 Ruby
由于框架中很多的代码,都是重复的,所以在这里使用了嵌入式 Ruby 模板来生成对应的文件color.m.irb:
//// <%= klass.name %>+Night.m// <%= klass.name %>+Night//// Copyright (c) 2015 Draveness. All rights reserved.//// These files are generated by ruby script, if you want to modify code// in this file, you are supposed to update the ruby code, run it and// test it. And finally open a pull request.#import "<%= klass.name %>+Night.h"#import "DKNightVersionManager.h"#import @interface<%=klass.name%>()@property(nonatomic,strong)NSMutableDictionary*pickers;@end@implementation<%=klass.name%>(Night)<%klass.properties.eachdo|property|%><%="""-(DKColorPicker)dk_#{property.name}Picker{returnobjc_getAssociatedObject(self,@selector(dk_#{property.name}Picker));}-(void)dk_set#{property.cap_name}Picker:(DKColorPicker)picker{objc_setAssociatedObject(self,@selector(dk_#{property.name}Picker),picker,OBJC_ASSOCIATION_COPY_NONATOMIC);self.#{property.name}=picker(self.dk_manager.themeVersion);[self.pickers setValue:[picker copy]forKey:@\"#{property.setter}\"];}"""%><%end%>@end
这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的generator文件夹,其中包含了代码生成器的全部代码。
小结
如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的README文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,AFNetworking、BlocksKit也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。