forked from draveness/analyze
-
Notifications
You must be signed in to change notification settings - Fork 0
/
成熟的夜间模式解决方案.md
277 lines (200 loc) · 11.5 KB
/
成熟的夜间模式解决方案.md
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# 成熟的夜间模式解决方案
从开始写 [DKNightVersion](https://github.com/Draveness/DKNightVersion) 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。
其实夜间模式的实现就是相当于**多主题加颜色管理**。而最新版本的 [DKNightVersion](https://github.com/Draveness/DKNightVersion) 已经很好的解决了这个问题。
在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。
> 我们会以对 `backgroundColor` 为例说明整个框架的工作原理。
<p align='center'>
<img src="https://raw.githubusercontent.com/Draveness/DKNightVersion/master/images/DKNightVersion.gif">
</p>
## 方法调剂的版本
如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。
其核心思路就是**使用方法调剂修改 `backgroundColor` 的存取方法**。
### 使用 nightBackgroundColor
在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在**分类**中添加 `nightBackgroundColor` 属性,并且使用方法调剂改变 `backgroundColor` 的 setter 方法。
```objectivec
- (void)hook_setBackgroundColor:(UIColor*)backgroundColor {
if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {
[self setNormalBackgroundColor:backgroundColor];
}
[self hook_setBackgroundColor:backgroundColor];
}
```
在当前主题为 `DKThemeVersionNormal` 时,将颜色保存至 `normalBackgroundColor` 中,然后再调用原 `backgroundColor` 的 setter 方法,更新视图的颜色。
### DKNightVersionManager
这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。
整个 DKNightVersion 都是由一个 `DKNightVersionManager` 的单例来管理的,而它的主要工作就是负责**改变应用的主题**、并在主题改变时**通知其它视图更新颜色**:
```objectivec
- (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` 方法,刷新全部的视图颜色,而这个方法的实现比较简单:
```objectivec
- (void)changeColor {
if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {
self.backgroundColor = self.normalBackgroundColor;
} else {
self.backgroundColor = self.nightBackgroundColor;
}
}
```
上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:
1. 在高速滚动的 `scrollView` 上面来回切换夜间模式,会出现颜色错乱的问题
2. 由于对 `backgroundColor` 属性进行**不合适的**方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同
2. 无法适配第三方 UI 控件
## 使用色表的版本
为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 `nightBackgroundColor` 的使用,并且重新设计底层的实现,转而使用更为**稳定**、**安全**的方法实现夜间模式,先看一下效果图:
<p align='center'>
<img src="https://raw.githubusercontent.com/Draveness/DKNightVersion/master/images/DKNightVersion.gif">
</p>
<p align='center'>
<em>新的实现不仅能够支持夜间模式,而且能够支持多主题。</em>
</p>
### DKColorPicker
与上一个版本实现上的不同,在 2.0 中删除了全部的 `nightBackgroundColor`,使用一个名为 `dk_backgroundColorPicker` 的属性取代它。
```objectivec
@property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;
```
这个属性其实就是一个 block,它接收参数 `DKThemeVersion *themeVersion`,但是会返回一个 `UIColor *`:
> 在第一次传入 picker 或者每次主题改变时,都会将当前主题 `DKThemeVersion` 传入 picker 并执行,然后,将得到的 `UIColor` 赋值给对应的属性 `backgroundColor` 更新视图颜色。
```objectivec
typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);
```
比如下面使用 `DKColorPickerWithRGB` 创建一个临时的 `DKColorPicker`:
1. 在 `DKThemeVersionNormal` 时返回 `0xffffff`
2. 在 `DKThemeVersionNight` 时返回 `0x343434`
3. 在自定义的主题下返回 `0xfafafa` (这里的顺序与色表中主题的顺序有关)
```objectivec
cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);
```
同时,每一个对象还持有一个 `pickers` 数组,来存储自己的全部 `DKColorPicker`:
```objectivec
@interface NSObject ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;
@end
```
在第一次使用这个属性时,当前对象注册为 `DKNightVersionThemeChangingNotificaiton` 通知的观察者。
在每次收到通知时,都会调用 `night_update` 方法,将当前主题传入 `DKColorPicker`,并再次执行,并将结果传入对应的属性 `[self performSelector:sel withObject:result]`。
```objectivec
- (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
}];
}];
}
```
也就是说,在每次改变主题的时候,都会发出通知。
### DKColorTable
虽然我们在上面临时创建了一些 `DKColorPicker`。不过在 `DKNightVersion` 中,我更推荐使用色表,来减少相同的 `DKColorPicker` 的创建,并且能够更好地管理整个应用中的颜色:
```objectivec
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。
```objectivec
self.tableView.dk_backgroundColorPicker = DKColorPickerWithKey(BG);
```
在使用时,上面的代码就相当于返回了一个在 `NORMAL` 时返回 `#ffffff`、`NIGHT` 时返回 `#343434` 以及 `RED` 时返回 `#fafafa` 的 `DKColorPicker`。
### pickerify
虽然说,我们使用色表以及 `DKColorPicker` 解决了,但是,到目前为止我们还没有解决第三方框架的问题。
比如我们使用了某个第三方框架,或者自己添加了某个 `color` 属性,比如说:
```objectivec
@interface DKView ()
@property (nonatomic, strong) UIColor *weirdColor;
@end
```
`weirdColor` 并没有对应的 `DKColorPicker`,但是,我们可以通过 `pickerify` 在想要使用 `dk_weirdColorPicker` 的地方生成这个对应的 picker:
```objectivec
@pickerify(DKView, weirdColor);
```
然后,我们就可以使用 `dk_weirdColorPicker` 属性了:
```objectivec
view.dk_weirdColorPicker = DKColorPickerWithKey(BG);
```
`pickerify` 其实是一个宏:
```objectivec
#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
由于框架中很多的代码,都是重复的,所以在这里使用了**嵌入式 Ruby 模板**来生成对应的文件 `color.m.irb`:
```objectivec
//
// <%= 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](https://github.com/Draveness/DKNightVersion) 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,`AFNetworking`、 `BlocksKit` 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。
> 关注仓库,及时获得更新:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze)
> Follow: [Draveness · Github](https://github.com/Draveness)