Skip to content

Commit dbaa4b1

Browse files
author
Draveness
committed
Update dknightversion article file
1 parent 376dfbc commit dbaa4b1

File tree

10 files changed

+729
-18
lines changed

10 files changed

+729
-18
lines changed

DKNightVersion/成熟的夜间模式解决方案.md

Lines changed: 128 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
# 成熟的夜间模式解决方案
22

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'>![DKNightVersion-Demo](https://raw.githubusercontent.com/Draveness/DKNightVersion/master/images/DKNightVersion.gif)
6+
7+
其实夜间模式的实现就是相当于**多主题加颜色管理**。而最新版本的 [DKNightVersion](https://github.com/Draveness/DKNightVersion) 已经很好的解决了这个问题。
48

59
在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。
610

7-
> 我们会以对 `backgroundColor` 为例说明整个框架的工作原理
11+
> 我们会以对 `backgroundColor` 为例说明整个框架的工作原理
812
913
## 方法调剂的版本
1014

11-
而如何在不改变原有的架构,甚至不改变原有的代码的基础上,就能为应用优雅地添加夜间模式就成为一个在很多应用开发的过程中不得不面对的一个问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。
15+
如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。
16+
17+
其核心思路就是**使用方法调剂修改 `backgroundColor` 的存取方法**
1218

1319
### 使用 nightBackgroundColor
1420

15-
在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在**分类**中添加 `nightBackgroundColor` 这种属性,并且使用方法调剂改变 `backgroundColor` 的 setter 方法。
21+
在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在**分类**中添加 `nightBackgroundColor` 属性,并且使用方法调剂改变 `backgroundColor` 的 setter 方法。
1622

1723
```objectivec
1824
- (void)hook_setBackgroundColor:(UIColor*)backgroundColor {
@@ -23,11 +29,13 @@
2329
}
2430
```
2531

26-
在当前主题为 `DKThemeVersionNormal` 时,将颜色保存至 `normalBackgroundColor` 属性中,然后在调用原 `backgroundColor` 的 setter 方法,更新视图的颜色。
32+
在当前主题为 `DKThemeVersionNormal` 时,将颜色保存至 `normalBackgroundColor` 中,然后再调用原 `backgroundColor` 的 setter 方法,更新视图的颜色。
33+
34+
### DKNightVersionManager
2735

28-
这时,如果我们在全局修改整个应用的主题时,并不会立刻更新整个应用的颜色
36+
这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面
2937

30-
整个 DKNightVersion 都是由一个 DKNightVersionManager 的单例来管理的,而它的主要只能就是负责改变整个应用的主题、并在主题发生改变时使其它视图更新颜色
38+
整个 DKNightVersion 都是由一个 `DKNightVersionManager` 的单例来管理的,而它的主要工作就是负责**改变应用的主题**、并在主题改变时**通知其它视图更新颜色**
3139

3240
```objectivec
3341
- (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object {
@@ -51,7 +59,7 @@
5159
}
5260
```
5361

54-
如果主题更新,那么就会递归地调用 `changeColor:` 方法,刷新全部的视图颜色,而这个方法的实现实在是太过简单
62+
如果主题更新,那么就会递归地调用 `changeColor` 方法,刷新全部的视图颜色,而这个方法的实现比较简单
5563

5664
```objectivec
5765
- (void)changeColor {
@@ -66,14 +74,19 @@
6674
上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:
6775

6876
1. 在高速滚动的 `scrollView` 上面来回切换夜间模式,会出现颜色错乱的问题
69-
2. 由于对 `backgroundColor` 属性进行**不合适的**方法调剂,其行为无法预测
77+
2. 由于对 `backgroundColor` 属性进行**不合适的**方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同
7078
2. 无法适配第三方 UI 控件
7179

7280
## 使用色表的版本
7381

7482
为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 `nightBackgroundColor` 的使用,并且重新设计底层的实现,转而使用更为**稳定****安全**的方法实现夜间模式,先看一下效果图:
7583

76-
<p align='center'>![DKNightVersion-Demo](https://raw.githubusercontent.com/Draveness/DKNightVersion/master/images/DKNightVersion.gif)
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>
7790

7891
### DKColorPicker
7992

@@ -85,15 +98,17 @@
8598

8699
这个属性其实就是一个 block,它接收参数 `DKThemeVersion *themeVersion`,但是会返回一个 `UIColor *`
87100

101+
> 在第一次传入 picker 或者每次主题改变时,都会将当前主题 `DKThemeVersion` 传入 picker 并执行,然后,将得到的 `UIColor` 赋值给对应的属性 `backgroundColor` 更新视图颜色。
102+
88103
```objectivec
89104
typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);
90105
```
91106

92-
比如使用 `DKColorPickerWithRGB` 创建一个临时的 `DKColorPicker`
107+
比如下面使用 `DKColorPickerWithRGB` 创建一个临时的 `DKColorPicker`
93108

94-
1.`DKThemVersionNormal` 时返回 `0xffffff`
109+
1.`DKThemeVersionNormal` 时返回 `0xffffff`
95110
2.`DKThemeVersionNight` 时返回 `0x343434`
96-
3. 在自定义的主题 `RED` 下返回 `0xfafafa`
111+
3. 在自定义的主题下返回 `0xfafafa` (这里的顺序与色表中主题的顺序有关)
97112

98113
```objectivec
99114
cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);
@@ -109,9 +124,9 @@ cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafaf
109124
@end
110125
```
111126

112-
在第一次使用这个属性时,会将当前对象注册为 `DKNightVersionThemeChangingNotificaiton` 的通知
127+
在第一次使用这个属性时,当前对象注册为 `DKNightVersionThemeChangingNotificaiton` 通知的观察者
113128

114-
在每次收到通知时,都会调用 `night_update` 方法,将当前主题传入 `DKColorPicker`,并再次执行。
129+
在每次收到通知时,都会调用 `night_update` 方法,将当前主题传入 `DKColorPicker`,并再次执行,并将结果传入对应的属性 `[self performSelector:sel withObject:result]`
115130

116131
```objectivec
117132
- (void)night_updateColor {
@@ -133,7 +148,7 @@ cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafaf
133148

134149
### DKColorTable
135150

136-
虽然我们在上面临时创建了一些 `DKColorPicker`不过在 `DKNightVersion` 中,我更推荐使用色表,来减少相同的 `DKColorPicker` 的创建,并且能够更好地管理整个应用中的颜色:
151+
虽然我们在上面临时创建了一些 `DKColorPicker`不过在 `DKNightVersion` 中,我更推荐使用色表,来减少相同的 `DKColorPicker` 的创建,并且能够更好地管理整个应用中的颜色:
137152

138153
```objectivec
139154
NORMAL NIGHT RED
@@ -144,7 +159,7 @@ NORMAL NIGHT RED
144159
#ffffff #444444 #ffffff BAR
145160
```
146161

147-
上面就是默认色表文件 `DKColorTable.txt` 中的内容,其中,第一行表示主题,`NORMAL` 主题必须存在,而且必须为第一行,而最右面的 `BG``SEP` 就是对应 `DKColorPicker` 的 key。
162+
上面就是默认色表文件 `DKColorTable.txt` 中的内容,其中,第一行表示主题,`NORMAL` 主题必须存在,而且必须为第一列,而最右面的 `BG``SEP` 就是对应 `DKColorPicker` 的 key。
148163

149164
```objectivec
150165
self.tableView.dk_backgroundColorPicker = DKColorPickerWithKey(BG);
@@ -154,10 +169,105 @@ self.tableView.dk_backgroundColorPicker = DKColorPickerWithKey(BG);
154169

155170
### pickerify
156171

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+
```
158265

266+
这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的 `generator` 文件夹,其中包含了代码生成器的全部代码。
159267

268+
## 小结
160269

270+
如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的 [README](https://github.com/Draveness/DKNightVersion) 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,`AFNetworking`、 `BlocksKit` 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。
161271

162272
> 关注仓库,及时获得更新:[iOS-Source-Code-Analyze](https://github.com/draveness/iOS-Source-Code-Analyze)
163273
> Follow: [Draveness · Github](https://github.com/Draveness)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Bucket
3+
type = "1"
4+
version = "2.0">
5+
</Bucket>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "0730"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "72A15AD51CCCA22E00DCF938"
18+
BuildableName = "call-method"
19+
BlueprintName = "call-method"
20+
ReferencedContainer = "container:objc.xcodeproj">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
</BuildActionEntries>
24+
</BuildAction>
25+
<TestAction
26+
buildConfiguration = "Debug"
27+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29+
shouldUseLaunchSchemeArgsEnv = "YES">
30+
<Testables>
31+
</Testables>
32+
<MacroExpansion>
33+
<BuildableReference
34+
BuildableIdentifier = "primary"
35+
BlueprintIdentifier = "72A15AD51CCCA22E00DCF938"
36+
BuildableName = "call-method"
37+
BlueprintName = "call-method"
38+
ReferencedContainer = "container:objc.xcodeproj">
39+
</BuildableReference>
40+
</MacroExpansion>
41+
<AdditionalOptions>
42+
</AdditionalOptions>
43+
</TestAction>
44+
<LaunchAction
45+
buildConfiguration = "Debug"
46+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
47+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
48+
launchStyle = "0"
49+
useCustomWorkingDirectory = "NO"
50+
ignoresPersistentStateOnLaunch = "NO"
51+
debugDocumentVersioning = "YES"
52+
debugServiceExtension = "internal"
53+
allowLocationSimulation = "YES">
54+
<BuildableProductRunnable
55+
runnableDebuggingMode = "0">
56+
<BuildableReference
57+
BuildableIdentifier = "primary"
58+
BlueprintIdentifier = "72A15AD51CCCA22E00DCF938"
59+
BuildableName = "call-method"
60+
BlueprintName = "call-method"
61+
ReferencedContainer = "container:objc.xcodeproj">
62+
</BuildableReference>
63+
</BuildableProductRunnable>
64+
<AdditionalOptions>
65+
</AdditionalOptions>
66+
</LaunchAction>
67+
<ProfileAction
68+
buildConfiguration = "Release"
69+
shouldUseLaunchSchemeArgsEnv = "YES"
70+
savedToolIdentifier = ""
71+
useCustomWorkingDirectory = "NO"
72+
debugDocumentVersioning = "YES">
73+
<BuildableProductRunnable
74+
runnableDebuggingMode = "0">
75+
<BuildableReference
76+
BuildableIdentifier = "primary"
77+
BlueprintIdentifier = "72A15AD51CCCA22E00DCF938"
78+
BuildableName = "call-method"
79+
BlueprintName = "call-method"
80+
ReferencedContainer = "container:objc.xcodeproj">
81+
</BuildableReference>
82+
</BuildableProductRunnable>
83+
</ProfileAction>
84+
<AnalyzeAction
85+
buildConfiguration = "Debug">
86+
</AnalyzeAction>
87+
<ArchiveAction
88+
buildConfiguration = "Release"
89+
revealArchiveInOrganizer = "YES">
90+
</ArchiveAction>
91+
</Scheme>

0 commit comments

Comments
 (0)