iOS換膚功能的簡單處理框架

換膚功能是在APP開發過程當中遇到的比較多的場景,爲了提供更好的用戶體驗,許多APP會爲用戶提供切換主題的功能。主題顏色管理涉及到的的步驟有ios

  • 顏色配置
  • 使用顏色
  • UI元素動態變動的能力
  • 動態修改配置
  • 主題包管理
  • 如何實施
  • 優化

效果以下:
git

ezgif.com-optimize

DEMO代碼:gitee.com/dhar/iosdem…json

顏色配置

由於涉及到多種配置,因此以代碼的方式定義顏色實踐和維護的難度是比較高的,一種合適的方案是--顏色的配置是經過配置文件的形式進行導入的。配置文件會通過轉換步驟,最終造成代碼層級的配置,以全局的方式提供給各個模塊使用,這裏會涉及到一個顏色管理者的概念,通常地這回事一個單例對象,提供全局訪問的接口。同一個APP中在不一樣的模塊中保存不一樣的主題顏色配置,在不一樣的層級中也能夠存在不一樣的主題顏色配置,由於涉及到層級間的配置差別,因此顏色的配置須要引入一個等級的概念,通常地較高層級顏色的配置等級是高於較低層級的,存在相同的配置較高層級的配置會覆蓋較低層級的配置。bash

咱們採用的顏色配置的文件形以下面所示,爲何是在一個json文件的colorkey下面呢,是爲了考慮到將來的擴展性,若是不一樣的主題會涉及到一些尺寸值的差別化,咱們能夠添加dimensionskey進行擴展配置。優化

{
  "color": {
      "Black_A":"323232",
      "Black_AT":"323232",
      "Black_B":"888888",
      "Black_BT":"888888",

      "White_A":"ffffff",
      "White_AT":"ffffff",
      "White_AN":"ffffff",

      "Red_A":"ff87a0",
      "Red_AT":"ff87a0",
      "Red_B":"ff5073",
      "Red_BT":"ff5073",

      "Colour_A":"377ce4",
      "Colour_B":"6aaafa",
      "Colour_C":"ff8c55",
      "Colour_D":"ffa200",
      "Colour_E":"c4a27a",
  }
}
複製代碼

有了以上的配置,顏色配置的工做主要就是解析該配置文件,把配置保存在一個單例對象中便可,這部分主要的步驟以下:ui

  • 配置文件類表根據等級排序
  • 獲取每一個配置文件中的配置,進行保存
  • 通知外部主題顏色配置發生改變

對應的代碼以下,這裏有個須要注意的地方是,加載配置文件的時候使用了文件讀寫鎖進行讀寫的鎖定操做,防止讀髒數據的發生,直到配置文件加載完成,釋放讀寫鎖,這時讀進程能夠繼續。spa

- (void)loadConfigWithFileName:(NSString *)fileName level:(NSInteger)level {
    if (fileName.length == 0) {
        return;
    }
    
    pthread_rwlock_wrlock(&_rwlock);
    __block BOOL finded = NO;
    [self.configFileQueue enumerateObjectsUsingBlock:^(YTThemeConfigFile *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
        if ([obj.fileName isEqualToString:fileName]) {
            finded = YES;
            *stop = YES;
        }
    }];
    if (!finded) {
        // 新增配置文件
        YTThemeConfigFile *file = [[YTThemeConfigFile alloc] init];
        file.fileName = fileName;
        file.level = level;
        [self.configFileQueue addObject:file];
        // 優先級排序
        [self.configFileQueue sortUsingComparator:^NSComparisonResult(YTThemeConfigFile *_Nonnull obj1, YTThemeConfigFile *_Nonnull obj2) {
            if (obj1.level > obj2.level) {
                return NSOrderedDescending;
            }
            return NSOrderedAscending;
        }];
        [self setupConfigFilesContainDefault:YES];
    }
    pthread_rwlock_unlock(&_rwlock);
}

