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 
#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
实现文件中主要做了以下几件事
  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 

#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

你可能感兴趣的:(iOS APP 开发中的主题切换设计思路)