iOS App 换肤方法 - 本地换肤

说到主题切换,那么久要做到切换主题瞬间,使所有相关的界面都发生变化,这就需要一种机制来将主题切换这是事件跑出来,并且接受主题切换事件的相关View 做出相应的改变。想到这里你肯定也想到了NSNotification。没错,这就是个不错的选择,很适合我们的场景。下面具体来实现下。

不管是本地换肤还是动态换肤都需要一个Manager 进行初始化主题模式,一半情况下都使用单例初始化就可以。

YNThemeManager.h

主要提供这几个方法:

  • (void)setupThemeNameArray:(NSArray *)array; 是用来初始化主题模式名称的, 例如我们初始化两个本地资源文件 YNTheme-White 和 YNTheme-Black 是bundle文件名称
[[YNThemeManager sharedInstance] setupThemeNameArray:@[@"YNTheme-White", @"YNTheme-Black"]];

-- (BOOL)changeTheme:(NSString *)themeName; 用来改变主题模式的,在实际使用中只需要将已有的bundle名称传入即可

[[YNThemeManager sharedInstance] changeTheme:@"YNTheme-White"];
  • + (UIColor *)colorWithID:(NSString *)colorID;用来获取颜色
  • + (UIImage *)imageWithName:(NSString *)imageName;用来获取图片

YNThemeManager.m

1.初始化

+ (instancetype)sharedInstance{
    
    static YNThemeManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[YNThemeManager alloc] init];
    });
    return manager;
}

2.首先申明几个属性
bundle colorsMap themeArray

/** 主题bundle*/
@property (nonatomic,strong) NSBundle *bundle;
/** 颜色对照表*/
@property (nonatomic, copy) NSDictionary *colorsMap;
/** 主题数组*/
@property (nonatomic, copy) NSArray *themeArray;

3.主题数组赋值

- (void)setupThemeNameArray:(NSArray *)array{
    self.themeArray = array;
}

4.改变主题.m实现

- (BOOL)changeTheme:(NSString *)themeName{
    /** 判断当前切换主题是否在主题数组中*/
    if (![_themeArray containsObject:themeName]) {
        return NO;
    }
    /** 获取bundle路径*/
    NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:themeName withExtension:@"bundle"]];
    if (!bundle) {
        return NO;
    }
    /** 获取bundle下plist文件路径*/
    NSString *mapPath = [bundle pathForResource:@"ColorsMap" ofType:@"plist"];
    if (!mapPath) {
        return NO;
    }
    /** 获取字典*/
    NSDictionary *colorsMap = [NSDictionary dictionaryWithContentsOfFile:mapPath];
      /** 赋值*/
    _themeName = themeName;
    self.bundle = bundle;
    self.colorsMap = colorsMap;
    /** 发送修改通知*/
    [self sendChangeThemeNotification];
    return YES;
}
/** 发送修改通知*/
- (void)sendChangeThemeNotification {
    [[NSNotificationCenter defaultCenter] postNotificationName:YNThemeChangeNotification object:nil];
}

5.获取颜色

+ (UIColor *)colorWithID:(NSString *)colorID{
    if (!colorID) {
        return [UIColor clearColor];
    }
    return [UIColor yn_colorWithHexString:[[self class] colorStringWithID:colorID]];
}
/** 用来查找plist 文件中对应色值的value */
+ (NSString *)colorStringWithID:(NSString *)colorID{
    
    NSArray *array = [colorID componentsSeparatedByString:@"_"];
    NSAssert(array.count > 1,  @"未找到对应颜色-%@", colorID);
    NSDictionary *colorDict = [[YNThemeManager sharedInstance].colorsMap valueForKeyPath:array[0]];
    NSString *value = colorDict[colorID][@"Color"];
    NSAssert(value, @"未找到对应颜色-%@", colorID);
    return value;
}

6.获取图片

+ (UIImage *)imageWithName:(NSString *)imageName {
    if (!imageName) {
        return nil;
    }
    NSBundle *bundle = [YNThemeManager sharedInstance].bundle;
    UIImage *image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
    NSAssert(image, @"未找到对应图片-%@", imageName);
    
    return image;
}
  • 首先,控制器中的控件比较多,改变起来逻辑相当复杂,逻辑可能不是很清楚
  • 其次就是VC 中有些View 有很多层次,如;VC 中有一个HeaderView ,HeaderView中有BlackView,BlackView 中又有ImageView ,ImageView 中可能还有其他控件,如果要是在主题切换时改变ImageView,面临的问题就是
    VC ---->HeaderView -----> BlackView ---->ImageView
    这么长的一个通知链。估计写起来会忍不住吐槽。同时维护起来也是很大的问题。
基于以上问题,我改变了设计思路,决定采用系统控件主动接受通知。因此想到了对控件做手脚,以Label为例,为UILabel搞一个主题扩展
  • 大家可以看到其中有换肤属性theme_textColor ,如下图,我们在属性theme_textColor 的Setter方法中有根据主题配置调用系统的相应方法,然后对控件注册监听,等切换主题之后就会收到通知,然后执行theme_didChanged方法,为控件设置正确的主题UI下面直接上代码:
#import 

NS_ASSUME_NONNULL_BEGIN

@interface UILabel (YNTheme)

@property (nonatomic, copy) NSString *theme_textColor;

@property (nonatomic, copy) NSAttributedString *theme_attributedText;

@end

NS_ASSUME_NONNULL_END
@implementation UILabel (YNTheme)

- (void)theme_didChanged {
    [super theme_didChanged];
    if (self.theme_textColor) {
        self.textColor = [YNThemeManager colorWithID:self.theme_textColor];
    }
    if (self.attributedText) {
        self.attributedText = self.attributedText.theme_replaceRealityColor;
    }
}

