关注仓库,及时获得更新:iOS-Source-Code-Analyze
Follow: Draveness Github
从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。
其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。
在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。
我们会以对
backgroundColor
为例说明整个框架的工作原理。
如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。
其核心思路就是使用方法调剂修改 backgroundColor
的存取方法。
在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在分类中添加 nightBackgroundColor
属性,并且使用方法调剂改变 backgroundColor
的 setter 方法。
- (void)hook_setBackgroundColor:(UIColor*)backgroundColor { if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) { [self setNormalBackgroundColor:backgroundColor]; } [self hook_setBackgroundColor:backgroundColor]; }
在当前主题为 DKThemeVersionNormal
时,将颜色保存至 normalBackgroundColor
中,然后再调用原 backgroundColor
的 setter 方法,更新视图的颜色。
这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。
整个 DKNightVersion 都是由一个 DKNightVersionManager
的单例来管理的,而它的主要工作就是负责改变应用的主题、并在主题改变时通知其它视图更新颜色:
- (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object { if ([object respondsToSelector:@selector(changeColor)]) { [object changeColor]; } if ([object respondsToSelector:@selector(subviews)]) { if (![object subviews]) { // Basic case, do nothing. return; } else { for (id subview in [object subviews]) { // recursive darken all the subviews of current view. [self changeColor: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
属性进行不合适的方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 nightBackgroundColor
的使用,并且重新设计底层的实现,转而使用更为稳定、安全的方法实现夜间模式,先看一下效果图:
新的实现不仅能够支持夜间模式,而且能够支持多主题。
与上一个版本实现上的不同,在 2.0 中删除了全部的 nightBackgroundColor
,使用一个名为 dk_backgroundColorPicker
的属性取代它。
@property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;
这个属性其实就是一个 block ,它接收参数 DKThemeVersion *themeVersion
,但是会返回一个 UIColor *
:
在第一次传入 picker 或者每次主题改变时,都会将当前主题
DKThemeVersion
传入 picker 并执行,然后,将得到的UIColor
赋值给对应的属性backgroundColor
更新视图颜色。
typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);
比如下面使用 DKColorPickerWithRGB
创建一个临时的 DKColorPicker
:
DKThemeVersionNormal
时返回 0xffffff
DKThemeVersionNight
时返回 0x343434
0xfafafa
(这里的顺序与色表中主题的顺序有关)cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfaffa);
同时,每一个对象还持有一个 pickers
数组,来存储自己的全部 DKColorPicker
:
@interface NSObject () @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *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" [self performSelector:sel withObject:result]; #pragma clang diagnostic pop }]; }]; }
也就是说,在每次改变主题的时候,都会发出通知。
虽然我们在上面临时创建了一些 DKColorPicker
。不过在 DKNightVersion
中,我更推荐使用色表,来减少相同的 DKColorPicker
的创建,并且能够更好地管理整个应用中的颜色:
NORMAL NIGHT RED #ffffff #343434 #fafafa BG #aaaaaa #313131 #aaaaaa SEP #0000ff #ffffff #fa0000 TINT #000000 #ffffff #000000 TEXT #ffffff #444444 #ffffff BAR
上面就是默认色表文件 DKColorTable.txt
中的内容,其中,第一行表示主题,NORMAL
主题必须存在,而且必须为第一列,而最右面的 BG
、SEP
就是对应 DKColorPicker
的 key 。
self.tableView.dk_backgroundColorPicker = DKColorPickerWithKey(BG);
在使用时,上面的代码就相当于返回了一个在 NORMAL
时返回 #ffffff
、NIGHT
时返回 #343434
以及 RED
时返回 #fafafa
的 DKColorPicker
。
虽然说,我们使用色表以及 DKColorPicker
解决了,但是,到目前为止我们还没有解决第三方框架的问题。
比如我们使用了某个第三方框架,或者自己添加了某个 color
属性,比如说:
@interface DKView () @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<NSString *, DKColorPicker> *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 模板来生成对应的文件 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 <objc/runtime.h> @interface <%= klass.name %> () @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers; @end @implementation <%= klass.name %> (Night) <% klass.properties.each do |property| %><%= """ - (DKColorPicker)dk_#{property.name}Picker { return objc_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 才能优雅地解决问题。
关注仓库,及时获得更新:iOS-Source-Code-Analyze
Follow: Draveness Github
![]() | 1 herozzm 2016-05-04 15:52:02 +08:00 via Android ....lz 这样是不想让大家好好的看 V2EX 吗?什么玩意 |
2 lxy &nbp; 2016-05-04 16:08:46 +08:00 好像楼上的手机用户以为自己就是全世界了。 |
![]() | 3 area346 2016-05-04 16:23:44 +08:00 学习一下,感觉夜间模式需要解决的问题蛮多的,尤其是针对图片多的应用 |
![]() | 4 Draven OP |
5 a412739861 2016-05-04 16:34:37 +08:00 @Draven 很棒,之前也尝试过,不过项目有使用 XIB 和 UIAppearance ,就没有使用。问下后续有提供这两者的支持吗?尤其是前者。 |
![]() | 6 Draven OP @a412739861 其实,我一直在考虑这个事情,不过没有什么眉目,我尽量提供前者的支持把,对于 UIAppearance 的支持还是比较容易的,你确实提醒了我,因为我们目前的项目中没有使用 UIAppearance ,所以我也没有想到添加相关的 API |
![]() | 7 tzheng 2016-05-04 16:36:37 +08:00 via iPhone Red 只是红字吗?天文用途那种红光界面也许会更实用? |
![]() | 8 Draven OP @tzheng 额。。并不是这个意思,,。这个 RED 是个自定义的主题,我给它起了个名字叫 RED , 有对应的背景颜色之类,这些都是可以自己配置的 |
9 a412739861 2016-05-04 17:17:49 +08:00 @Draven 我之前翻的一个 folk 里面实现了小部分的功能,不知道你有没看看到过,等晚上回去我找找,把那个 folk 的地址发给你看看。 |
10 a412739861 2016-05-04 17:19:41 +08:00 大体上是 IBInspectable 和 Runtime 替换 awakeFromNib 的做法。 |
![]() | 11 Draven OP @a412739861 好的, 我去看一下这个 |
![]() | 12 sablib 2016-05-04 18:51:27 +08:00 我目前是 swizzle 掉 viewWillAppear , viewDidDisappear , willMoveToWindow 等方法然后在适当的时候调用 applySkin 方法实现换肤之类的。。 |
13 a412739861 2016-05-04 22:53:52 +08:00 |
![]() | 14 fhefh 2016-06-16 16:51:38 +08:00 mark~~ |