iOS 实现主题切换,相信在将来的app里也是会频繁出现的,尽管如今只是出如今主流的APP,如(QQ、新浪微博、酷狗音乐、网易云音乐等),可是如今是看颜值、追求个性的年代,因此根据用户喜爱自定义/切换主题也是将来app的必备功能了。git
为了下降耦合度,决定采用的方案是使用NSObject的分类来实现主题设置,有些读者可能会想为什么不使用UIView的分类而是使用NSObject的分类?建议这部分读者看一下UIBarItem父类,而后仔细思考一下,就会理解了。github
建议读者在理解思路之后先下载源码大概看一下(纵观全局)再阅读如下内容:
源码地址:github.com/iphone5solo…数组
因为是在NSObject的分类里面建立,为了方便管理,设置全局变量_themeColorPool,并经过懒加载完成_themeColorPool的实例化。数组中的对象原来采用的是NSDictionary,可是因为NSDictionary存储时,会对对象采用强引用致使对象不能被及时释放,因此最终采用的解决方案是采用NSMapTable存储,实现对象的弱引用,详情见下一步就会理解了app
/** 主题颜色池 */
static NSMutableArray<NSMapTable *> *_themeColorPool;
#pragma mark - 懒加载
- (NSMutableArray *)themeColorPool
{
if (!_themeColorPool) {
_themeColorPool = [NSMutableArray array];
}
return _themeColorPool;
}复制代码
因为颜色设置有的能够直接经过属性设置也有的须要经过调用方法才可设置。以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;
}
}
}复制代码
/** * 设置主题色 * 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
将navigationBar的background和UIButton的setTitleColor: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;
}复制代码
当对象应该被释放后,下一次当主题色池有新元素添加时,会遍历主题色池,根据对象的引用计数来决定是否移除对象(实现自动管理内存),所以:主题色池中最多可能会残留一个对象,这对内存几乎没有任何影响,若是要及时释放对象本人认为能够采用KVO监听对象的引用计数(何尝试),可是耗能高,不建议这么作!
了解UIAppearance的读者应该能够理解,并且使用UIAppearance的目的也为为了设置全局色,因此为了不冲突,若是使用了该“技术”就不添加到主题色池
观察了新浪微博、酷狗音乐等app,发现设置主题图片仍是颇有必要的,并且发现每套主题皮肤/图片都有对应的主题色,因此在设计接口的时候都考虑了这方面的需求。先看一下设置主题图片的基本原理,以下:
####代码实现:
/** 主题图片池 */
static NSMutableArray<id> *_themeImagePool;
- (NSMutableArray *)themeImagePool
{
if (!_themeImagePool) {
_themeImagePool = [NSMutableArray array];
}
return _themeImagePool;
}复制代码
由于在设置图片是,比较复杂,如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];
}
}复制代码
当设置图片时,会经过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的图片
// UITabBarItem
[childController.tabBarItem py_addToThemeImagePool];复制代码
// 从新加载主题图片,并设置主题色为红色
[self py_reloadThemeImageWithThemeColor:[UIColor redColor] setting:^(const NSArray<id> *objects) {
// 根据控件类型完成相关设置
}复制代码
篇幅可能有点大,能耐心读到这里的读者相信会有很多收获的,但愿读者在阅读此教程的时候,千万不要学习代码实现,而是要多思考:为何要这样实现?那样实现有什么很差?多学学接口为何要这样设计,那样设计是否是更合理?当你带着这些问题再回过头来去看看源码时,但愿你会有更多的收货!固然,这里只是提供了一种思路,你也能够在此基础上实现夜间模式的切换等。期待大家的实现!
固然若是您有更多的想法想表达或者交流的话,欢迎到留言/评论!由于本人比较喜欢活跃在GitHub社区,因此,若是您有什么想反馈的也能够issuse me,在这也鼓励你们去多多发现优秀源码,而且共享给你们。毕竟分享是双方获利的,何乐而不为?
源码地址:github.com/iphone5solo…
源码做者:CoderKo1o
本文参与掘金技术征文:gold.xitu.io/post/58522d…