[貝聊科技]談談 iOS 如何動態切換 APP 的主題

在移動互聯網的下半場,愈來愈多的 APP 更加註重用戶體驗,以期來打動用戶。主題的切換就是能夠加強用戶體驗、結合運營活動的一個點:譬如 QQ 的夜間模式,節日裏電商 APP 的皮膚切換等等的這些小細節每每就是贏得用戶尊重的根本。本文將從主題的動態切換出發,介紹下貝聊 iOS 客戶端在實現主題動態所採用的方案,供讀者參考。安全

從切換方案提及

讓 APP 已有的控件能切換主題能夠用子類化,swizzle 或 category 來實現,其中子類化和 category 實現起來差很少,都是讓控件調特定的方法達到切換風格的效果,而 swizzle 的影響範圍會比較廣,使用的時候能夠經過 Associated Object 添加一個標記值,讓須要切換風格的控件設置這個標記值,讓標記值來決定是否須要 swizzle。考慮到上述幾種方案的複雜度,最後選擇了 category 來實現。bash

@implementation UILabel (BLTheme)

- (void)bl_setThemeTextColor {
    NSString *hexColorString = [BLThemeManager sharedInstance].styleModel.navTextColor;
    UIColor *color;
    if (hexColorString) {
        color = [UIColor colorFromHexString:hexColorString];
    }

    if (color) {
        [self setTextColor:color];
    }

}

@end複製代碼

簡單說來就是經過已配置的描述文件,在 category 內部讀取了下配置的樣式數據(樣式數據可能爲默認樣式或自定義樣式)。ide

怎樣實現一個主題管理類

主題管理類的核心功能就是負責主題的更新,切換。正以下圖所示,想讓主題管理類通知到這麼多待切換的 category 並非一件容易的事,由於以爲在 category 上添加觀察者並非太好的設計,你很難知道什麼時機該把觀察者移除了。ui

難道說得改爲子類化的實現,而後 ovrride dealloc 方法,惋惜這樣作感受就沒那麼純粹了。atom

這也就意味着,可能須要本身動手來實現回調機制了,讓切換主題相關的 category 經過主題管理類註冊一個回調 block,主題類維護使用一個字典維護這些 block,待切換時由主題管理類統一回調,達到相似 Notification 的效果。spa

先來看看 categroy 使用此方案後的變化,仍然是剛纔的 UILabel 類:設計

- (void)bl_setThemeTextColor {
    NSString *hexColorString = [BLThemeManager sharedInstance].styleModel.navTextColor;
    UIColor *color;
    if (hexColorString) {
        color = [UIColor colorFromHexString:hexColorString];
    }

    if (color) {
        [self setTextColor:color];
    }

    @weakify(self)
    SwitchThemeBlock switchThemeBlock = ^{
        @strongify(self)
        [self bl_setThemeTextColor];
    };
    [[BLThemeManager sharedInstance] addObserveKey:[self keyWithSelector:@selector(bl_setThemeTextColor)] withSwitchThemBlock:switchThemeBlock];
}複製代碼

只是在方法的底部添加了註冊 block 的方法,而註冊 block 的方法也十分簡單,只需依據 key 判斷下是否須要將 block 加入代碼中。指針

// BLThemeManager.m
- (void)addObserveKey:(BLThemeMapModel *)key withSwitchThemBlock:(SwitchThemeBlock)block {
    if (block) {
        NSArray *allKeys = [self.blockDictionary allKeys];

        if (![allKeys containsObject:key]) {
            self.blockDictionary[key] = block;
        }
    }
}複製代碼

那麼問題來了,到底該如何設計一個這樣的 key 呢?調試

  1. 同一個控件的主題 category 有多個須要切換主題的方法(例如 UIButton 有setTitleColor:forState: 和 setImage:forState:);
  2. 多個控件都是經過同一個 categroy 來切換主題(例若有多個 UIButton 須要切換主題);

其實統籌來看,就是如何經過某個類的實例和所需定製主題的方法來肯定一個 key。code

一開始很天然的拼了一個類的地址和方法名來做爲key [NSString stringWithFormat:@"%p#%@", class, NSStringFromSelector(selector)]

流程能跑起來了,可是問題也很明顯,只知道一個對象的指針字符串,根據對象是否被釋放而進行的字典清理將變得難以實現:

// BLThemeManager.m
- (void)updateTheme {
    for (BLThemeMapModel *key in [self.blockDictionary allKeys]) {
        id object = key.target;
        if (object) {
            if ([object respondsToSelector:NSSelectorFromString(key.selectorName)]) {
                SwitchThemeBlock block = self.blockDictionary[key];
                if (block) {
                    block();
                }
            }

        } else {
            [self.blockDictionary removeObjectForKey:key];
        }
    }
}複製代碼

