iOS 實現快速切換主題詳細教程(附上源碼)| 掘金技術徵文

前言

iOS 實現主題切換,相信在將來的app裏也是會頻繁出現的,儘管如今只是出如今主流的APP,如(QQ、新浪微博、酷狗音樂、網易雲音樂等),可是如今是看顏值、追求個性的年代,因此根據用戶喜愛自定義/切換主題也是將來app的必備功能了。git

實現思路

爲了下降耦合度,決定採用的方案是使用NSObject的分類來實現主題設置,有些讀者可能會想爲什麼不使用UIView的分類而是使用NSObject的分類?建議這部分讀者看一下UIBarItem父類,而後仔細思考一下,就會理解了。github

設置主題色

PYThemeColor.png

  1. 建立主題色池
  2. 將須要設置主題色的控件及其對應屬性/方法添加到主題色池中
  3. 調用設置主題色方法時,遍歷主題色池中的控件,使用KVC設置對應屬性或調用對應的方法來實現主題色的設置

代碼實現

建議讀者在理解思路之後先下載源碼大概看一下(縱觀全局)再閱讀如下內容:
源碼地址:github.com/iphone5solo…數組

1. 建立主題色池

因爲是在NSObject的分類裏面建立,爲了方便管理,設置全局變量_themeColorPool,並經過懶加載完成_themeColorPool的實例化。數組中的對象原來採用的是NSDictionary,可是因爲NSDictionary存儲時,會對對象採用強引用致使對象不能被及時釋放,因此最終採用的解決方案是採用NSMapTable存儲,實現對象的弱引用,詳情見下一步就會理解了app

/** 主題顏色池 */
static NSMutableArray<NSMapTable *> *_themeColorPool;

#pragma mark - 懶加載
- (NSMutableArray *)themeColorPool
{
    if (!_themeColorPool) {
        _themeColorPool = [NSMutableArray array];
    }
    return _themeColorPool;
}複製代碼
2. 添加控件到主題色池中

因爲顏色設置有的能夠直接經過屬性設置也有的須要經過調用方法纔可設置。以UIButton爲例,設置背景色可經過屬性button.backgroundColor設置,設置選中狀態時的字體顏色則要調用setTitleColor:forState:方法纔可設置,因而,就得提供兩個方法供使用者調用,以下iphone

/** * 添加到主題色池 * selector : 執行方法 * objects : 方法參數數組 * 注意:方法參數必須按順序一一對應,若是涉及到的主題色設置使用 PYTHEME_THEME_COLOR 宏定義代替 * 若是數組中某個參數爲nil,需包裝爲 [NSNull null] 對象再添加到數組中 */
- (void)py_addToThemeColorPoolWithSelector:(SEL)selector objects:(NSArray<id> *)objects;
/** * 添加到主題色池 * propertyName : 屬性名 */
- (void)py_addToThemeColorPool:(NSString *)propertyName;複製代碼

實現以下:post

#pragma mark - Theme Color
/** * 添加到主題色池 * selector : 執行方法 * objects : 方法參數數組 * 注意:方法參數必須按順序一一對應,若是涉及到的主題色設置使用 PYTHEME_THEME_COLOR 宏定義代替 * 若是數組中某個參數爲nil,需包裝爲 [NSNull null] 對象再添加到數組中 */
- (void)py_addToThemeColorPoolWithSelector:(SEL)selector objects:(NSArray *)objects
{
    // 判斷參數是否爲空
    if (!objects) return;
    Class appearanceClass = NSClassFromString(@"_UIAppearance");
    // 若是對象爲_UIAppearance,直接返回
    if ([self isMemberOfClass:appearanceClass]) return;
    // 鍵:對象地址+方法名 值:對象
    NSString *pointSelectorString = [NSString stringWithFormat:@"%p%@", self, NSStringFromSelector(selector)];
    // 採用NSMapTable存儲對象,使用弱引用
    NSMapTable *mapTable = [NSMapTable mapTableWithKeyOptions:NSMapTableCopyIn valueOptions:NSMapTableWeakMemory];
    [mapTable setObject:self forKey:pointSelectorString];
    [mapTable setObject:objects forKey:PYTHEME_COLOR_ARGS_KEY];
    // 判斷是否已經在主題色池中
    for (NSMapTable *subMapTable in [[self themeColorPool] copy]) {
        if ([[subMapTable description] isEqualToString:[mapTable description]]) { // 存在,直接返回
            return;
        }
    }
    // 不存在,添加主題色池中
    [[self themeColorPool] addObject:mapTable];
    if (_currentThemeColor) { // 已經設置主題色,直接設置
        [self py_performSelector:selector withObjects:objects];
    }
}

