iOS關於換膚和夜間模式的一些思考

介紹

  • 很久沒寫文章了,正好最近在研究換膚,因此將最近的心得和體會與你們分享一下。git

  • iOS換膚的方式比較單一,查找了不少資料,發現主流的方式有以下兩種:github

    • 方式一:經過給 Category 添加屬性的方式實現換膚,有一個 Manager 用以管理顏色和圖片,當主題改變時,經過發出通知告訴 UIKit 中的相關類,該改變視圖顏色了,這時視圖就會根據 Manager 中提供的不一樣主題的顏色來改變本身的顏色。objective-c

      • 這種方案的優勢在於:總體思路比較簡單明瞭,實現起來也不困難。
      • 缺點在於:
        • 對於每種控件,都已經將顏色固定死,沒有辦法設置好比同一個父視圖的兩個子視圖不一樣的顏色顯示。
        • 當咱們的項目已經完成了,並且項目體積也比較大時,這種方式的缺點就暴露的很是明顯了:更改界面十分麻煩,由於咱們的界面比較多時,須要給每一個界面的每一個控件都添加在 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 的背景圖片。windows

    • 這種方式的原理在於:使用 UI_APPEARANCE_SELECTOR 標記的方式會將當前對 UI 設置的外觀保存起來,等到視圖在添加到 window 以前會調用這個以前保存的外觀,更新視圖外觀。因此並非 UIKit 中全部的類的全部屬性均可以使用這個方法來設置 UI,只有當屬性上有標誌 UI_APPEARANCE_SELECTOR 才能夠用這個方法來設置。app

      • 這種方式的優勢是能夠十分便捷的設置一些全局的系統控件的外觀。字體

      • 可是缺點也十分明顯:ui

        • 當咱們想要區分同一個父視圖上方的子視圖時,這種方案就會十分的不方便,與第一種方法同樣,很難達到個性化定製的目的。
        • 而且當咱們想要設置 UILabel 等控件在不一樣視圖上的字體顏色等時,常常會失效,經過查看系統 API,能夠發現 UILabel 的 setTextColor: 等方法並無 UI_APPEARANCE_SELECTOR 標誌位,因此這也是這個換膚方式並非萬能的緣由。Stack Overflow有一篇關於 UILabel 設置顏色失效的緣由,他們說這是蘋果系統的一個 bug。而解決這個問題的方法也比較簡單,只要咱們重寫 setTextColor: 方法,給它加上一個 UI_APPEARANCE_SELECTOR 標誌位,那麼就能夠給它定製顏色。可是這種方式的缺點也十分明顯,對代碼的改動並無任何減小。反而當有不少控件都不能正確顯示顏色時,還須要增長很大的工做量。
      • 總結:我認爲這種設置 UIAppearance 的方式仍是比較適用於當全局的顏色已經固定時,設置主題,好比 UINavigationBar 和 UITabbar 這種控件,就比較適合使用這種方式來進行操做。當咱們的換膚比較簡單,不涉及相似夜間模式這種須要幾乎把全部的控件顏色都改變時,我以爲也可使用這種方法來進行換膚操做。atom

      • 另外:這個方法須要注意的一個點是,當咱們改變主題顏色時,須要先將控件從 window 上移除,再從新添加纔會觸發這種方式。spa

        - (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 的設置。3d

    • 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 的這種方式下,當在顏色匹配表中找不到對應字段時,會直接使用原來的顏色進行設置,感受也沒有什麼特別大的問題。
      • 這種方式的優點在於:
        • 能夠儘量減小對原項目的改動
        • 而且能夠實現對不一樣要求的控件進行個性化定製。基本上完成了對一開始提出的問題的解決。

總結

  • 這種方案仍是一種比較不成熟的方案,沒有通過真正項目的認證,當項目比較大時,這種方案可能仍是不可以很好的解決問題。不過這也是一次新的嘗試。之後我會就這方面繼續進行修改和嘗試。也歡迎有想法的你們來與我進行討論,但願能不吝賜教!
  • 項目的代碼在:這個地址
相關文章
相關標籤/搜索