iOS APP 开发中的主题切换设计思路

主题切换通知变化的方式


  • 说到主题切换,那么就要做到切换主题瞬间,使所有相关的界面都发生变化,这就需要一种机制来将主题切换这个事件抛出来,并且接受主题切换事件的相关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 <UIKit/UIKit.h>
#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 <objc/runtime.h>

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
实现文件中主要做了以下几件事
  1. hook系统的dealloc方法,这是为了能够在dealloc之前remove notification;
  2. 为UIView添加几个属性。通过关联对象,为UIView添加几个属性,主要是为了注册notification、保存block;
  3. 在接到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 <UIKit/UIKit.h>

#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:yanyongjie@live.cn
qq:1046152198

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容