/** * 添加到主題色池 * propertyName : 屬性名 */
- (void)py_addToThemeColorPool:(NSString *)propertyName
{
    // 若是對象爲_UIAppearance,直接返回
    Class appearanceClass = NSClassFromString(@"_UIAppearance");
    if ([self isMemberOfClass:appearanceClass]) return;
    // 鍵:對象地址+屬性名 值:對象
    NSString *pointString = [NSString stringWithFormat:@"%p%@", self, propertyName];
    // 採用NSMapTable存儲對象,使用弱引用
    NSMapTable *mapTable = [NSMapTable mapTableWithKeyOptions:NSMapTableCopyIn valueOptions:NSMapTableWeakMemory];
    [mapTable setObject:self forKey:pointString];
    // 判斷是否已經在主題色池中
    for (NSMapTable *subMapTable in [[self themeColorPool] copy]) {
        if ([[subMapTable description] isEqualToString:[mapTable description]]) { // 存在,直接返回
            return;
        }
    }
    // 不存在,添加主題色池中
    [[self themeColorPool] addObject:mapTable];
    if (_currentThemeColor) { // 已經設置主題色,直接設置
        [self setValue:_currentThemeColor forKey:propertyName];
    }
}複製代碼

爲了知足個別需求,因此仍是提供一下從主題色池中移除控件的方法學習

/** * 從主題色池移除 * selector : 方法選擇器 */
- (void)py_removeFromThemeColorPoolWithSelector:(SEL)selector;

/** * 從主題色池移除 * propertyName : 屬性名 */
- (void)py_removeFromThemeColorPool:(NSString *)propertyName;複製代碼

實現以下:字體

/** * 從主題色池移除 * selector : 執行方法 */
- (void)py_removeFromThemeColorPoolWithSelector:(SEL)selector
{
    // 若是對象爲_UIAppearance,直接返回
    Class appearanceClass = NSClassFromString(@"_UIAppearance");
    if ([self isMemberOfClass:appearanceClass]) return;

    // 鍵:對象地址+方法名 值:對象
    NSString *pointSelectorString = [NSString stringWithFormat:@"%p%@", self, NSStringFromSelector(selector)];
    // 判斷是否已經在主題色池中
    for (NSMapTable *subMapTable in [[self themeColorPool] copy]) {
        // 取出key
        NSString *objectKey = nil;
        // 獲取mapTable中全部key
        NSEnumerator *enumerator = [subMapTable keyEnumerator];
        NSString *key;
        while (key = [enumerator nextObject]) {
            if (![key isEqualToString:PYTHEME_COLOR_ARGS_KEY]) {
                objectKey = key;
                break;
            }
        }
        if([objectKey isEqualToString:pointSelectorString]) { // 存在,移除
            [[self themeColorPool] removeObject:subMapTable];
            return;
        }
    }
}

/** * 從主題色池移除 * propertyName : 屬性名 */
- (void)py_removeFromThemeColorPool:(NSString *)propertyName
{
    // 若是對象爲_UIAppearance,直接返回
    Class appearanceClass = NSClassFromString(@"_UIAppearance");
    if ([self isMemberOfClass:appearanceClass]) return;

    // 鍵:對象地址+屬性名 值:對象
    NSString *pointString = [NSString stringWithFormat:@"%p%@", self, propertyName];
    // 判斷是否已經在主題色池中
    for (NSMapTable *subMapTable in [[self themeColorPool] copy]) {
        // 獲取mapTable中全部key
        NSEnumerator *enumerator = [subMapTable keyEnumerator];
        if([[enumerator nextObject] isEqualToString:pointString]) { // 存在,移除
            [[self themeColorPool] removeObject:subMapTable];
            return;
        }
    }
}複製代碼
3. 設置主題色
/** * 設置主題色 * color : 主題色 */
- (void)py_setThemeColor:(UIColor *)color;複製代碼

實現以下:ui

