1
1
# 成熟的夜间模式解决方案
2
2
3
- 从开始写 [ DKNightVersion] ( https://github.com/Draveness/DKNightVersion ) 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定,其实夜间模式的实现就是相当于** 多主题和颜色管理** 。而最新版本的 [ DKNightVersion] ( https://github.com/Draveness/DKNightVersion ) 已经很好的解决了这个问题。
3
+ 从开始写 [ DKNightVersion] ( https://github.com/Draveness/DKNightVersion ) 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。
4
+
5
+ <p align =' center ' >
6
+
7
+ 其实夜间模式的实现就是相当于** 多主题加颜色管理** 。而最新版本的 [ DKNightVersion] ( https://github.com/Draveness/DKNightVersion ) 已经很好的解决了这个问题。
4
8
5
9
在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。
6
10
7
- > 我们会以对 ` backgroundColor ` 为例说明整个框架的工作原理
11
+ > 我们会以对 ` backgroundColor ` 为例说明整个框架的工作原理。
8
12
9
13
## 方法调剂的版本
10
14
11
- 而如何在不改变原有的架构,甚至不改变原有的代码的基础上,就能为应用优雅地添加夜间模式就成为一个在很多应用开发的过程中不得不面对的一个问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。
15
+ 如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。
16
+
17
+ 其核心思路就是** 使用方法调剂修改 ` backgroundColor ` 的存取方法** 。
12
18
13
19
### 使用 nightBackgroundColor
14
20
15
- 在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在** 分类** 中添加 ` nightBackgroundColor ` 这种属性 ,并且使用方法调剂改变 ` backgroundColor ` 的 setter 方法。
21
+ 在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在** 分类** 中添加 ` nightBackgroundColor ` 属性 ,并且使用方法调剂改变 ` backgroundColor ` 的 setter 方法。
16
22
17
23
``` objectivec
18
24
- (void )hook_setBackgroundColor:(UIColor*)backgroundColor {
23
29
}
24
30
```
25
31
26
- 在当前主题为 ` DKThemeVersionNormal ` 时,将颜色保存至 ` normalBackgroundColor ` 属性中,然后在调用原 ` backgroundColor ` 的 setter 方法,更新视图的颜色。
32
+ 在当前主题为 ` DKThemeVersionNormal ` 时,将颜色保存至 ` normalBackgroundColor ` 中,然后再调用原 ` backgroundColor ` 的 setter 方法,更新视图的颜色。
33
+
34
+ ### DKNightVersionManager
27
35
28
- 这时,如果我们在全局修改整个应用的主题时,并不会立刻更新整个应用的颜色 。
36
+ 这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面 。
29
37
30
- 整个 DKNightVersion 都是由一个 DKNightVersionManager 的单例来管理的,而它的主要只能就是负责改变整个应用的主题、并在主题发生改变时使其它视图更新颜色 :
38
+ 整个 DKNightVersion 都是由一个 ` DKNightVersionManager ` 的单例来管理的,而它的主要工作就是负责 ** 改变应用的主题 ** 、并在主题改变时 ** 通知其它视图更新颜色 ** :
31
39
32
40
``` objectivec
33
41
- (void )changeColor:(id <DKNightVersionChangeColorProtocol>)object {
51
59
}
52
60
```
53
61
54
- 如果主题更新,那么就会递归地调用 ` changeColor: ` 方法,刷新全部的视图颜色,而这个方法的实现实在是太过简单 :
62
+ 如果主题更新,那么就会递归地调用 ` changeColor ` 方法,刷新全部的视图颜色,而这个方法的实现比较简单 :
55
63
56
64
``` objectivec
57
65
- (void )changeColor {
66
74
上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:
67
75
68
76
1 . 在高速滚动的 ` scrollView ` 上面来回切换夜间模式,会出现颜色错乱的问题
69
- 2 . 由于对 ` backgroundColor ` 属性进行** 不合适的** 方法调剂,其行为无法预测
77
+ 2 . 由于对 ` backgroundColor ` 属性进行** 不合适的** 方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同
70
78
2 . 无法适配第三方 UI 控件
71
79
72
80
## 使用色表的版本
73
81
74
82
为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 ` nightBackgroundColor ` 的使用,并且重新设计底层的实现,转而使用更为** 稳定** 、** 安全** 的方法实现夜间模式,先看一下效果图:
75
83
76
- <p align =' center ' >
84
+ <p align =' center ' >
85
+ <img src="https://raw.githubusercontent.com/Draveness/DKNightVersion/master/images/DKNightVersion.gif">
86
+ </p >
87
+ <p align =' center ' >
88
+ <em>新的实现不仅能够支持夜间模式,而且能够支持多主题。</em>
89
+ </p >
77
90
78
91
### DKColorPicker
79
92
85
98
86
99
这个属性其实就是一个 block,它接收参数 ` DKThemeVersion *themeVersion ` ,但是会返回一个 ` UIColor * ` :
87
100
101
+ > 在第一次传入 picker 或者每次主题改变时,都会将当前主题 ` DKThemeVersion ` 传入 picker 并执行,然后,将得到的 ` UIColor ` 赋值给对应的属性 ` backgroundColor ` 更新视图颜色。
102
+
88
103
``` objectivec
89
104
typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);
90
105
```
91
106
92
- 比如使用 ` DKColorPickerWithRGB ` 创建一个临时的 ` DKColorPicker ` :
107
+ 比如下面使用 ` DKColorPickerWithRGB ` 创建一个临时的 ` DKColorPicker ` :
93
108
94
- 1 . 在 ` DKThemVersionNormal ` 时返回 ` 0xffffff `
109
+ 1 . 在 ` DKThemeVersionNormal ` 时返回 ` 0xffffff `
95
110
2 . 在 ` DKThemeVersionNight ` 时返回 ` 0x343434 `
96
- 3 . 在自定义的主题 ` RED ` 下返回 ` 0xfafafa `
111
+ 3 . 在自定义的主题下返回 ` 0xfafafa ` (这里的顺序与色表中主题的顺序有关)
97
112
98
113
``` objectivec
99
114
cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff , 0x343434 , 0xfafafa );
@@ -109,9 +124,9 @@ cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafaf
109
124
@end
110
125
```
111
126
112
- 在第一次使用这个属性时,会将当前对象注册为 ` DKNightVersionThemeChangingNotificaiton ` 的通知 。
127
+ 在第一次使用这个属性时,当前对象注册为 ` DKNightVersionThemeChangingNotificaiton ` 通知的观察者 。
113
128
114
- 在每次收到通知时,都会调用 ` night_update ` 方法,将当前主题传入 ` DKColorPicker ` ,并再次执行。
129
+ 在每次收到通知时,都会调用 ` night_update ` 方法,将当前主题传入 ` DKColorPicker ` ,并再次执行,并将结果传入对应的属性 ` [self performSelector:sel withObject:result] ` 。
115
130
116
131
``` objectivec
117
132
- (void )night_updateColor {
@@ -133,7 +148,7 @@ cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafaf
133
148
134
149
### DKColorTable
135
150
136
- 虽然我们在上面临时创建了一些 ` DKColorPicker ` , 不过在 ` DKNightVersion ` 中,我更推荐使用色表,来减少相同的 ` DKColorPicker ` 的创建,并且能够更好地管理整个应用中的颜色:
151
+ 虽然我们在上面临时创建了一些 ` DKColorPicker ` 。 不过在 ` DKNightVersion ` 中,我更推荐使用色表,来减少相同的 ` DKColorPicker ` 的创建,并且能够更好地管理整个应用中的颜色:
137
152
138
153
``` objectivec
139
154
NORMAL NIGHT RED
@@ -144,7 +159,7 @@ NORMAL NIGHT RED
144
159
#ffffff #444444 #ffffff BAR
145
160
```
146
161
147
- 上面就是默认色表文件 ` DKColorTable.txt ` 中的内容,其中,第一行表示主题,` NORMAL ` 主题必须存在,而且必须为第一行 ,而最右面的 ` BG ` 、` SEP ` 就是对应 ` DKColorPicker ` 的 key。
162
+ 上面就是默认色表文件 ` DKColorTable.txt ` 中的内容,其中,第一行表示主题,` NORMAL ` 主题必须存在,而且必须为第一列 ,而最右面的 ` BG ` 、` SEP ` 就是对应 ` DKColorPicker ` 的 key。
148
163
149
164
``` objectivec
150
165
self.tableView.dk_backgroundColorPicker = DKColorPickerWithKey(BG);
@@ -154,10 +169,105 @@ self.tableView.dk_backgroundColorPicker = DKColorPickerWithKey(BG);
154
169
155
170
### pickerify
156
171
157
- 虽然说,我们使用色表以及 ` DKColorPicker ` 解决了
172
+ 虽然说,我们使用色表以及 ` DKColorPicker ` 解决了,但是,到目前为止我们还没有解决第三方框架的问题。
173
+
174
+ 比如我们使用了某个第三方框架,或者自己添加了某个 ` color ` 属性,比如说:
175
+
176
+ ``` objectivec
177
+ @interface DKView ()
178
+
179
+ @property (nonatomic , strong ) UIColor * weirdColor;
180
+
181
+ @end
182
+ ```
183
+
184
+ ` weirdColor ` 并没有对应的 ` DKColorPicker ` ,但是,我们可以通过 ` pickerify ` 在想要使用 ` dk_weirdColorPicker ` 的地方生成这个对应的 picker:
185
+
186
+ ``` objectivec
187
+ @pickerify(DKView, weirdColor);
188
+ ```
189
+
190
+ 然后,我们就可以使用 ` dk_weirdColorPicker ` 属性了:
191
+
192
+ ``` objectivec
193
+ view.dk_weirdColorPicker = DKColorPickerWithKey(BG);
194
+ ```
195
+
196
+ ` pickerify ` 其实是一个宏:
197
+
198
+ ``` objectivec
199
+ #define pickerify (KLASS, PROPERTY ) interface \
200
+ KLASS (Night) \
201
+ @property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \
202
+ @end \
203
+ @interface \
204
+ KLASS () \
205
+ @property (nonatomic, strong) NSMutableDictionary<NSString * , DKColorPicker> * pickers; \
206
+ @end \
207
+ @implementation \
208
+ KLASS (Night) \
209
+ - (DKColorPicker)dk_ ## PROPERTY ## Picker { \
210
+ return objc_getAssociatedObject(self, @selector (dk_ ## PROPERTY ## Picker)); \
211
+ } \
212
+ - (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \
213
+ objc_setAssociatedObject(self, @selector (dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \
214
+ [ self setValue: picker (self.dk_manager.themeVersion) forKeyPath:@keypath (self, PROPERTY)] ;\
215
+ [ self.pickers setValue:[ picker copy] forKey:_ DKSetterWithPROPERTYerty(@#PROPERTY)] ; \
216
+ } \
217
+ @end
218
+ ```
219
+
220
+ 这个宏根据传入的类和属性名,为我们生成了对应 `picker` 的存取方法,它也可以说是一种元编程的手段。
221
+
222
+ > 这里生成的 setter 方法不是标准意义上的驼峰命名法 `dk_setweirdColorPicker:`,因为我不知道怎么才能让大写首字母之后的属性添加到这里(如果各位读者有解决方案,欢迎提 PR 或者 issue)。
223
+
224
+ ## 嵌入式 Ruby
225
+
226
+ 由于框架中很多的代码,都是重复的,所以在这里使用了**嵌入式 Ruby 模板**来生成对应的文件 `color.m.irb`:
227
+
228
+ ```objectivec
229
+ //
230
+ // <%= klass.name %>+Night.m
231
+ // <%= klass.name %>+Night
232
+ //
233
+ // Copyright (c) 2015 Draveness. All rights reserved.
234
+ //
235
+ // These files are generated by ruby script, if you want to modify code
236
+ // in this file, you are supposed to update the ruby code, run it and
237
+ // test it. And finally open a pull request.
238
+
239
+ #import "<%= klass.name %>+Night.h"
240
+ #import "DKNightVersionManager.h"
241
+ #import <objc/runtime.h>
242
+
243
+ @interface <%= klass.name %> ()
244
+
245
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;
246
+
247
+ @end
248
+
249
+ @implementation <%= klass.name %> (Night)
250
+
251
+ <% klass.properties.each do |property| %><%= """
252
+ - (DKColorPicker)dk_#{property.name}Picker {
253
+ return objc_getAssociatedObject(self, @selector(dk_#{property.name}Picker));
254
+ }
255
+
256
+ - (void)dk_set#{property.cap_name}Picker:(DKColorPicker)picker {
257
+ objc_setAssociatedObject(self, @selector(dk_#{property.name}Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
258
+ self.#{property.name} = picker(self.dk_manager.themeVersion);
259
+ [self.pickers setValue:[picker copy] forKey:@\"#{property.setter}\"];
260
+ }
261
+ """ %><% end %>
262
+
263
+ @end
264
+ ```
158
265
266
+ 这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的 `generator` 文件夹,其中包含了代码生成器的全部代码。
159
267
268
+ ## 小结
160
269
270
+ 如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的 [README](https:// github.com/Draveness/DKNightVersion) 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,`AFNetworking`、 `BlocksKit` 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。
161
271
162
272
> 关注仓库,及时获得更新:[iOS-Source-Code-Analyze](https:// github.com/draveness/iOS-Source-Code-Analyze)
163
273
> Follow: [Draveness · Github](https:// github.com/Draveness)
0 commit comments