美文网首页iOS常用
iOS App 换肤方法 - 本地换肤

iOS App 换肤方法 - 本地换肤

作者: 小老弟码代码 | 来源:发表于2020-10-12 16:59 被阅读0次

    说到主题切换,那么久要做到切换主题瞬间,使所有相关的界面都发生变化,这就需要一种机制来将主题切换这是事件跑出来,并且接受主题切换事件的相关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
    

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

    相关文章

      网友评论

        本文标题:iOS App 换肤方法 - 本地换肤

        本文链接:https://www.haomeiwen.com/subject/tazlpktx.html