iOS拓展---【转载】成熟的夜间模式解决方案

【转载】成熟的夜间模式解决方案

04 May 2016 iOSgit

关注仓库,及时得到更新:iOS-Source-Code-Analyzegithub

从开始写 DKNightVersion 这个框架到如今已经将近一年了,目前整个框架的设计也趋于稳定。编程

其实夜间模式的实现就是至关于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。数组

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

咱们会以对 backgroundColor 为例说明整个框架的工做原理。ruby

方法调剂的版本

如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为不少开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。架构

其核心思路就是使用方法调剂修改 backgroundColor 的存取方法框架

使用 nightBackgroundColor

在思考以后,我想到,想要在不改动原有代码的基础上实现夜间模式只能经过在分类中添加 nightBackgroundColor 属性,而且使用方法调剂改变 backgroundColor 的 setter 方法。post

- (void)hook_setBackgroundColor:(UIColor*)backgroundColor { if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) { [self setNormalBackgroundColor:backgroundColor]; } [self hook_setBackgroundColor:backgroundColor]; } 

在当前主题为 DKThemeVersionNormal 时,将颜色保存至 normalBackgroundColor 中,而后再调用原 backgroundColor 的 setter 方法,更新视图的颜色。this

DKNightVersionManager

这里只解决了颜色设置的问题,下面会说明,若是在主题改变时,实时更新颜色,而不用从新进入当前页面。

整个 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 在实际应用中会有比较多的问题:

  1. 在高速滚动的 scrollView 上面来回切换夜间模式,会出现颜色错乱的问题
  2. 因为对 backgroundColor 属性进行不合适的方法调剂,其行为没法预测,好比:在设置颜色后,再取出,不必定与设置时传入的颜色相同
  3. 没法适配第三方 UI 控件

使用色表的版本

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

<em>新的实现不只可以支持夜间模式,并且可以支持多主题。</em>

</p>

DKColorPicker

与上一个版本实现上的不一样,在 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

  1. DKThemeVersionNormal 时返回 0xffffff
  2. DKThemeVersionNight 时返回 0x343434
  3. 在自定义的主题下返回 0xfafafa (这里的顺序与色表中主题的顺序有关)
cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa); 

同时,每个对象还持有一个 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 }]; }]; } 

也就是说,在每次改变主题的时候,都会发出通知。

DKColorTable

虽然咱们在上面临时建立了一些 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 主题必须存在,并且必须为第一列,而最右面的 BGSEP 就是对应 DKColorPicker 的 key。

self.tableView.dk_backgroundColorPicker = DKColorPickerWithKey(BG); 

在使用时,上面的代码就至关于返回了一个在 NORMAL 时返回 #ffffffNIGHT 时返回 #343434 以及 RED 时返回 #fafafaDKColorPicker

pickerify

虽说,咱们使用色表以及 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

因为框架中不少的代码,都是重复的,因此在这里使用了嵌入式 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,我我的以为是没有问题,AFNetworkingBlocksKit 也使用方法调剂来改变原有方法的实现,不能由于它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。

关注仓库,及时得到更新:iOS-Source-Code-Analyze

 

 

 

 

T

相关文章
相关标签/搜索