/** * 設置主題色 * color : 主題色 */
- (void)py_setThemeColor:(UIColor *)color
{
    _currentThemeColor = color;
    // 遍歷緩主題池,設置統一主題色
    for (NSMapTable *mapTable in [_themeColorPool copy]) {
        // 取出key
        NSString *objectKey = nil;
        // 獲取mapTable中全部key
        NSEnumerator *enumerator = [mapTable keyEnumerator];
        NSString *key;
        while (key = [enumerator nextObject]) {
            if (![key isEqualToString:PYTHEME_COLOR_ARGS_KEY]) {
                objectKey = key;
                break;
            }
        }
        if (!key) { // 若是key爲空,則mapTable 爲空,移除mapTable
            [_themeColorPool removeObject:mapTable];
        }
        // 取出對象
        id object = [mapTable objectForKey:objectKey];
        if ([objectKey containsString:@":"]) { // 方法
            // 取出參數
            NSArray *args = [mapTable objectForKey:PYTHEME_COLOR_ARGS_KEY];
            // 取出方法
            NSString *selectorName = [objectKey substringFromIndex:[[NSString stringWithFormat:@"%p", object] length]];
            SEL selector = NSSelectorFromString(selectorName);
            // 調用方法,設置屬性
            [object py_performSelector:selector withObjects:args];
        } else { // 成員屬性
            // 取出屬性值
            NSString *propertyName = [objectKey substringFromIndex:[[NSString stringWithFormat:@"%p", object] length]];
            // 給對象的對應屬性賦值(使用KVC)
            [object setValue:color forKeyPath:propertyName];
        }
    }
}複製代碼

使用

假設有個需求:UINavigationBar背景顏色UIButton選中時的字體顏色會隨着主題顏色的變化而變化,實現以下:spa

navigationBarbackgroundUIButtonsetTitleColor:forState:方法添加到主題池中,方法參數中若是是設置爲主題色的參數則用PYTHEME_THEME_COLOR佔位,若是參數爲nil,則使用[NSNull null]代替

// 建立導航欄
UINavigationBar *navigationBar = [[UINavigationBar alloc] init];
// 添加到主題色池中
[navigationBar py_addToThemeColorPool:@"barTintColor"];

// 建立按鈕
UIButton *button = [[UIButton alloc] init];
// 添加到主題色中
[button py_addToThemeColorPoolWithSelector:@selector(setTitleColor:forState:) objects:@[PYTHEME_THEME_COLOR, @(UIControlStateSelected)]];複製代碼

設置主題色

// 設置主題色爲紅色
[self py_setThemeColor:[UIColor redColor]];複製代碼

這裏有一點注意的是[object py_performSelector:selector withObjects:args];這是本身實現的performSelector 多參調用關於這方面的網上已經有不少教程了,這裏就很少介紹了。直接附上的我實現(內部方法,主要考慮到本身的使用):

