主题切换通知变化的方式
- 说到主题切换,那么就要做到切换主题瞬间,使所有相关的界面都发生变化,这就需要一种机制来将主题切换这个事件抛出来,并且接受主题切换事件的相关page(View)做出相应改变。看到这里,你可定也想到了NSNotification。没错,这是一个不错的选择,很适合我们的场景。
- 那还有没有其他好的方式呢?答案当然是有的,另一个类似的机制就是KVO。主题系统中一定需要一个单例来存储当前的主题状态,我们就可以去使用系统内建的KVO方式来观察这个主题状态,状态切换时,每个观察者就能够拿到这个事件去做一些处理。
- 第三种思路就是使用delegate。我们可以在主题管理Manager的单件中提供注册delegate的方法,将所有设置的delegate保存在一个list中,这样就可以在主题状态切换时候,遍历delegate去通知。
以上三种思路各有优劣,具体分析如下:
通知方式 | 优点 | 缺点 |
---|---|---|
NSNotification方式 | 不需要自己管理观察者序列 | main线程同步方式,性能欠佳 |
KVO方式 | 不需要自己管理观察者序列 | main主线程同步方式,性能上欠佳 |
自己管理delegate方式 | 可以做一些优化,如非顶层View可延后通知,通知时采用异步通知 | 需要自己维护观察者队列 |
当你看到这里,可能比较疑惑,该如何选择呢?事实上,NSNotification和KVO方式没有太大的差异,可能存在的问题就是多层级VIEW叠加是是否会出现性能问题,导致APP出现ANR。第三种方式可以在某种程度上解决这个问题。因为自己管理delegate的话可以控制是否有必要通知,而且可以在异步线程去通知,性能上有所优化空间。但是,虽然可以异步线程去通知,但我们主题切换一般是UI层级的操作,也就是必须要在主线程操作,so,个人认为异步线程通知,主线程修改UI,相较前两种方式优化效果有限。
对比了这么多,最终我实现使用了NSNotification方式,后面会贴出源码。
主题切换通知对象的讨论
按照我们的一般设计思想,UIView只处理UI相关的绘制而不去处理逻辑,这一点是大多数设计模式所要遵从的设计思路。那么,按照这个思路,我们设计主题切换思路大体如下:
在管理主题管理Manager中保存当前主题的状态,当主题发生变化时候,post出Notification。在我们在ViewController的基类里面接收这个通知,所有需要在主题切换时发生变化的ViewController只需要复写这个方法(姑且叫它onGetThemeSwitch:(GNThemeState *)state),在这个方法中更改View的样式即可。设计合情合理,符合大多数的设计规范。
看起来没什么问题。但是在具体实现起来,发现问题不小。
首先,ViewContrller中View一般较多,改变起来会导致onGetThemeSwitch逻辑相当复杂,逻辑不清晰*
-
其次,更要命的是很多ViewController中的View很多层次比较深,例如ViewController中有一个HeaderView,HeaderView中又有一个CtlView(用来盛放操作按钮),CtlView有可能有一个BackView,BackView中又有一个UIImageView(啊,看着就头疼),如果要在主题切换时改变UIImageView,那么面临的问题就是:
ViewController--->CtlView--->BackView--->UIImageView
这么长一个通知链。估计写起代码来会忍不住吐槽。同时代码的可维护性也是一个很大的问题。
基于以上问题,我改变了设计思路,决定采用View主动接受通知。因此想到了对UIView做手脚,为UIView搞一个主题扩展,下面直接上代码:
首先看看头文件:
//
// UIView+DayNight.h
// GameNews
//
// Copyright © 2016年 youxibar. All rights reserved.
//
#import
#import "GNDayNightManager.h"
/**
* 主题切换block
*
* @param state 当前主题状态(GNDayNightState)
*/
typedef void (^UIViewDayNight_themeChangeBlock)(GNDayNightState state);
/**
* 对UIView进行扩展,支持主题切换时两种回调方式
* 方式一: 子类重写dn_onDayNightStateHasChange:方法,该方法在主题变化时会被调用,注此方式需要主动设置self.dn_isNeedTheme=YES
* 方式二: 注册block dn_setThemeChangeBlock:,主题变化时回调block
*/
@interface UIView (DayNight)
/**
* 设置背景色的ID
*/
@property (nonatomic, strong) NSString *dn_backgroundColorID;
/**
* 是否注册主题通知,YES == 注册主题通知,主题切换时
* dn_onDayNightStateHasChange: 会被调用
*/
@property (nonatomic, assign) BOOL dn_isNeedTheme;
/**
* 子类通过复写该方法来做主题切换相关操作(切换图片,改变颜色等)
*
* @param state 主题状态
*/
- (void)dn_onDayNightStateHasChange:(GNDayNightState) state;
/**
* 注册主题变化的block
*
* @param themeChangeBlock 主题切换block
*/
- (void)dn_setThemeChangeBlock:(UIViewDayNight_themeChangeBlock)themeChangeBlock;
@end
我们可以看到,我们为外界提供了两种主题通知方式:
- 覆盖dn_onDayNightStateHasChange:父类这一方法,在主题变化时候会回调此方法;
- 采用dn_setThemeChangeBlock这种方式,主题变化时block会被回调。这种方式是为了解决直接使用系统UIView及子类时无法使用上面第一种方法而添加的一种方式。此方式的另一好处是代码看起来比较紧凑。
再看下实现文件
//
// UIView+DayNight.m
// GameNews
//
// Created by baidu on 16/5/27.
// Copyright © 2016年 youxibar. All rights reserved.
//
#import "UIView+DayNight.h"
#import
static int gn_dn_backgroundColorId; //backgroundColorID
static int gn_dn_isNeedThemeId; //是否需要注册主题ID
static int gn_dn_isHasRegistNotifId; //标识是否已经注册主题切换通知ID
static int gn_dn_themeChangeBlockId; //主题切换blockID
@implementation UIView (DayNight)
/**
* 交换dealloc函数,需要在dealloc之前remove通知
*/
+ (void)load {
NSString *className = NSStringFromClass(self.class);
NSLog(@"classname %@", className);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
//为了在dealloc之前去除theme通知,交换dealloc函数
SEL originalDealloc = NSSelectorFromString(@"dealloc");
SEL swizzledDealloc = @selector(gn_dealloc);
Method origMethod = class_getInstanceMethod(class, originalDealloc);
Method swizzMethod = class_getInstanceMethod(class, swizzledDealloc);
method_exchangeImplementations(origMethod, swizzMethod);
});
}
- (void)gn_dealloc {
if (self.dn_isHasRegistNotif) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:GNDAYNIGHT_STATE_CHANGE object:nil];
}
[self gn_dealloc];
}
#pragma mark - add property
//为UIView提供基础的主题背景色设置
- (NSString *)dn_backgroundColorID {
return objc_getAssociatedObject(self, &gn_dn_backgroundColorId);
}
- (void)setDn_backgroundColorID:(NSString *)colorID {
objc_setAssociatedObject(self, &gn_dn_backgroundColorId, colorID, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
self.dn_isNeedTheme = @(YES);
[self setBackgroundColor:UIColorFromRGB([[GNDayNightManager sharedInstance] getColorWithColorID:colorID])];
}
//标识是否需要主题,当设置为需要主题时,注册 GNDAYNIGHT_STATE_CHANGE 主题切换通知
- (BOOL)dn_isNeedTheme {
NSNumber *isNeedThemeNum = objc_getAssociatedObject(self, &gn_dn_isNeedThemeId);
return [isNeedThemeNum boolValue];
}
- (void)setDn_isNeedTheme:(BOOL)dn_isNeedTheme {
objc_setAssociatedObject(self, &gn_dn_isNeedThemeId, @(dn_isNeedTheme), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (dn_isNeedTheme && !self.dn_isHasRegistNotif) { //未注测过通知且需要主题通知,进行注册
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dn_onDayNightStateChange:) name:GNDAYNIGHT_STATE_CHANGE object:nil];
} else if(!dn_isNeedTheme && self.dn_isHasRegistNotif){ //设置为不需要注册,且已经注册,则把通知移除
[[NSNotificationCenter defaultCenter] removeObserver:self name:GNDAYNIGHT_STATE_CHANGE object:nil];
self.dn_isHasRegistNotif = NO;
}
}
//标识是否已经注册通知,防止dn_isNeedTheme被多次设置后导致同一个UIView注册多次通知
- (BOOL)dn_isHasRegistNotif {
NSNumber *isHasReg = objc_getAssociatedObject(self, &gn_dn_isHasRegistNotifId);
return [isHasReg boolValue];
}
- (void)setDn_isHasRegistNotif:(BOOL)dn_isHasRegistNotif {
NSNumber *hasRegistNotif = @(dn_isHasRegistNotif);
objc_setAssociatedObject(self, &gn_dn_isHasRegistNotifId, hasRegistNotif, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//themeChangeBlock
- (UIViewDayNight_themeChangeBlock)dn_themeChangeBlock {
return objc_getAssociatedObject(self, &gn_dn_themeChangeBlockId);
}
#pragma mark - public
- (void)dn_setThemeChangeBlock:(UIViewDayNight_themeChangeBlock)themeChangeBlock {
if (themeChangeBlock) {
self.dn_isNeedTheme = YES;
objc_setAssociatedObject(self, &gn_dn_themeChangeBlockId, themeChangeBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}else {
self.dn_isNeedTheme = NO;
}
}
#pragma mark - callback
- (void)dn_onDayNightStateChange:(NSNotification*)notification {
//处理backgroundColor
if ([[self dn_backgroundColorID] length] > 0) {
UIColor *backColor = UIColorFromRGB([[GNDayNightManager sharedInstance] getColorWithColorID:[self dn_backgroundColorID]]);
[UIView animateWithDuration:GNCommonAnimationsTime animations:^{
[self setBackgroundColor:backColor];
}];
}
if ([self dn_themeChangeBlock]) {
UIViewDayNight_themeChangeBlock block = [self dn_themeChangeBlock];
block([GNDayNightManagerInstance state]);
}
//子类可以复写此方法
[self dn_onDayNightStateHasChange:[[GNDayNightManager sharedInstance] state]];
}
- (void)dn_onDayNightStateHasChange:(GNDayNightState) state {
//子类按需实现
}
@end
实现文件中主要做了以下几件事
- hook系统的dealloc方法,这是为了能够在dealloc之前remove notification;
- 为UIView添加几个属性。通过关联对象,为UIView添加几个属性,主要是为了注册notification、保存block;
- 在接到notification回调的时候,将事件派发给注册的block及dn_onDayNightStateHasChange:方法。
再来看看主题管理类
主题管理类的基础功能如下:
- 保存当前主题状态;
- 提供主题切换的能力;
- 在主题切换时将该时间抛出去;
- 提供取image、color、font等的方法;
- 如果要支持在线下载的话还需要提供下载、解压、安装等能力。
我们来看下代码(m文件就不贴了):
//
// GNDayNightManager.h
// GameNews
//
// Created by baidu on 16/5/25.
// Copyright © 2016年 youxibar. All rights reserved.
//
#import
#define GNDayNightManagerInstance [GNDayNightManager sharedInstance]
extern NSString * const GNDAYNIGHT_STATE_CHANGE; //夜间<->日间模式切换通知
/**
* 主题状态
*/
typedef NS_ENUM(NSUInteger, GNDayNightState) {
GNDayNightInvalidState = 0, //error state
GNDayNightDayState, //日间主题
GNDayNightNightState, //夜间主题
};
/**
* 按钮状态
*/
typedef NS_ENUM(NSUInteger, GNButtonImgType) {
GNButtonImgNormalType, //按钮normal态
GNButtonImgPressType, //按钮press态
};
@interface GNDayNightManager : UIImageView
@property (nonatomic, assign) GNDayNightState state;
/**
* 获得主题管理单例
*
* @return 主题单例
*/
+ (GNDayNightManager*)sharedInstance;
/**
* 切换当前主题
*/
- (void)switchState;
/**
* 设置当前主题状态
*
* @param state (GNDayNightState)
*/
- (void)setState:(GNDayNightState)state;
/**
* 通过iconID及状态(GNButtonImgType)获取图片name
*
* @param iconID iconID
* @param type 所需图片状态(GNButtonImgType)
*
* @return 图片name
*/
- (NSString *)getIconNameWithIcon:(NSString *)iconID type:(GNButtonImgType) type;
/**
* 通过colorID获得当前主题对应的Color(16进制rgb)
*
* @param colorID colorID
*
* @return 16进制rgb
*/
- (int)getColorWithColorID:(NSString *)colorID;
@end
针对page也需要接受通知
上面针对UIView做了处理,实际应用中,可能要配合页面处理,因此,最好在page父类同样提供主题相关方法。
源码地址
https://github.com/yyj2013/simpleTheme
有问题请请联系我:
email:[email protected]
qq:1046152198