iOS关于换肤和夜间模式的一些思考

news/2024/7/8 7:47:50

介绍

  • 好久没写文章了,正好最近在研究换肤,所以将最近的心得和体会与大家分享一下。

  • iOS换肤的方式比较单一,查找了很多资料,发现主流的方式有如下两种:

    • 方式一:通过给 Category 添加属性的方式实现换肤,有一个 Manager 用以管理颜色和图片,当主题改变时,通过发出通知告诉 UIKit 中的相关类,该改变视图颜色了,这时视图就会根据 Manager 中提供的不同主题的颜色来改变自己的颜色。

      • 这种方案的优点在于:整体思路比较简单明了,实现起来也不困难。
      • 缺点在于:
        • 对于每种控件,都已经将颜色固定死,没有办法设置比如同一个父视图的两个子视图不同的颜色显示。
        • 当我们的项目已经完成了,而且项目体积也比较大时,这种方式的缺点就暴露的非常明显了:更改界面十分麻烦,因为我们的界面比较多时,需要给每个界面的每个控件都添加在 Category 中增加的属性, 这种方式工作量巨大。
    • 方式二:使用系统提供的 UIAppearance 来更改主题,这种方式的优点在于,系统提供了非常简单方便的 API 供我们使用,最常用的就是 + (instancetype)appearance; 方法和 + (instancetype)appearanceWhenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, …; 这两个方法。具体用法如下: [[UINavigationBar appearance] setBarTintColor:myNavBarBackgroundColor]; 可以设置全局的 UINavigationBar 的 barTintColor。而 [[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], nil] setBackgroundImage:myNavBarButtonBackgroundImage forState:state barMetrics:metrics]; 表示在指定视图中设置 color,在此示例中是设置 UINavigationBar 上的 UIBarButtonItem 的背景图片。

    • 这种方式的原理在于:使用 UI_APPEARANCE_SELECTOR 标记的方式会将当前对 UI 设置的外观保存起来,等到视图在添加到 window 之前会调用这个之前保存的外观,更新视图外观。所以并不是 UIKit 中所有的类的所有属性都可以使用这个方法来设置 UI,只有当属性上有标志 UI_APPEARANCE_SELECTOR 才可以用这个方法来设置。

      • 这种方式的优点是可以十分便捷的设置一些全局的系统控件的外观。

      • 但是缺点也十分明显:

        • 当我们想要区分同一个父视图上方的子视图时,这种方案就会十分的不方便,与第一种方法一样,很难达到个性化定制的目的。
        • 并且当我们想要设置 UILabel 等控件在不同视图上的字体颜色等时,经常会失效,通过查看系统 API,可以发现 UILabel 的 setTextColor: 等方法并没有 UI_APPEARANCE_SELECTOR 标志位,所以这也是这个换肤方式并不是万能的原因。Stack Overflow有一篇关于 UILabel 设置颜色失效的原因,他们说这是苹果系统的一个 bug。而解决这个问题的方法也比较简单,只要我们重写 setTextColor: 方法,给它加上一个 UI_APPEARANCE_SELECTOR 标志位,那么就可以给它定制颜色。但是这种方式的缺点也十分明显,对代码的改动并没有任何减少。反而当有很多控件都不能正确显示颜色时,还需要增加很大的工作量。
      • 总结:我认为这种设置 UIAppearance 的方式还是比较适用于当全局的颜色已经固定时,设置主题,比如 UINavigationBar 和 UITabbar 这种控件,就比较适合使用这种方式来进行操作。当我们的换肤比较简单,不涉及类似夜间模式这种需要几乎把所有的控件颜色都改变时,我觉得也可以使用这种方法来进行换肤操作。

      • 另外:这个方法需要注意的一个点是,当我们改变主题颜色时,需要先将控件从 window 上移除,再重新添加才会触发这种方式。

        - (void)p_updateSystemWindow {
            NSArray *windowArray = [UIApplication sharedApplication].windows;
            for (UIWindow *window in windowArray) {
                for (UIView *subView in window.subviews) {
                    [subView removeFromSuperview];
                    [window addSubview:subView];
                }
            }
        }
        复制代码

自己的想法

  • 首先我们应该明确需求背景:
    • 最基本的就是:能够实现换肤
    • 项目已经完成,并且项目比较复杂不适合一个控制器一个控制器的去修改
    • 能够实现控件的个性化颜色定制,而并不是所有的一类控件都是同种颜色
  • 产生的问题:
    • 是否可以结合上述两种方式,产生自己的方式来进行简便的换肤?
    • 如何做到尽量少改动代码,就能实现换肤的效果?
    • 如何实现控件的个性化颜色定制?
  • 如何解决:
    • 既然整个项目都已经完成,那么如果我想尽量少改动代码,是否可以使用 methodSwizzling 的方式来 hook 系统的 setXXXColor: 方法实现不需要或尽量少对原项目代码进行改动。
    • 既然需要对控件进行个性化定制,是否可以使用 tag 的方式,对需要个性化的控件添加 tag 从而根据不同的 tag 来使用不同的颜色,而不需要个性化的颜色保持原本状态不进行修改。