#pragma mark - performSelector 多參調用
- (id)py_performSelector:(SEL)selector withObjects:(const NSArray<id> *)objects
{
    // 1. 建立方法簽名
    // 根據方法來初始化NSMethodSignature
    NSMethodSignature *methodSignate = [[self class] instanceMethodSignatureForSelector:selector];
    if (!methodSignate) { // 沒有該方法
        return self;
    }
    // 2. 建立invocation對象(包裝方法)
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignate];
    // 3. 設置相關屬性
    // 調用者
    invocation.target = self;
    // 調用方法
    invocation.selector = selector;
    // 獲取除self、_cmd的參數個數
    NSInteger paramsCount = methodSignate.numberOfArguments - 2;
    // 取最少的,防止越界
    NSInteger count = MIN(paramsCount, objects.count);
    // 用於dictionary的拷貝(用於保住objCopy,避免非法內存訪問)
    NSMutableDictionary *objCopy = nil;
    // 設置參數
    for (int i = 0; i < count; i++) {
        // 取出參數對象
        id obj = objects[i];
        // 若是是主題顏色參數顏色,則設置
        if ([obj isKindOfClass:[NSString class]] && [obj isEqualToString:PYTHEME_THEME_COLOR]) {
            obj = _currentThemeColor;
        }
        // 判斷須要設置的參數是不是NSNull, 若是是就設置爲nil
        if ([obj isKindOfClass:[NSNull class]]) {
            obj = nil;
        }
        // 獲取參數類型
        const char *argumentType = [methodSignate getArgumentTypeAtIndex:i + 2];
        // 判斷參數類型 根據類型轉化數據類型(若是有必要)
        NSString *argumentTypeString = [NSString stringWithUTF8String:argumentType];
        if ([argumentTypeString isEqualToString:@"@"]) { // id
            // 若是是dictionary,可能存在 PYTHEME_THEME_COLOR
            if ([obj isKindOfClass:[NSDictionary class]]) { // NSDictionary
                objCopy = [obj mutableCopy];
                // 取出全部鍵
                NSArray *keys = [objCopy allKeys];
                for (NSString *key in keys) {
                    // 取出值
                    id value = objCopy[key];
                    if ([value isKindOfClass:[NSString class]] && [value isEqualToString:PYTHEME_THEME_COLOR]) {
                        // 替換成顏色
                        [objCopy setValue:_currentThemeColor forKey:key];
                    }
                }
                [invocation setArgument:&objCopy atIndex:i + 2];
            } else { // 其餘
                [invocation setArgument:&obj atIndex:i + 2];
            }
        }  else if ([argumentTypeString isEqualToString:@"B"]) { // bool
            bool objVaule = [obj boolValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"f"]) { // float
            float objVaule = [obj floatValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"d"]) { // double
            double objVaule = [obj doubleValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"c"]) { // char
            char objVaule = [obj charValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"i"]) { // int
            int objVaule = [obj intValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"I"]) { // unsigned int
            unsigned int objVaule = [obj unsignedIntValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"S"]) { // unsigned short
            unsigned short objVaule = [obj unsignedShortValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"L"]) { // unsigned long
            unsigned long objVaule = [obj unsignedLongValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"s"]) { // shrot
            short objVaule = [obj shortValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"l"]) { // long
            long objVaule = [obj longValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"q"]) { // long long
            long long objVaule = [obj longLongValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"C"]) { // unsigned char
            unsigned char objVaule = [obj unsignedCharValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"Q"]) { // unsigned long long
            unsigned long long objVaule = [obj unsignedLongLongValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"{CGRect={CGPoint=dd}{CGSize=dd}}"]) { // CGRect
            CGRect objVaule = [obj CGRectValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        } else if ([argumentTypeString isEqualToString:@"{UIEdgeInsets=dddd}"]) { // UIEdgeInsets
            UIEdgeInsets objVaule = [obj UIEdgeInsetsValue];
            [invocation setArgument:&objVaule atIndex:i + 2];
        }
    }
    // 4.調用方法
    [invocation invoke];
    // 5. 設置返回值
    id returnValue = nil;
    if (methodSignate.methodReturnLength != 0) { // 有返回值
        // 將返回值賦值給returnValue
        [invocation getReturnValue:&returnValue];
    }
    return returnValue;
}複製代碼

細節處理

1. 設置主題色的方式
  • 經過屬性直接設置主題色
  • 經過調用方法並以主題色爲參數來設置主題色
  • 經過調用方法但主題色被封裝後(如:NSDictionary)做爲參數設置主題色
2. 自動管理內存管理

當對象應該被釋放後,下一次當主題色池有新元素添加時,會遍歷主題色池,根據對象的引用計數來決定是否移除對象(實現自動管理內存),所以:主題色池中最多可能會殘留一個對象,這對內存幾乎沒有任何影響,若是要及時釋放對象本人認爲能夠採用KVO監聽對象的引用計數(何嘗試),可是耗能高,不建議這麼作!

3. 當對象爲_UIAppearance類時,不添加到主題色池

瞭解UIAppearance的讀者應該能夠理解,並且使用UIAppearance的目的也爲爲了設置全局色,因此爲了不衝突,若是使用了該「技術」就不添加到主題色池

設置主題圖片

觀察了新浪微博、酷狗音樂等app,發現設置主題圖片仍是頗有必要的,並且發現每套主題皮膚/圖片都有對應的主題色,因此在設計接口的時候都考慮了這方面的需求。先看一下設置主題圖片的基本原理,以下:

  1. 建立一個主題圖片池(使用懶加載)
  2. 將相關控件對象直接添加到主題圖片池中
  3. 設置主題圖片時,經過block把主題圖片池中的全部對象傳遞給用戶,用戶實現block,在block中得到對象,並根據需求設置相關屬性完成主題圖片的設置

####代碼實現:

1. 建立一個主題圖片池(使用懶加載)
/** 主題圖片池 */
static NSMutableArray<id> *_themeImagePool;

- (NSMutableArray *)themeImagePool
{
    if (!_themeImagePool) {
        _themeImagePool = [NSMutableArray array];
    }
    return _themeImagePool;
}複製代碼
2. 添加相關控件到主題圖片池中

由於在設置圖片是,比較複雜,如UITabBar上面的UIBarItem的圖片、字體顏色等,因此爲了知足大部分用戶的需求,決定採用的是直接存儲控件對象

