iOS成熟的夜间模式解决方案

XASJer 3年前
   <p>关注仓库,及时获得更新: <a href="/misc/goto?guid=4959671674863698147" rel="nofollow,noindex">iOS-Source-Code-Analyze</a></p>    <p>从开始写 <a href="/misc/goto?guid=4958873479023124863" rel="nofollow,noindex">DKNightVersion</a> 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。</p>    <p>其实夜间模式的实现就是相当于 <strong>多主题加颜色管理</strong> 。而最新版本的 <a href="/misc/goto?guid=4958873479023124863" rel="nofollow,noindex">DKNightVersion</a> 已经很好的解决了这个问题。</p>    <p>在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。</p>    <p>我们会以对 backgroundColor 为例说明整个框架的工作原理。</p>    <p><img src="https://simg.open-open.com/show/a3944d1d50407a8c564de22feaaba243.gif"></p>    <h2>方法调剂的版本</h2>    <p>如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。</p>    <p>其核心思路就是 <strong> 使用方法调剂修改 backgroundColor 的存取方法 </strong> 。</p>    <h3>使用 nightBackgroundColor</h3>    <p>在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在 <strong>分类</strong> 中添加 nightBackgroundColor 属性,并且使用方法调剂改变 backgroundColor 的 setter 方法。</p>    <pre>  <code class="language-objectivec">- (void)hook_setBackgroundColor:(UIColor*)backgroundColor {      if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {          [self setNormalBackgroundColor:backgroundColor];      }      [self hook_setBackgroundColor:backgroundColor];  }  </code></pre>    <p>在当前主题为 DKThemeVersionNormal 时,将颜色保存至 normalBackgroundColor 中,然后再调用原 backgroundColor 的 setter 方法,更新视图的颜色。</p>    <h3>DKNightVersionManager</h3>    <p>这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。</p>    <p>整个 DKNightVersion 都是由一个 DKNightVersionManager 的单例来管理的,而它的主要工作就是负责 <strong>改变应用的主题</strong> 、并在主题改变时 <strong>通知其它视图更新颜色</strong> :</p>    <pre>  <code class="language-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];                  }              }          }      }  }  </code></pre>    <p>如果主题更新,那么就会递归地调用 changeColor 方法,刷新全部的视图颜色,而这个方法的实现比较简单:</p>    <pre>  <code class="language-objectivec">- (void)changeColor {      if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {          self.backgroundColor = self.normalBackgroundColor;      } else {          self.backgroundColor = self.nightBackgroundColor;      }  }  </code></pre>    <p>上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:</p>    <ol>     <li>在高速滚动的 scrollView 上面来回切换夜间模式,会出现颜色错乱的问题</li>     <li>由于对 backgroundColor 属性进行 <strong>不合适的</strong> 方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同</li>     <li>无法适配第三方 UI 控件</li>    </ol>    <h2>使用色表的版本</h2>    <p>为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 nightBackgroundColor 的使用,并且重新设计底层的实现,转而使用更为 <strong>稳定</strong> 、 <strong>安全</strong> 的方法实现夜间模式,先看一下效果图:</p>    <p><img alt="iOS成熟的夜间模式解决方案" src="https://simg.open-open.com/show/a3944d1d50407a8c564de22feaaba243.gif"></p>    <p>新的实现不仅能够支持夜间模式,而且能够支持多主题。</p>    <h3>DKColorPicker</h3>    <p>与上一个版本实现上的不同,在 2.0 中删除了全部的 nightBackgroundColor ,使用一个名为 dk_backgroundColorPicker 的属性取代它。</p>    <pre>  <code class="language-objectivec">@property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;  </code></pre>    <p>这个属性其实就是一个 block,它接收参数 DKThemeVersion *themeVersion ,但是会返回一个 UIColor * :</p>    <p>在第一次传入 picker 或者每次主题改变时,都会将当前主题 DKThemeVersion 传入 picker 并执行,然后,将得到的 UIColor 赋值给对应的属性 backgroundColor 更新视图颜色。</p>    <pre>  <code class="language-objectivec">typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);    </code></pre>    <p>比如下面使用 DKColorPickerWithRGB 创建一个临时的 DKColorPicker :</p>    <ol>     <li>在 DKThemeVersionNormal 时返回 0xffffff</li>     <li>在 DKThemeVersionNight 时返回 0x343434</li>     <li>在自定义的主题下返回 0xfafafa (这里的顺序与色表中主题的顺序有关)</li>    </ol>    <pre>  <code class="language-objectivec">cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);    </code></pre>    <p>同时,每一个对象还持有一个 pickers 数组,来存储自己的全部 DKColorPicker :</p>    <pre>  <code class="language-objectivec">@interface NSObject ()    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;    @end  </code></pre>    <p>在第一次使用这个属性时,当前对象注册为 DKNightVersionThemeChangingNotificaiton 通知的观察者。</p>    <p>在每次收到通知时,都会调用 night_update 方法,将当前主题传入 DKColorPicker ,并再次执行,并将结果传入对应的属性 [self performSelector:sel withObject:result] 。</p>    <pre>  <code class="language-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                           }];      }];  }  </code></pre>    <p>也就是说,在每次改变主题的时候,都会发出通知。</p>    <h3>DKColorTable</h3>    <p>虽然我们在上面临时创建了一些 DKColorPicker 。不过在 DKNightVersion 中,我更推荐使用色表,来减少相同的 DKColorPicker 的创建,并且能够更好地管理整个应用中的颜色:</p>    <pre>  <code class="language-objectivec">NORMAL   NIGHT    RED    #ffffff  #343434  #fafafa BG  #aaaaaa  #313131  #aaaaaa SEP  #0000ff  #ffffff  #fa0000 TINT  #000000  #ffffff  #000000 TEXT  #ffffff  #444444  #ffffff BAR  </code></pre>    <p>上面就是默认色表文件 DKColorTable.txt 中的内容,其中,第一行表示主题, NORMAL 主题必须存在,而且必须为第一列,而最右面的 BG 、 SEP 就是对应 DKColorPicker 的 key。</p>    <pre>  <code class="language-objectivec">self.tableView.dk_backgroundColorPicker =  DKColorPickerWithKey(BG);    </code></pre>    <p>在使用时,上面的代码就相当于返回了一个在 NORMAL 时返回 #ffffff 、 NIGHT 时返回 #343434 以及 RED 时返回 #fafafa 的 DKColorPicker 。</p>    <h3>pickerify</h3>    <p>虽然说,我们使用色表以及 DKColorPicker 解决了,但是,到目前为止我们还没有解决第三方框架的问题。</p>    <p>比如我们使用了某个第三方框架,或者自己添加了某个 color 属性,比如说:</p>    <pre>  <code class="language-objectivec">@interface DKView ()    @property (nonatomic, strong) UIColor *weirdColor;    @end  </code></pre>    <p>weirdColor 并没有对应的 DKColorPicker ,但是,我们可以通过 pickerify 在想要使用 dk_weirdColorPicker 的地方生成这个对应的 picker:</p>    <pre>  <code class="language-objectivec">@pickerify(DKView, weirdColor);  </code></pre>    <p>然后,我们就可以使用 dk_weirdColorPicker 属性了:</p>    <pre>  <code class="language-objectivec">view.dk_weirdColorPicker = DKColorPickerWithKey(BG);    </code></pre>    <p>pickerify 其实是一个宏:</p>    <pre>  <code class="language-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  </code></pre>    <p>这个宏根据传入的类和属性名,为我们生成了对应 picker 的存取方法,它也可以说是一种元编程的手段。</p>    <p>这里生成的 setter 方法不是标准意义上的驼峰命名法 dk_setweirdColorPicker: ,因为我不知道怎么才能让大写首字母之后的属性添加到这里(如果各位读者有解决方案,欢迎提 PR 或者 issue)。</p>    <h2>嵌入式 Ruby</h2>    <p>由于框架中很多的代码,都是重复的,所以在这里使用了 <strong>嵌入式 Ruby 模板</strong> 来生成对应的文件 color.m.irb :</p>    <pre>  <code class="language-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  </code></pre>    <p>这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的 generator 文件夹,其中包含了代码生成器的全部代码。</p>    <h2>小结</h2>    <p>如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的 <a href="/misc/goto?guid=4958873479023124863" rel="nofollow,noindex">README</a> 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题, AFNetworking 、 BlocksKit 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。</p>    <p>来自: http://draveness.me/cheng-shou-de-ye-jian-mo-shi-jie-jue-fang-an/</p>