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 <UIKit/UIKit.h>

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 <Foundation/Foundation.h>

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 <objc/runtime.h>
#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 <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (YNDeallocExecutor)

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

@end

NS_ASSUME_NONNULL_END

#import "NSObject+YNDeallocExecutor.h"
#import <objc/runtime.h>

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

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

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