/** 添加到主題圖片池 */
- (void)py_addToThemeImagePool;

/** 從主題圖片池中移除 */
- (void)py_removeFromThemeImagePoo複製代碼

實現以下:

#pragma mark - Theme Image
/** 添加到主題圖片池 */
- (void)py_addToThemeImagePool
{
    // 若是對象爲_UIAppearance,直接返回
    Class appearanceClass = NSClassFromString(@"_UIAppearance");
    if ([self isMemberOfClass:appearanceClass]) return;

    if ([self isKindOfClass:[UITabBarItem class]]) { // 若是是UITabBarItem,判斷是否有設置圖片
        UITabBarItem *item = (UITabBarItem *)self;
        if (!item.image) { // 沒有設置圖片
            item.image = [[UIImage alloc] init];
        }
        if (!item.selectedImage) { // 沒有設置圖片
            item.selectedImage = [[UIImage alloc] init];
        }
    }
    // 判斷是否已經在主題圖片池中
    if (![[self themeImagePool] containsObject:self]) { // 不在主題圖片池中
        [[self themeImagePool] addObject:self];
    }
    // 遍歷主題圖片池(移除應該被回收的對象)
    for (id object in [self themeImagePool]) {
        NSInteger retainCount = [[object valueForKey:@"retainCount"] integerValue];
        if (retainCount == 2) { // 對象應該被回收了
            [[self themeImagePool] removeObject:self];
        }
    }
}

/** 從主題圖片池中移除 */
- (void)py_removeFromThemeImagePool
{
    // 若是對象爲_UIAppearance,直接返回
    Class appearanceClass = NSClassFromString(@"_UIAppearance");
    if ([self isMemberOfClass:appearanceClass]) return;

    // 判斷是否已經在圖片池中
    if ([[self themeImagePool] containsObject:self]) { // 在主題圖片池中
        [[self themeImagePool] removeObject:self];
    }
}複製代碼
3. 設置主題圖片和相關配色

當設置圖片時,會經過block將主題圖片池裏面的全部控件傳遞給用戶,用戶根據需求進行相關設置,若是提供了配色,就會採用上面設置主題色功能來設置主題色

/** 
 * 從新加載主題圖片
 * themeColor : 主題色
 * block : 設置主題圖片時調用的block
 */
- (void)py_reloadThemeImageWithThemeColor:(UIColor *)themeColor setting:(PYThemeImageSettingBlock)block;複製代碼

實現以下:

/** 從新加載主題圖片 */
- (void)py_reloadThemeImageWithThemeColor:(UIColor *)themeColor setting:(PYThemeImageSettingBlock)block
{
    if (themeColor) { // 有主題色,設置主題色
        [self py_setThemeColor:themeColor];
    }

    if (block) { // 存在block,直接調用
        block([self themeImagePool]);
    }
}複製代碼
使用

假設如今有這麼一個需求:更換主題圖片時,更換UITabBarItem的圖片

  1. 將UITabBarItem添加到圖片池
    // UITabBarItem
    [childController.tabBarItem py_addToThemeImagePool];複製代碼
  2. 切換主題圖片並設置配色爲紅色
    // 從新加載主題圖片,並設置主題色爲紅色
    [self py_reloadThemeImageWithThemeColor:[UIColor redColor] setting:^(const NSArray<id> *objects) {
        // 根據控件類型完成相關設置
    }複製代碼

總結

篇幅可能有點大,能耐心讀到這裏的讀者相信會有很多收穫的,但願讀者在閱讀此教程的時候,千萬不要學習代碼實現,而是要多思考:爲何要這樣實現?那樣實現有什麼很差?多學學接口爲何要這樣設計,那樣設計是否是更合理?當你帶着這些問題再回過頭來去看看源碼時,但願你會有更多的收貨!固然,這裏只是提供了一種思路,你也能夠在此基礎上實現夜間模式的切換等。期待大家的實現!

指望

固然若是您有更多的想法想表達或者交流的話,歡迎到留言/評論!由於本人比較喜歡活躍在GitHub社區,因此,若是您有什麼想反饋的也能夠issuse me,在這也鼓勵你們去多多發現優秀源碼,而且共享給你們。畢竟分享是雙方獲利的,何樂而不爲?

源碼地址:github.com/iphone5solo…
源碼做者:CoderKo1o

本文參與掘金技術徵文:gold.xitu.io/post/58522d…

相關文章
相關標籤/搜索