我的实践

  • 首先需要提供一个 Manager 来进行主题的控制,在我的项目中,它叫做 LYThemeManager , 这个 Manager 的作用是控制切换不同的主题,当主题进行改变时,可以发出通知,告知 UI 控件该改变自己的颜色了。并且它所提供的 (UIColor *)colorWithReceiver:(id)receiver selString:(NSString *)selector;(UIColor *)colorWithReceiver:(id)receiver withTag:(NSInteger)tag selString:(NSString *)selector; 分别是实现全局控件 UI 的设置以及 个性化控件 UI 的设置。

    • LYThemeManager 内部有两个字典,分别是读取不同的 plist , colorInfoDic 用于读取全局 UI 的颜色设置,而 specialColorInfoDic 用于读取个性化控件的颜色设置,具体的 plist 中的内容如下:

      在 specialPlist 中前面的数字表示 tag 值,后面表示设置的属性意义。

    • 以 UIView 的 category 为例,首先在这个类中,使用了 methodSwizzle 来实现 hook 系统方法,在这里我 hook 了系统的 setBackgroundColor: 方法和 setTintColor: 方法。

      + (void)load {
          [self swizzleViewColor];
      }
      
      #pragma mark - MethodSwizzling
      
      + (void)swizzleViewColor {
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
              [LYMethodSwizzleUtils swizzleInstanceMethodWithClass:[self class] OriginMethod:@selector(setBackgroundColor:) swappedMethod:@selector(ly_setBackgroundColor:)];
              [LYMethodSwizzleUtils swizzleInstanceMethodWithClass:[self class] OriginMethod:@selector(setTintColor:) swappedMethod:@selector(ly_setTintColor:)];
          });
      }
      复制代码
    • setBackgroundColor: 方法为例:

      - (void)ly_setBackgroundColor:(UIColor *)color {
          
      // 利用 selector 来选方法,注意子类和父类不要使用同名方法,否则会导致符号混乱产生循环引用。
          UIColor *bgColor = [[LYThemeManager shareManager] colorWithReceiver:self withTag:self.tag selString:[NSString stringWithFormat:@"%ld:viewBackgroundColor", self.tag]];
          if (bgColor) {
              [self.pickers setObject:bgColor forKey:@"setBackgroundColor:"];
              [self ly_setBackgroundColor:bgColor];
          } else {
              [self ly_setBackgroundColor:color];
          }
      }
      复制代码

      在这里为什么我要使用个性化颜色设置的方法:(UIColor *)colorWithReceiver:(id)receiver withTag:(NSInteger)tag selString:(NSString *)selector; ,这是因为几乎所有 UIKit 中的控件都继承自 UIView,当我们直接将所有的 setBackgroundColor: 方法都设置为同一颜色时,达到的效果是灾难性的所有控件都是同一颜色。无法进行区分。所以这里使用个性化的,只对 controller 中的 view 改变颜色。

    • 添加了一个字典属性 pickers, 这个属性用以将我们 hook 的方法添加进来,它的 key 是方法名, value是它应该被设置的 color,当收到改变颜色的通知时,需要遍历这个属性中所有的数据,来实现颜色更新。

      @interface UIView ()
      @property (nonatomic, strong) NSMutableDictionary <NSString *, UIColor *> *pickers;
      @end
      
      #pragma mark - Add Property
      
      - (NSMutableDictionary<NSString *,UIColor *> *)pickers {
          NSMutableDictionary <NSString *, UIColor *> *pickers = objc_getAssociatedObject(self, @selector(pickers));
          if (!pickers) {
              pickers = @{}.mutableCopy;
              objc_setAssociatedObject(self, @selector(pickers), pickers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
              
              [[NSNotificationCenter defaultCenter] removeObserver:self];
              [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateTheme) name:LYThemeChangeNotification object:nil];
          }
          return pickers;
      }
      复制代码
    • 最后就是对通知的响应:

      #pragma mark - Response Notification
      
      - (void)updateTheme {
          
          [self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, UIColor * _Nonnull obj, BOOL * _Nonnull stop) {
              SEL selector = NSSelectorFromString(key);
              [UIView animateWithDuration:0.3 animations:^{
      #pragma clang diagnostic push
      #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                  [self performSelector:selector withObject:obj];
      #pragma clang diagnostic pop
              }];
          }];
      }
      复制代码
    • 由于几乎所有的 UIKit 中的控件都继承自 UIView,并且响应方式都同于 UIView ,所以在其他的 category 中省去了对属性 picker 的 Add Property 步骤以及对通知的响应。

    • 在 UILabel 中的 setTextColor: 方法也使用了个性化的设置,对于不需要特殊设置的 UILabel 的 textColor 则原本默认是什么颜色,就是什么颜色。

    • 所有的 tag 值,我都以宏定义的方式存储在 ThemeConfig.pch 中了,当需要个性定义的控件比较多时,通过 tag 管理也是一个缺点。

    • 整体上思路就是如此,这个方案只是一个初步方案,还有很多很多不足之处。

      • 缺点在于:
        • 比如说通过 tag 来管理颜色,实际上也会修改原项目的代码,因为我们需要设置不同控件的 tag 值。
        • hook 系统的方法或许会带来意想不到的bug。不过在我 hook 的这种方式下,当在颜色匹配表中找不到对应字段时,会直接使用原来的颜色进行设置,感觉也没有什么特别大的问题。
      • 这种方式的优势在于:
        • 可以尽可能减少对原项目的改动
        • 并且可以实现对不同要求的控件进行个性化定制。基本上完成了对一开始提出的问题的解决。

