概述
从开始接触oc到真正上手ios项目其实没有花多长的时间,但老实说对一些比较基础的知识还是不够熟练,所以希望可以借助blog来记录自己学习、成长的过程吧。
新型冠状病毒现在正在全国肆虐,公司安排最近一段时间在家远程工作,分配给我的任务是“研究收集现有的换肤框架的实现方案”,在网上搜索了很久,发现了DKNightVersion换肤框架,oc语言编写,支持夜间模式和多种自定义主题,demo截图如下。
属性设置
我们从最简单的页面代码开始分析如何使用DKNightVersion,先从PresentingViewController开始分析,大致可以看出设置背景色与设置按钮字体颜色通过以下代码:
self.view.dk_backgroundColorPicker = DKColorPickerWithKey(BG);//设置背景色
[button dk_setTitleColorPicker:DKColorPickerWithKey(TINT) forState:UIControlStateNormal];//设置按钮字体颜色
[switchButton dk_setTitleColorPicker:DKColorPickerWithKey(TINT) forState:UIControlStateNormal];//设置按钮字体颜色
其中我们可能不太明白的地方是:
1、方法 DKColorPickerWithKey()和参数BG的含义;
2、 属性 dk_backgroundColorPicker(dk_setTitleColorPicker)
DKColorPickerWithKey(BG)
DKColorPickerWithKey的定义如下:
#define DKColorPickerWithKey(key) [[DKColorTable sharedColorTable] pickerWithKey:@#key]
其实际执行的是DKColorTable的pickerWithKey方法,下面我们将详细分析一下DKColorTable类。
DKColorTable类
//单例方法定义
+ (instancetype)sharedColorTable {
static DKColorTable *sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
sharedInstance = [[DKColorTable alloc] init];
sharedInstance.file = @"DKColorTable.txt";
});
return sharedInstance;
}
设置sharedInstance.file属性时,会调用其定义好的set方法:
//file属性set方法
- (void)setFile:(NSString *)file {
_file = file;
[self reloadColorTable];//解析色表
}
主题中使用的所以颜色都得在DKColorTable.txt中定义好,DKColorTable.txt中的内容为:
NORMAL NIGHT RED
#ffffff #343434 #fafafa BG
#aaaaaa #313131 #aaaaaa SEP
#0000ff #ffffff #fa0000 TINT
#000000 #ffffff #000000 TEXT
#ffffff #444444 #ffffff BAR
#f0f0f0 #222222 #dedede HIGHLIGHTED
从DKColorTable.txt中解析主题以及对应的背景、字体等属性的颜色。
第一行主题名:NORMAL、NIGHT、RED
最后一列属性名:BG、SEP、TINT、TEXT、BAR、HIGHLIGHTED
若知道主题名与属性名则可以唯一确定颜色值!
//返回DKColorPicker类型的block,而block中的传入参数则是主题名,通过主题名与传入参数key(属性名)来获取具体颜色
- (DKColorPicker)pickerWithKey:(NSString *)key {
NSParameterAssert(key);
//可以获取到色表中的一行,主题名与颜色值一一对应
NSDictionary *themeToColorDictionary = [self.table valueForKey:key];
DKColorPicker picker = ^(DKThemeVersion *themeVersion) {
return [themeToColorDictionary valueForKey:themeVersion];
};
return picker;
}
结合上面的分析,可知DKColorPickerWithKey(BG)其实返回的是一个DKColorPicker类型的block,而想知道具体赋值的是什么颜色则需要查看在运行此block时传入的DKThemeVersion(主题名)是什么,具体的调用则是在dk_backgroundColorPicker中体现。
dk_backgroundColorPicker
页面属性dk_backgroundColorPicker和按钮属性dk_setTitleColorPicker都是为了实现换肤而新增的属性。下面的讨论将以dk_backgroundColorPicker为例,此属性在UIView+Night.h中定义:
@property (nonatomic, copy, setter = dk_setBackgroundColorPicker:) DKColorPicker dk_backgroundColorPicker;
对应的.m文件中定义了get/set方法:
- (DKColorPicker)dk_backgroundColorPicker {
return objc_getAssociatedObject(self, @selector(dk_backgroundColorPicker));
}
- (void)dk_setBackgroundColorPicker:(DKColorPicker)picker {
objc_setAssociatedObject(self, @selector(dk_backgroundColorPicker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
self.backgroundColor = picker(self.dk_manager.themeVersion);//真正设置背景色
[self.pickers setValue:[picker copy] forKey:@"setBackgroundColor:"];
}
通过关联属性的方式(objc_getAssociatedObject和objc_setAssociatedObject)来定义它的get/set方法。picker是上文中提及的DKColorPicker类型的block,此时传入参数self.dk_manager.themeVersion(属性名),则可以获取到颜色值,完成背景色的赋值。
除了设置背景色的,还需要特别注意self.pickers属性。
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;
“setBackgroundColor”与picker一一对应,此属性将会帮助我们在切换主题时自动切换背景色,后续会详细讲解。
主题切换
点击主题切换按钮时,调用函数switchColor
- (void)switchColor {
if ([self.dk_manager.themeVersion isEqualToString:DKThemeVersionNight]) {
[self.dk_manager dawnComing];
} else {
[self.dk_manager nightFalling];
}
}
根据英文含义,很容易明白[self.dk_manager dawnComing]为切换为正常模式,而[self.dk_manager nightFalling]表示切换至夜间模式。
其中最为重要的是dk_manager,它是DKNightVersionManager主题管理类的单例,负责整体主题的操作,下面我们将重点讲解DKNightVersionManager类。
DKNightVersionManager
先研究一下它的单例初始化代码
+ (DKNightVersionManager *)sharedManager {
static dispatch_once_t once;
static DKNightVersionManager *instance;
dispatch_once(&once, ^{
instance = [self new];
instance.changeStatusBar = YES;
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
DKThemeVersion *themeVersion = [userDefaults valueForKey:DKNightVersionCurrentThemeVersionKey];
themeVersion = themeVersion ?: DKThemeVersionNormal;//初始的模式为NORMAL
instance.themeVersion = themeVersion;//设置themeVersion(会调用setter方法)
instance.supportsKeyboard = YES;
});
return instance;
}
首先从NSUserDefault中获取主题名,然后将获取的主题名赋值给themeVersion,从而可以得知themeVersion的赋值将直接导致主题的切换!同时上面的代码也解答了怎么保存已经设置了的主题名与默认的初始主题为NORMAL的问题。
- (void)setThemeVersion:(DKThemeVersion *)themeVersion {
//设置主题时,若设置的主题与现主题相同,则直接返回
if ([_themeVersion isEqualToString:themeVersion]) {
// if type does not change, don't execute code below to enhance performance.
return;
}
_themeVersion = themeVersion;
//在NSUserDefault中保存设置的主题
// Save current theme version to user default
[[NSUserDefaults standardUserDefaults] setValue:themeVersion forKey:DKNightVersionCurrentThemeVersionKey];
//发送通知 UI控件调整颜色
[[NSNotificationCenter defaultCenter] postNotificationName:DKNightVersionThemeChangingNotification
object:nil];
//设置顶部导航栏颜色
if (self.shouldChangeStatusBar) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if ([themeVersion isEqualToString:DKThemeVersionNight]) {
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
} else {
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
}
#pragma clang diagnostic pop
}
}
代码中需要着重注意两点:
1、设置主题时,会在NSUserDefault中保存最新的主题名
2、发送名为“DKNightVersionThemeChangingNotification”,通知所有UI控件相应的切换颜色
所以,所谓的切换主题,则直接简化为赋值themeVersion,剩下的工作将通过通知的形式分发给各个UI控件,由UI控件自行完成。而UI控件又是如何注册通知,在接收到相应通知时具体又是怎么操作的呢?下面我们将进行重点讲解。
UI控件响应通知
随着主题切换的通知,UI控件则需要响应通知再进行各项属性设置,而UI控件是何时注册的通知,响应了通知事件后,又进行了哪些操作呢?首先,我们从最最基础的NSObject类开始看起。
//NSObject+Night.h
@interface NSObject (Night)
/**
* Default global DKNightVersionManager, this property gives us a more
* convinient way to access it.
*/
@property (nonatomic, strong, readonly) DKNightVersionManager *dk_manager;
@end
若导入此头文件,则直接导致所有对象都拥有DKNightVersionManager这一单例属性,则可以轻松获知当前主题名以及切换主题。
//NSObject+Night.m
@interface NSObject ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;
@end
@implementation NSObject (Night)
- (NSMutableDictionary<NSString *, DKColorPicker> *)pickers {
//获取pickers属性
NSMutableDictionary<NSString *, DKColorPicker> *pickers = objc_getAssociatedObject(self, @selector(pickers));
if (!pickers) {
@autoreleasepool {
// Need to removeObserver in dealloc
// 移除通知事件
if (objc_getAssociatedObject(self, &DKViewDeallocHelperKey) == nil) {
__unsafe_unretained typeof(self) weakSelf = self; // NOTE: need to be __unsafe_unretained because __weak var will be reset to nil in dealloc
id deallocHelper = [self addDeallocBlock:^{
[[NSNotificationCenter defaultCenter] removeObserver:weakSelf];
}];
objc_setAssociatedObject(self, &DKViewDeallocHelperKey, deallocHelper, OBJC_ASSOCIATION_ASSIGN);
}
}
pickers = [[NSMutableDictionary alloc] init];
objc_setAssociatedObject(self, @selector(pickers), pickers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 移除通知事件
[[NSNotificationCenter defaultCenter] removeObserver:self name:DKNightVersionThemeChangingNotification object:nil];
// 重新添加通知事件 事件触发时调用night_updateColor函数
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(night_updateColor) name:DKNightVersionThemeChangingNotification object:nil];
}
return pickers;
}
// dk_manager属性
- (DKNightVersionManager *)dk_manager {
return [DKNightVersionManager sharedManager];
}
// 响应DKNightVersionThemeChangingNotification事件时触发的函数
- (void)night_updateColor {
[self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull selector, DKColorPicker _Nonnull picker, BOOL * _Nonnull stop) {
SEL sel = NSSelectorFromString(selector);
id result = picker(self.dk_manager.themeVersion);
[UIView animateWithDuration:DKNightVersionAnimationDuration
animations:^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:sel withObject:result];
#pragma clang diagnostic pop
}];
}];
}
@end
这段代码解释了何时会注册通知事件,即使用pickers属性时,就会为UI控件注册DKNightVersionThemeChangingNotification通知事件,而事件触发时,则会调用函数night_updateColor来进行具体操作。self.pickers中存放的是什么呢?还记得在PresentingViewController页面代码中有对self.pickers进行赋值嘛?
[self.pickers setValue:[picker copy] forKey:@"setBackgroundColor:"];
也就是说,night_updateColor函数中,遍历了self.pickers中的所有元素,将Key值作为函数,将picker(self.dk_manager.themeVersion)【获取当前主题的颜色】的执行结果作为参数,进行执行,其实际代码意义为
[self setBackgroundColor:picker(self.dk_manager.themeVersion)];
若UIView组件引用了NSObject+Night.h这个头文件,则就可以在接受到切换主题的通知事件后改变背景色。至此,响应主题切换颜色的功能大致就已经完成了!
UIView+Night.m中的代码我们已经看过了,我将再以UILabel+Night.m的代码为例,再大致阐述一下UILabel是如何实现响应主题切换的。
// UILabel+Night.h
#import <UIKit/UIKit.h>
//调用了此头文件,则UILabel在调用pickers属性时,将会自动添加DKNightVersionThemeChangingNotification通知的响应事件
#import "NSObject+Night.h"
@interface UILabel (Night)
@property (nonatomic, copy, setter = dk_setTextColorPicker:) DKColorPicker dk_textColorPicker;
@property (nonatomic, copy, setter = dk_setShadowColorPicker:) DKColorPicker dk_shadowColorPicker;
@property (nonatomic, copy, setter = dk_setHighlightedTextColorPicker:) DKColorPicker dk_highlightedTextColorPicker;
@end
//UILabel+Night.m
#import "UILabel+Night.h"
#import "DKNightVersionManager.h"
#import <objc/runtime.h>
@interface UILabel ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;
@end
@implementation UILabel (Night)
- (DKColorPicker)dk_textColorPicker {
return objc_getAssociatedObject(self, @selector(dk_textColorPicker));
}
- (void)dk_setTextColorPicker:(DKColorPicker)picker {
objc_setAssociatedObject(self, @selector(dk_textColorPicker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
// 设置文本颜色
self.textColor = picker(self.dk_manager.themeVersion);
// 将setTextColor函数名与picker存储至pickers中,当响应切换事件时,则可以通过pickers来改变颜色
// 这也是第一次使用pickers属性,使用了此属性后也就注册了切换监听事件
[self.pickers setValue:[picker copy] forKey:@"setTextColor:"];
}
- (DKColorPicker)dk_shadowColorPicker {
return objc_getAssociatedObject(self, @selector(dk_shadowColorPicker));
}
- (void)dk_setShadowColorPicker:(DKColorPicker)picker {
objc_setAssociatedObject(self, @selector(dk_shadowColorPicker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
self.shadowColor = picker(self.dk_manager.themeVersion);
[self.pickers setValue:[picker copy] forKey:@"setShadowColor:"];
}
- (DKColorPicker)dk_highlightedTextColorPicker {
return objc_getAssociatedObject(self, @selector(dk_highlightedTextColorPicker));
}
- (void)dk_setHighlightedTextColorPicker:(DKColorPicker)picker {
objc_setAssociatedObject(self, @selector(dk_highlightedTextColorPicker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
self.highlightedTextColor = picker(self.dk_manager.themeVersion);
[self.pickers setValue:[picker copy] forKey:@"setHighlightedTextColor:"];
}
@end
代码整体还是相对比较好理解的,对UILabel中可以设置不同颜色的属性进行了封装,并在封装后的属性的setter方法中保存设置颜色的具体方法(例:setTextColor)与picker(存放了属性的各个主题的所有颜色),将二者存放于pickers字典当中,而pickers属性在初始化时会注册DKNightVersionThemeChangingNotification监听事件,从而保证了在主题切换时,通过pickers字典自行完成颜色的切换。
在UIKit目录下作者封装好了系统定义的UIKit控件,而在Manual目录下则封装的是作者自定的一些控件,我们自己也可以根据自己的需求自行进行添加。
后记
一步一步的把代码跟读下来,发现作者的整体设计还是很巧妙的,值得学习一下这种的设计思路。从阅读源码到写完这篇博客,大概花了3天左右的时间(没办法,在家的效率真的是一言难尽~),其中还有很多部分,比如DKImagePicker与dellocBlockExecutor等都还没有细看。若后续真正将此框架现有app后,将进一步完善此博客。
第一次写这么长的博文,都是按照我自己看代码的习惯进行阐述,若有一些理解错误之处,还望不吝赐教,若有不太明白的地方,可以一同讨论,一同进步~
愿疫情早日过去,早日可以坐在办公室摸鱼~
哈哈~
网友评论