介紹
好久沒(méi)寫(xiě)文章了,正好最近在研究換膚,所以將最近的心得和體會(huì)與大家分享一下。
-
iOS換膚的方式比較單一,查找了很多資料,發(fā)現(xiàn)主流的方式有如下兩種:
-
方式一:通過(guò)給 Category 添加屬性的方式實(shí)現(xiàn)換膚,有一個(gè) Manager 用以管理顏色和圖片,當(dāng)主題改變時(shí),通過(guò)發(fā)出通知告訴 UIKit 中的相關(guān)類,該改變視圖顏色了,這時(shí)視圖就會(huì)根據(jù) Manager 中提供的不同主題的顏色來(lái)改變自己的顏色。
- 這種方案的優(yōu)點(diǎn)在于:整體思路比較簡(jiǎn)單明了,實(shí)現(xiàn)起來(lái)也不困難。
- 缺點(diǎn)在于:
- 對(duì)于每種控件,都已經(jīng)將顏色固定死,沒(méi)有辦法設(shè)置比如同一個(gè)父視圖的兩個(gè)子視圖不同的顏色顯示。
- 當(dāng)我們的項(xiàng)目已經(jīng)完成了,而且項(xiàng)目體積也比較大時(shí),這種方式的缺點(diǎn)就暴露的非常明顯了:更改界面十分麻煩,因?yàn)槲覀兊慕缑姹容^多時(shí),需要給每個(gè)界面的每個(gè)控件都添加在 Category 中增加的屬性, 這種方式工作量巨大。
方式二:使用系統(tǒng)提供的 UIAppearance 來(lái)更改主題,這種方式的優(yōu)點(diǎn)在于,系統(tǒng)提供了非常簡(jiǎn)單方便的 API 供我們使用,最常用的就是 + (instancetype)appearance; 方法和
+ (instancetype)appearanceWhenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, …;這兩個(gè)方法。具體用法如下:[[UINavigationBar appearance] setBarTintColor:myNavBarBackgroundColor];可以設(shè)置全局的 UINavigationBar 的 barTintColor。而[[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], nil] setBackgroundImage:myNavBarButtonBackgroundImage forState:state barMetrics:metrics];表示在指定視圖中設(shè)置 color,在此示例中是設(shè)置 UINavigationBar 上的 UIBarButtonItem 的背景圖片。-
這種方式的原理在于:使用 UI_APPEARANCE_SELECTOR 標(biāo)記的方式會(huì)將當(dāng)前對(duì) UI 設(shè)置的外觀保存起來(lái),等到視圖在添加到 window 之前會(huì)調(diào)用這個(gè)之前保存的外觀,更新視圖外觀。所以并不是 UIKit 中所有的類的所有屬性都可以使用這個(gè)方法來(lái)設(shè)置 UI,只有當(dāng)屬性上有標(biāo)志 UI_APPEARANCE_SELECTOR 才可以用這個(gè)方法來(lái)設(shè)置。
這種方式的優(yōu)點(diǎn)是可以十分便捷的設(shè)置一些全局的系統(tǒng)控件的外觀。
-
但是缺點(diǎn)也十分明顯:
- 當(dāng)我們想要區(qū)分同一個(gè)父視圖上方的子視圖時(shí),這種方案就會(huì)十分的不方便,與第一種方法一樣,很難達(dá)到個(gè)性化定制的目的。
- 并且當(dāng)我們想要設(shè)置 UILabel 等控件在不同視圖上的字體顏色等時(shí),經(jīng)常會(huì)失效,通過(guò)查看系統(tǒng) API,可以發(fā)現(xiàn) UILabel 的
setTextColor:等方法并沒(méi)有 UI_APPEARANCE_SELECTOR 標(biāo)志位,所以這也是這個(gè)換膚方式并不是萬(wàn)能的原因。Stack Overflow有一篇關(guān)于 UILabel 設(shè)置顏色失效的原因,他們說(shuō)這是蘋(píng)果系統(tǒng)的一個(gè) bug。而解決這個(gè)問(wèn)題的方法也比較簡(jiǎn)單,只要我們重寫(xiě)setTextColor:方法,給它加上一個(gè) UI_APPEARANCE_SELECTOR 標(biāo)志位,那么就可以給它定制顏色。但是這種方式的缺點(diǎn)也十分明顯,對(duì)代碼的改動(dòng)并沒(méi)有任何減少。反而當(dāng)有很多控件都不能正確顯示顏色時(shí),還需要增加很大的工作量。
總結(jié):我認(rèn)為這種設(shè)置 UIAppearance 的方式還是比較適用于當(dāng)全局的顏色已經(jīng)固定時(shí),設(shè)置主題,比如 UINavigationBar 和 UITabbar 這種控件,就比較適合使用這種方式來(lái)進(jìn)行操作。當(dāng)我們的換膚比較簡(jiǎn)單,不涉及類似夜間模式這種需要幾乎把所有的控件顏色都改變時(shí),我覺(jué)得也可以使用這種方法來(lái)進(jìn)行換膚操作。
-
另外:這個(gè)方法需要注意的一個(gè)點(diǎn)是,當(dāng)我們改變主題顏色時(shí),需要先將控件從 window 上移除,再重新添加才會(huì)觸發(fā)這種方式。
- (void)p_updateSystemWindow { NSArray *windowArray = [UIApplication sharedApplication].windows; for (UIWindow *window in windowArray) { for (UIView *subView in window.subviews) { [subView removeFromSuperview]; [window addSubview:subView]; } } }
-
自己的想法
- 首先我們應(yīng)該明確需求背景:
- 最基本的就是:能夠?qū)崿F(xiàn)換膚
- 項(xiàng)目已經(jīng)完成,并且項(xiàng)目比較復(fù)雜不適合一個(gè)控制器一個(gè)控制器的去修改
- 能夠?qū)崿F(xiàn)控件的個(gè)性化顏色定制,而并不是所有的一類控件都是同種顏色
- 產(chǎn)生的問(wèn)題:
- 是否可以結(jié)合上述兩種方式,產(chǎn)生自己的方式來(lái)進(jìn)行簡(jiǎn)便的換膚?
- 如何做到盡量少改動(dòng)代碼,就能實(shí)現(xiàn)換膚的效果?
- 如何實(shí)現(xiàn)控件的個(gè)性化顏色定制?
- 如何解決:
- 既然整個(gè)項(xiàng)目都已經(jīng)完成,那么如果我想盡量少改動(dòng)代碼,是否可以使用 methodSwizzling 的方式來(lái) hook 系統(tǒng)的
setXXXColor:方法實(shí)現(xiàn)不需要或盡量少對(duì)原項(xiàng)目代碼進(jìn)行改動(dòng)。 - 既然需要對(duì)控件進(jìn)行個(gè)性化定制,是否可以使用 tag 的方式,對(duì)需要個(gè)性化的控件添加 tag 從而根據(jù)不同的 tag 來(lái)使用不同的顏色,而不需要個(gè)性化的顏色保持原本狀態(tài)不進(jìn)行修改。
- 既然整個(gè)項(xiàng)目都已經(jīng)完成,那么如果我想盡量少改動(dòng)代碼,是否可以使用 methodSwizzling 的方式來(lái) hook 系統(tǒng)的
我的實(shí)踐
-
首先需要提供一個(gè) Manager 來(lái)進(jìn)行主題的控制,在我的項(xiàng)目中,它叫做
LYThemeManager, 這個(gè) Manager 的作用是控制切換不同的主題,當(dāng)主題進(jìn)行改變時(shí),可以發(fā)出通知,告知 UI 控件該改變自己的顏色了。并且它所提供的(UIColor *)colorWithReceiver:(id)receiver selString:(NSString *)selector;和(UIColor *)colorWithReceiver:(id)receiver withTag:(NSInteger)tag selString:(NSString *)selector;分別是實(shí)現(xiàn)全局控件 UI 的設(shè)置以及 個(gè)性化控件 UI 的設(shè)置。-
在
LYThemeManager內(nèi)部有兩個(gè)字典,分別是讀取不同的 plist ,colorInfoDic用于讀取全局 UI 的顏色設(shè)置,而specialColorInfoDic用于讀取個(gè)性化控件的顏色設(shè)置,具體的 plist 中的內(nèi)容如下:imageimage在 specialPlist 中前面的數(shù)字表示 tag 值,后面表示設(shè)置的屬性意義。
-
以 UIView 的 category 為例,首先在這個(gè)類中,使用了 methodSwizzle 來(lái)實(shí)現(xiàn) hook 系統(tǒng)方法,在這里我 hook 了系統(tǒng)的
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 來(lái)選方法,注意子類和父類不要使用同名方法,否則會(huì)導(dǎo)致符號(hào)混亂產(chǎn)生循環(huán)引用。 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]; } }在這里為什么我要使用個(gè)性化顏色設(shè)置的方法:
(UIColor *)colorWithReceiver:(id)receiver withTag:(NSInteger)tag selString:(NSString *)selector;,這是因?yàn)閹缀跛?UIKit 中的控件都繼承自 UIView,當(dāng)我們直接將所有的 setBackgroundColor: 方法都設(shè)置為同一顏色時(shí),達(dá)到的效果是災(zāi)難性的所有控件都是同一顏色。無(wú)法進(jìn)行區(qū)分。所以這里使用個(gè)性化的,只對(duì) controller 中的 view 改變顏色。 -
添加了一個(gè)字典屬性 pickers, 這個(gè)屬性用以將我們 hook 的方法添加進(jìn)來(lái),它的 key 是方法名, value是它應(yīng)該被設(shè)置的 color,當(dāng)收到改變顏色的通知時(shí),需要遍歷這個(gè)屬性中所有的數(shù)據(jù),來(lái)實(shí)現(xiàn)顏色更新。
@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; } -
最后就是對(duì)通知的響應(yīng):
#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,并且響應(yīng)方式都同于 UIView ,所以在其他的 category 中省去了對(duì)屬性 picker 的 Add Property 步驟以及對(duì)通知的響應(yīng)。
在 UILabel 中的
setTextColor:方法也使用了個(gè)性化的設(shè)置,對(duì)于不需要特殊設(shè)置的 UILabel 的 textColor 則原本默認(rèn)是什么顏色,就是什么顏色。所有的 tag 值,我都以宏定義的方式存儲(chǔ)在 ThemeConfig.pch 中了,當(dāng)需要個(gè)性定義的控件比較多時(shí),通過(guò) tag 管理也是一個(gè)缺點(diǎn)。
-
整體上思路就是如此,這個(gè)方案只是一個(gè)初步方案,還有很多很多不足之處。
- 缺點(diǎn)在于:
- 比如說(shuō)通過(guò) tag 來(lái)管理顏色,實(shí)際上也會(huì)修改原項(xiàng)目的代碼,因?yàn)槲覀冃枰O(shè)置不同控件的 tag 值。
- hook 系統(tǒng)的方法或許會(huì)帶來(lái)意想不到的bug。不過(guò)在我 hook 的這種方式下,當(dāng)在顏色匹配表中找不到對(duì)應(yīng)字段時(shí),會(huì)直接使用原來(lái)的顏色進(jìn)行設(shè)置,感覺(jué)也沒(méi)有什么特別大的問(wèn)題。
- 這種方式的優(yōu)勢(shì)在于:
- 可以盡可能減少對(duì)原項(xiàng)目的改動(dòng)
- 并且可以實(shí)現(xiàn)對(duì)不同要求的控件進(jìn)行個(gè)性化定制。基本上完成了對(duì)一開(kāi)始提出的問(wèn)題的解決。
- 缺點(diǎn)在于:
-
總結(jié)
- 這種方案還是一種比較不成熟的方案,沒(méi)有經(jīng)過(guò)真正項(xiàng)目的認(rèn)證,當(dāng)項(xiàng)目比較大時(shí),這種方案可能還是不能夠很好的解決問(wèn)題。不過(guò)這也是一次新的嘗試。以后我會(huì)就這方面繼續(xù)進(jìn)行修改和嘗試。也歡迎有想法的大家來(lái)與我進(jìn)行討論,希望能不吝賜教!
- 項(xiàng)目的代碼在:這個(gè)地址