总结

  • 这种方案还是一种比较不成熟的方案,没有经过真正项目的认证,当项目比较大时,这种方案可能还是不能够很好的解决问题。不过这也是一次新的尝试。以后我会就这方面继续进行修改和尝试。也欢迎有想法的大家来与我进行讨论,希望能不吝赐教!
  • 项目的代码在:这个地址

http://www.niftyadmin.cn/n/4606601.html

相关文章

Qt在vs2010下的配置

首先不要使用中文目录&#xff0c; 1 下载Qt的安装包和VS2010的Qt插件 2. 安装Qt SDK 3. 安装Qt的VS开发插件 4. 编译Qt Qt默认使用mingw进行编译&#xff0c;如果要使用VS2010开发&#xff0c;需要将Qt重新编译。 进入开始菜单Microsoft Visual Studio 2010&#xff0c;Visual…

ItemDecoration解析(一) getItemOffsets

介绍An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapters data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.All ItemDecora…

The Linux I/O Stack Diagram

http://www.thomas-krenn.com/en/oss/linux-io-stack-diagram/linux-io-stack-diagram_v0.1.pdf

浅谈JavaScript的函数的call以及apply

我爱撸码&#xff0c;撸码使我感到快乐&#xff01;大家好&#xff0c;我是Counter。今天就来谈谈js函数的call以及apply&#xff0c;具体以代码举例来讲解吧&#xff0c;例如有函数&#xff1a; function func(a, b) {return a b;} 非常简单的一个函数&#xff0c;返回a b 的…

C语言实现密码登录界面,你可能已被盯上!

登录界面是一个网站最重要的部分之一&#xff0c;一个良好的登录界面设计&#xff0c;将会给用户一个良好的使用体验&#xff0c;甚至能够引导非注册用户注册。它不仅仅在界面设计中很重要&#xff0c;也关系着一个网站的用户体验。今天小编用C语言写了一个简单的密码登陆界面 …

我日常的VIM

相信每一vim本书都会介绍给初学者一个叫vimtutor的&#xff0c;可以花几十分钟时间跟着过一边。我总结一下最近看的 移动 只列我用的比较多的命令&#xff0c;所有的注释都用"(vim脚本注释符) 1 h "左2 j "下3 k "上4 l "右&…

OpenCV中关于cvGetCaptureProperty函数

OpenCV中关于cvGetCaptureProperty函数OpenCV中提供了一个函数cvGetCaptureProperty(Capture* cap,int property_index)函数来获取视频文件的一些属性&#xff0c;这是其中的OpenCV中属性的一些宏定义&#xff1a;#define CV_CAP_PROP_POS_MSEC 0#define CV_CAP_PROP_POS_FRAME…

马云:未来两三百年金融是“八二理论” 最大机会在互联网金融

10月16日&#xff0c;蚂蚁金服召开年会&#xff0c;这一天也是蚂蚁金服成立两周年。在10月16日的年会上&#xff0c;蚂蚁金服董事长兼CEO彭蕾将CEO的接力棒交给总裁井贤栋&#xff0c;未来将以蚂蚁金服董事长身份专注公司长期发展、全球化战略、人才培养和文化建设传承。 阿里巴…