那麼,應該怎樣設計 block 對應的 key 呢?

  1. 能從 key 中獲取到註冊的類;
  2. key 中也存有方法作 key 的惟一性和對象訪問該方法安全性的校驗 respondsToSelector

爲此,實現了一個輔助的 model,用以訪問須要註冊的對象實例和方法名,同時做爲 Dictionary 的 key,它還須要實現 NSCoping 協議:

@interface BLThemeMapModel : NSObject <NSCopying>

@property (nonatomic, weak, readonly) id target;
@property (nonatomic, copy, readonly) NSString *selectorName;

- (instancetype)initWithTarget:(id)target selctorName:(NSString *)selectorName;

@end複製代碼

weak 修飾的對象實例可以在對象被釋放後自動置 nil,下面附上最初的 .m 文件實現。

@implementation BLThemeMapModel

- (instancetype)initWithTarget:(id)target selctorName:(NSString *)selectorName {
    if (self = [super init]) {
        _target = target;
        _selectorName = selectorName;
    }
    return self;
}

- (BOOL)isEqual:(id)object {
    BLThemeMapModel *model = (BLThemeMapModel *)object;

    if (model) {
        if([self.target isEqualToString:model.target] &&
           [self.selectorName isEqualToString:model.selectorName]){
            return YES;
        }

    }
    return NO;
}

- (NSUInteger)hash {
    NSUInteger hash = [self.target hash] ^ [self.selectorName hash];
    return hash;
}

- (id)copyWithZone:(NSZone *)zone {
    BLThemeMapModel *themeModel = [[BLThemeMapModel allocWithZone:zone] initWithTarget:self.target selctorName:self.selectorName];
    return themeModel;
}

@end複製代碼

惋惜自測後發現一個挺莫名的 bug,最後調試了好一會才解決。細心的讀者能夠先想一想看~

由於 target 可能被置 nil,從而引發同一個 key 的 hash 值被修改了,而後在遍歷字典時,就沒法取到以前加入字典的對象了,即使對象是被釋放了,但仍有個 dirty 的 BLThemeMapModel 對象在字典裏。

定位問題後其實就很好辦了,在初始化方法中添加兩個用以 hash 的屬性:

_pointerString = [NSString stringWithFormat:@"%p", target];
_targetTypeName = NSStringFromClass([target class]);複製代碼

最後使用這兩個屬性完成 hash 和 -isEqual: 方法:

- (BOOL)isEqual:(id)object {
    BLThemeMapModel *model = (BLThemeMapModel *)object;

    if (model) {
        if([self.pointerString isEqualToString:model.pointerString] &&
           [self.selectorName isEqualToString:model.selectorName] &&
           [self.targetTypeName isEqualToString:model.targetTypeName]){
            return YES;
        }

    }
    return NO;
}

- (NSUInteger)hash {
    NSUInteger hash = [self.pointerString hash] ^ [self.selectorName hash] ^ [self.targetTypeName hash];
    return hash;
}複製代碼

至此,動態切換主題的功能大體就實現了,並且沒使用到 OC 的任何動態方法。

總結

本文描述了實現一個主題管理類的大體思路,但願能對讀者有所幫助。後來筆者想到既然有了 target 和 selector,能不能經過 NSInvocation 來動態調用,就不借助 block 來回調了,在嘗試中筆者 NSInvocation 的效率的確會低一點,並且沒有 block 靈活:

@implementation UIViewController (BLTheme)

- (UIStatusBarStyle)bl_setPreferredStatusBarStyle {
    NSInteger statusValue = [BLThemeManager sharedInstance].styleModel.statusBarColor;

    @weakify(self)
    SwitchThemeBlock switchThemeBlock = ^{
        @strongify(self)
        [self setNeedsStatusBarAppearanceUpdate];
    };
    [[BLThemeManager sharedInstance] addObserveKey:[self keyWithSelector:@selector(bl_setPreferredStatusBarStyle)] withSwitchThemBlock:switchThemeBlock];

    if (statusValue == 1) {
        return UIStatusBarStyleLightContent;
    } else {
        return UIStatusBarStyleDefault;
    }
}

@end複製代碼

就像這,邏輯上並不指望再調一次 bl_setPreferredStatusBarStyle,而是僅僅調用一下 [self setNeedsStatusBarAppearanceUpdate]; 用 block 能夠很靈活的指定好須要調用什麼方法。

或許,也能夠經過實現一個 weak proxy 的方式使用 Notification 來實現,筆者就沒有嘗試了,感興趣的讀者能夠試試。

相關文章
相關標籤/搜索