- (void)setupConfigFilesContainDefault:(BOOL)containDefault {
    NSMutableDictionary *defaultColorDict = nil, *currentColorDict = nil;
    
    // 加載默認配置
    if (containDefault) {
        defaultColorDict = [NSMutableDictionary dictionary];
        [self loadConfigDataWithColorMap:defaultColorDict valueMap:nil isDefault:YES];
        
        self.defaultColorMap = defaultColorDict;
    }
    
    // 加載主題配置
    if (_themePath.length > 0) {
        currentColorDict = [NSMutableDictionary dictionary];
        [self loadConfigDataWithColorMap:currentColorDict valueMap:nil isDefault:NO];
        
        self.currentColorMap = currentColorDict;
    }
    
    // 發送主體顏色變動通知
    [self notifyThemeDidChange];
}

- (void)notifyThemeDidChange {
    NSArray *allActionObjects = self.actionMap.objectEnumerator.allObjects;
    for (YTThemeAction *action in allActionObjects) {
        [action notifyThemeDidChange];
    }
}

- (void)loadConfigDataWithColorMap:(NSMutableDictionary *)colorMap valueMap:(NSMutableDictionary *)valueMap isDefault:(BOOL)isDefault {
    // 每一次新增一個配置文件,全部配置文件都得從新計算一次,這裏有不少重複多餘的工做
    [self.configFileQueue enumerateObjectsUsingBlock:^(YTThemeConfigFile *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
        NSDictionary *dict = nil;
        if (isDefault) {
            dict = obj.defaultDict;
        } else {
            dict = obj.currentDict;
        }
        if (dict.count > 0) {
            [self loadThemeColorTo:colorMap from:dict]; // 將全部配置表中的color字段的數據都放到colorMap中
        }
    }];
}

- (void)loadThemeColorTo:(NSMutableDictionary *)dictionary from:(NSDictionary *)from {
    NSDictionary<NSString *, NSString *> *colors = from[@"color"];
    [colors enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) {
        // 十六進制字符串轉爲UIColor
        UIColor *color = [UIColor yt_nullcolorWithHexString:obj];
        if (color) {
            [dictionary setObject:color forKey:key];
        } else {
            [dictionary setObject:obj forKey:key];
        }
    }];
}
複製代碼

管理者處理處理配置以外,還須要暴露外部接口給客戶端使用,以用於獲取不一樣主題下對應的顏色色值、圖片資源、尺寸信息等和主題相關的信息。好比咱們會提供一個colorForKey方法獲取不一樣主題下的同一個key對應的顏色色值,獲取色值的大體步驟以下:.net

  • 從當前的主題配置中獲取
  • 從默認的主題配置中獲取
  • 從預留的主題配置中獲取
  • 若是重定向的配置,遞歸處理
  • 以上步驟都完成還未找到返回默認黑色

這裏使用了讀寫鎖的寫鎖,若是同時有寫操做獲取了該鎖,讀取進程會阻塞直到寫操做的完成釋放鎖。code

/** 獲取顏色值 */
- (UIColor *)colorForKey:(NSString *)key {
    pthread_rwlock_rdlock(&_rwlock);
    UIColor *color = [self colorForKey:key isReserveKey:NO redirectCount:0];
    pthread_rwlock_unlock(&_rwlock);
    return color;
}

- (UIColor *)colorForKey:(NSString *)key isReserveKey:(BOOL)isReserveKey redirectCount:(NSInteger)redirectCount {
    if (key == nil) {
        return nil;
    }
    
    ///正常獲取色值
    id colorObj = [_currentColorMap objectForKey:key];
    if (colorObj == nil) {
        colorObj = [_defaultColorMap objectForKey:key];
    }
    
    if (isReserveKey && colorObj == nil) {
        return nil;
    }
    
    ///看看是否有替補key
    if (colorObj == nil) {
        NSString *reserveKey = [_reserveKeyMap objectForKey:key];
        if (reserveKey) {
            colorObj = [self colorForKey:reserveKey isReserveKey:YES redirectCount:redirectCount];
        }
    }
    
    ///查看當前key 可否轉成 color
    if (colorObj == nil) {
        colorObj = [UIColor yt_colorWithHexString:key];
    }
    
    if ([colorObj isKindOfClass:[UIColor class]]) {
        ///若是是 重定向 或者 替補 key 的color 要設置到 當前 colorDict 裏面
        // 重定向的配置形如:"Red_A":"Red_B",
        if (redirectCount > 0 || isReserveKey) {
            [_currentColorMap ?: _defaultColorMap setObject:colorObj forKey:key];
        }
        return colorObj;
    } else {
        if (redirectCount < 3) { // 重定向遞歸
            return [self colorForKey:colorObj isReserveKey:NO redirectCount:redirectCount + 1];
        } else {
            return [UIColor blackColor];
        }
    }
}
複製代碼