// MARK:  ================ Setters ===========================
- (void)setTheme_textColor:(NSString *)color {
    self.textColor = [YNThemeManager colorWithID:color];
    objc_setAssociatedObject(self, @selector(theme_textColor), color, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self theme_registChangedNotification];
}

- (void)setTheme_attributedText:(NSAttributedString *)attributedText {
    self.attributedText = attributedText.theme_replaceRealityColor;
    [self theme_registChangedNotification];
}

- (void)setSDTextColorID:(NSString *)SDTextColorID {
    self.theme_textColor = SDTextColorID;
}

// MARK:  ================ Getters ===========================
- (NSString *)theme_textColor {
    return objc_getAssociatedObject(self, @selector(theme_textColor));
}

- (NSAttributedString *)theme_attributedText {
    return self.attributedText;
}

@end
  • 当然这里面会用到通知,我们专门创建一个NSObject+YNTheme分类,用于通知管理,废话不多说,直接上代码。
#import 

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (YNTheme)

/**
    注册换肤监听,不会重复监听
    收到通知后会调用 theme_didChanged 方法
 */
- (void)theme_registChangedNotification;

/**
    注册换肤监听,不会重复监听
    会立即调用一次 themeChangeBlock,和收到通知后调用
 */
- (void)theme_observerChangedUsingBlock:(void(^)(id observer))themeChangeBlock;

/** 子类重写,收到换肤通知会调用本方法*/
- (void)theme_didChanged;

@end

NS_ASSUME_NONNULL_END
#import "NSObject+YNTheme.h"
#import "YNThemeManager.h"
#import 
#import "NSObject+YNDeallocExecutor.h"

static NSString *const kHasRegistChangedThemeNotification;

@interface NSObject ()

@property (nonatomic, copy) void(^theme_changeBlock)(id observer);

@end

@implementation NSObject (YNTheme)


- (void)theme_registChangedNotification {
    NSNumber *hasRegist = objc_getAssociatedObject(self, &kHasRegistChangedThemeNotification);
    /** 标识是否已经注册通知,防止多次设置后导致同一个控件被注册多次*/
    if (hasRegist) {
        return;
    }
    objc_setAssociatedObject(self, &kHasRegistChangedThemeNotification, @(YES), OBJC_ASSOCIATION_COPY_NONATOMIC);
    
    /** 接收通知*/
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(theme_didChanged) name:YNThemeChangeNotification object:nil];
    
    /** 暂时不明白*/
    __weak typeof(self) weakSelf = self;
    [self yn_executeAtDealloc:^{
        [[NSNotificationCenter defaultCenter] removeObserver:weakSelf];
    }];
}
- (void)theme_observerChangedUsingBlock:(void(^)(id observer))themeChangeBlock {
    self.theme_changeBlock = themeChangeBlock;
    [self theme_didChanged];
    [self theme_registChangedNotification];
}

- (void)theme_didChanged {
    if (self.theme_changeBlock) {
        __weak typeof(self) weakSelf = self;
        self.theme_changeBlock(weakSelf);
    }
}

- (void)setTheme_changeBlock:(void (^)(void))block {
    objc_setAssociatedObject(self, @selector(theme_changeBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void (^)(void))theme_changeBlock {
    return objc_getAssociatedObject(self, @selector(theme_changeBlock));
}
@end
  • 不知道大家发现没有这里面涉及到一个 block回调方法yn_executeAtDealloc这里面具体做什么,容我细细道来。
  • 我们在开发过程经常会遇到这样的情况,我们想监测一个NSObject对象到底有没有释放掉,通常的做法就是继承于一个父类在其dealloc方法中进行NSLog打印输出了,这时候我们有没有思考可以很方便的去实现dealloc方法的捕获?下面和大家分享一个简单的方法,来实现这个过程,废话不多说直接上代码。
#import 

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (YNDeallocExecutor)

- (void)yn_executeAtDealloc:(void (^)(void))block;

@end

NS_ASSUME_NONNULL_END

#import "NSObject+YNDeallocExecutor.h"
#import 

const void *YNDeallocExecutorsKey = &YNDeallocExecutorsKey;

@interface YNDeallocExecutor : NSObject

@property (nonatomic, copy) void(^deallocExecutorBlock)(void);

@end

@implementation YNDeallocExecutor

- (id)initWithBlock:(void(^)(void))deallocExecutorBlock {
    self = [super init];
    if (self) {
        _deallocExecutorBlock = [deallocExecutorBlock copy];
    }
    return self;
}

- (void)dealloc {
    _deallocExecutorBlock ? _deallocExecutorBlock() : nil;
}

@end

@implementation NSObject (YNDeallocExecutor)

- (void)yn_executeAtDealloc:(void (^)(void))block{
    if (block) {
        YNDeallocExecutor *executor = [[YNDeallocExecutor alloc] initWithBlock:block];
        /** 创建一个互斥锁,保证在同一时间内没有其它线程对self对象进行修改,起到线程的保护作用*/
        @synchronized (self) {
            [[self hs_deallocExecutors] addObject:executor];
        }
    }
}

- (NSHashTable *)hs_deallocExecutors {

    NSHashTable *table = objc_getAssociatedObject(self,YNDeallocExecutorsKey);
    if (!table) {
        table = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];
        objc_setAssociatedObject(self, YNDeallocExecutorsKey, table, OBJC_ASSOCIATION_RETAIN);
    }
    return table;
}

@end

以上就是我的换肤思路了,菜鸟小老弟,如有不足,请多多指教!!!

你可能感兴趣的:(iOS App 换肤方法 - 本地换肤)