很久沒寫文章了,正好最近在研究換膚,因此將最近的心得和體會與你們分享一下。git
iOS換膚的方式比較單一,查找了不少資料,發現主流的方式有以下兩種:github
方式一:經過給 Category 添加屬性的方式實現換膚,有一個 Manager 用以管理顏色和圖片,當主題改變時,經過發出通知告訴 UIKit 中的相關類,該改變視圖顏色了,這時視圖就會根據 Manager 中提供的不一樣主題的顏色來改變本身的顏色。objective-c
方式二:使用系統提供的 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
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];
}
}
}
複製代碼
setXXXColor:
方法實現不須要或儘可能少對原項目代碼進行改動。首先須要提供一個 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 中的內容以下:
以 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 管理也是一個缺點。
總體上思路就是如此,這個方案只是一個初步方案,還有不少不少不足之處。