使用顏色

顏色的使用也是經由管理者的,爲了方便,定義一個顏色宏提供給客戶端使用cdn

#define YTThemeColor(key) ([[YTThemeManager sharedInstance] colorForKey:key])
複製代碼

客戶端使用的代碼以下:

UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, 200, 40)];
label.text = @"Text";
label.textColor = YTThemeColor(kCK_Red_A);
label.backgroundColor = YTThemeColor(kCK_Black_H);
[self.view addSubview:label];
複製代碼

另外,由於顏色配置的key爲字符串類型,直接使用字符串常量並非個好辦法,因此把對應的字符串轉換爲宏定義是一個相對好的辦法。第一個是方便使用,可使用代碼提示;第二個是不容易出錯,特別是長的字符串;第三個也會必定程度上的提升效率。

YTColorDefine類的宏定義

// .h 中的聲明
///Black
FOUNDATION_EXTERN NSString *kCK_Black_A;
FOUNDATION_EXTERN NSString *kCK_Black_AT;
FOUNDATION_EXTERN NSString *kCK_Black_B;
FOUNDATION_EXTERN NSString *kCK_Black_BT;

// .m 中的定義
NSString *kCK_Black_A = @"Black_A";
NSString *kCK_Black_AT = @"Black_AT";
NSString *kCK_Black_B = @"Black_B";
NSString *kCK_Black_BT = @"Black_BT";
複製代碼

主題包管理

在實際的落地項目中,主題包管理涉及到的事項包括主題包下載和解壓動態加載主題包等內容,最後的一步是更換主題配置文件所在的配置路徑,爲了演示的方便,咱們會把不一樣主題的資源放置在bundle中某一個特定的文件夾下,經過切換管理者中的主題路徑配置來達到切換主題的效果,和動態下載更換主題的步驟是同樣的。

管理者提供一個設置主題配置的配置路徑的方法,在該方法中改變配置路徑的同時,從新加載配置便可,代碼以下

/**
 設置主題文件的路徑
 @param themePath 文件的路徑
 */
- (void)setupThemePath:(NSString *)themePath {
    pthread_rwlock_wrlock(&_rwlock);
    
    _themePath = [themePath copy];
    
    self.currentColorMap = nil;
    
    if ([_themePath.lowercaseString isEqualToString:[[NSBundle mainBundle] resourcePath].lowercaseString]) {
        _themePath = nil;
    }
    
    self.currentThemePath = _themePath;
    
    for (int i = 0; i < self.configFileQueue.count; i++) {
        YTThemeConfigFile *obj = [self.configFileQueue objectAtIndex:i];
        [obj resetCurrentDict];
    }
    [self setupConfigFilesContainDefault:NO];
    
    pthread_rwlock_unlock(&_rwlock);
}
複製代碼

如何實施

以上的流程涉及到的只是iOS平臺下的一個技術解決方案,真實的實踐過程當中會涉及到安卓平臺、Web頁面、UI出圖的標註,這些是要進行統一處理的,才能在各個端上有一致的體驗。第一步就是制定合理的顏色規範,把規範同步給各個端的利益相關人員;第二部是UI出圖顏色是規範的顏色定義值,而不是好比#ffffff這樣的顏色,須要是好比White_A這樣規範的顏色定義值,這樣客戶端處理使用的就是White_A這個值,不用管在不一樣主題下不一樣的顏色表現形式。

優化

loadConfigDataWithColorMap方法調用的優化

若是模塊不少,每一個模塊都會調用loadConfigWithFileName加載配置文件,那麼loadConfigDataWithColorMap方法處理文件的時間複雜度是O(N*N),會重複處理不少多餘的工做,理想的作法是底層保存一份公有的顏色配置,而後在APP層加載一份定製化的配置,在模塊中不用再加載主題配置文件,這樣會提升效率。

參考資料

讀寫鎖pthread_rwlock_t的使用

相關文章
相關標籤/搜索