成熟的夜间模式解决方案


从开始写 [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)




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值