当前位置: 首页 > news >正文

[贝聊科技]谈谈 iOS 如何动态切换 APP 的主题

在移动互联网的下半场,越来越多的 APP 更加注重用户体验,以期来打动用户。主题的切换就是可以增强用户体验、结合运营活动的一个点:譬如 QQ 的夜间模式,节日里电商 APP 的皮肤切换等等的这些小细节往往就是赢得用户尊重的根本。本文将从主题的动态切换出发,介绍下贝聊 iOS 客户端在实现主题动态所采用的方案,供读者参考。

从切换方案说起

让 APP 已有的控件能切换主题可以用子类化,swizzle 或 category 来实现,其中子类化和 category 实现起来差不多,都是让控件调特定的方法达到切换风格的效果,而 swizzle 的影响范围会比较广,使用的时候可以通过 Associated Object 添加一个标记值,让需要切换风格的控件设置这个标记值,让标记值来决定是否需要 swizzle。考虑到上述几种方案的复杂度,最后选择了 category 来实现。

@implementation UILabel (BLTheme)

- (void)bl_setThemeTextColor {
    NSString *hexColorString = [BLThemeManager sharedInstance].styleModel.navTextColor;
    UIColor *color;
    if (hexColorString) {
        color = [UIColor colorFromHexString:hexColorString];
    }

    if (color) {
        [self setTextColor:color];
    }

}

@end复制代码

简单说来就是通过已配置的描述文件,在 category 内部读取了下配置的样式数据(样式数据可能为默认样式或自定义样式)。

怎样实现一个主题管理类

主题管理类的核心功能就是负责主题的更新,切换。正如下图所示,想让主题管理类通知到这么多待切换的 category 并不是一件容易的事,因为觉得在 category 上添加观察者并不是太好的设计,你很难知道什么时机该把观察者移除了。

难道说得改成子类化的实现,然后 ovrride dealloc 方法,可惜这样做感觉就没那么纯粹了。

这也就意味着,可能需要自己动手来实现回调机制了,让切换主题相关的 category 通过主题管理类注册一个回调 block,主题类维护使用一个字典维护这些 block,待切换时由主题管理类统一回调,达到类似 Notification 的效果。

先来看看 categroy 使用此方案后的变化,仍然是刚才的 UILabel 类:

- (void)bl_setThemeTextColor {
    NSString *hexColorString = [BLThemeManager sharedInstance].styleModel.navTextColor;
    UIColor *color;
    if (hexColorString) {
        color = [UIColor colorFromHexString:hexColorString];
    }

    if (color) {
        [self setTextColor:color];
    }

    @weakify(self)
    SwitchThemeBlock switchThemeBlock = ^{
        @strongify(self)
        [self bl_setThemeTextColor];
    };
    [[BLThemeManager sharedInstance] addObserveKey:[self keyWithSelector:@selector(bl_setThemeTextColor)] withSwitchThemBlock:switchThemeBlock];
}复制代码

只是在方法的底部添加了注册 block 的方法,而注册 block 的方法也十分简单,只需依据 key 判断下是否需要将 block 加入代码中。

// BLThemeManager.m
- (void)addObserveKey:(BLThemeMapModel *)key withSwitchThemBlock:(SwitchThemeBlock)block {
    if (block) {
        NSArray *allKeys = [self.blockDictionary allKeys];

        if (![allKeys containsObject:key]) {
            self.blockDictionary[key] = block;
        }
    }
}复制代码

那么问题来了,到底该如何设计一个这样的 key 呢?

  1. 同一个控件的主题 category 有多个需要切换主题的方法(例如 UIButton 有setTitleColor:forState: 和 setImage:forState:);
  2. 多个控件都是通过同一个 categroy 来切换主题(例如有多个 UIButton 需要切换主题);

其实统筹来看,就是如何通过某个类的实例和所需定制主题的方法来确定一个 key。

一开始很自然的拼了一个类的地址和方法名来作为key [NSString stringWithFormat:@"%p#%@", class, NSStringFromSelector(selector)]

流程能跑起来了,但是问题也很明显,只知道一个对象的指针字符串,根据对象是否被释放而进行的字典清理将变得难以实现:

// BLThemeManager.m
- (void)updateTheme {
    for (BLThemeMapModel *key in [self.blockDictionary allKeys]) {
        id object = key.target;
        if (object) {
            if ([object respondsToSelector:NSSelectorFromString(key.selectorName)]) {
                SwitchThemeBlock block = self.blockDictionary[key];
                if (block) {
                    block();
                }
            }

        } else {
            [self.blockDictionary removeObjectForKey:key];
        }
    }
}复制代码

那么,应该怎样设计 block 对应的 key 呢?

  1. 能从 key 中获取到注册的类;
  2. key 中也存有方法做 key 的唯一性和对象访问该方法安全性的校验 respondsToSelector

为此,实现了一个辅助的 model,用以访问需要注册的对象实例和方法名,同时作为 Dictionary 的 key,它还需要实现 NSCoping 协议:

@interface BLThemeMapModel : NSObject <NSCopying>

@property (nonatomic, weak, readonly) id target;
@property (nonatomic, copy, readonly) NSString *selectorName;

- (instancetype)initWithTarget:(id)target selctorName:(NSString *)selectorName;

@end复制代码

weak 修饰的对象实例能够在对象被释放后自动置 nil,下面附上最初的 .m 文件实现。

@implementation BLThemeMapModel

- (instancetype)initWithTarget:(id)target selctorName:(NSString *)selectorName {
    if (self = [super init]) {
        _target = target;
        _selectorName = selectorName;
    }
    return self;
}

- (BOOL)isEqual:(id)object {
    BLThemeMapModel *model = (BLThemeMapModel *)object;

    if (model) {
        if([self.target isEqualToString:model.target] &&
           [self.selectorName isEqualToString:model.selectorName]){
            return YES;
        }

    }
    return NO;
}

- (NSUInteger)hash {
    NSUInteger hash = [self.target hash] ^ [self.selectorName hash];
    return hash;
}

- (id)copyWithZone:(NSZone *)zone {
    BLThemeMapModel *themeModel = [[BLThemeMapModel allocWithZone:zone] initWithTarget:self.target selctorName:self.selectorName];
    return themeModel;
}

@end复制代码

可惜自测后发现一个挺莫名的 bug,最后调试了好一会才解决。细心的读者可以先想想看~

因为 target 可能被置 nil,从而引起同一个 key 的 hash 值被修改了,然后在遍历字典时,就无法取到之前加入字典的对象了,即便对象是被释放了,但仍有个 dirty 的 BLThemeMapModel 对象在字典里。

定位问题后其实就很好办了,在初始化方法中添加两个用以 hash 的属性:

_pointerString = [NSString stringWithFormat:@"%p", target];
_targetTypeName = NSStringFromClass([target class]);复制代码

最后使用这两个属性完成 hash 和 -isEqual: 方法:

- (BOOL)isEqual:(id)object {
    BLThemeMapModel *model = (BLThemeMapModel *)object;

    if (model) {
        if([self.pointerString isEqualToString:model.pointerString] &&
           [self.selectorName isEqualToString:model.selectorName] &&
           [self.targetTypeName isEqualToString:model.targetTypeName]){
            return YES;
        }

    }
    return NO;
}

- (NSUInteger)hash {
    NSUInteger hash = [self.pointerString hash] ^ [self.selectorName hash] ^ [self.targetTypeName hash];
    return hash;
}复制代码

至此,动态切换主题的功能大致就实现了,而且没使用到 OC 的任何动态方法。

总结

本文描述了实现一个主题管理类的大致思路,希望能对读者有所帮助。后来笔者想到既然有了 target 和 selector,能不能通过 NSInvocation 来动态调用,就不借助 block 来回调了,在尝试中笔者 NSInvocation 的效率的确会低一点,而且没有 block 灵活:

@implementation UIViewController (BLTheme)

- (UIStatusBarStyle)bl_setPreferredStatusBarStyle {
    NSInteger statusValue = [BLThemeManager sharedInstance].styleModel.statusBarColor;

    @weakify(self)
    SwitchThemeBlock switchThemeBlock = ^{
        @strongify(self)
        [self setNeedsStatusBarAppearanceUpdate];
    };
    [[BLThemeManager sharedInstance] addObserveKey:[self keyWithSelector:@selector(bl_setPreferredStatusBarStyle)] withSwitchThemBlock:switchThemeBlock];

    if (statusValue == 1) {
        return UIStatusBarStyleLightContent;
    } else {
        return UIStatusBarStyleDefault;
    }
}

@end复制代码

就像这,逻辑上并不期望再调一次 bl_setPreferredStatusBarStyle,而是仅仅调用一下 [self setNeedsStatusBarAppearanceUpdate]; 用 block 可以很灵活的指定好需要调用什么方法。

或许,也可以通过实现一个 weak proxy 的方式使用 Notification 来实现,笔者就没有尝试了,感兴趣的读者可以试试。

相关文章:

  • 使用JSONObject需要注意避免的一个问题
  • 项目总结——PHP小型网站经验总结
  • RSA简介(二)——模幂算法
  • 代码中banner文字
  • HDU 6090 Rikka with Graph
  • 如何正确地保守一个秘密?来听听书生云的独门绝技
  • WordPress Shortcode(简码)介绍及使用详解
  • Mongodb 备份与恢复
  • Redhat7-禁用firewalld开启iptablessystemctl使用简介
  • C# List 作为参数传递的值变化
  • sql语句-5-联接组合查询
  • tomcat6的编译和导入myeclipse
  • JavaScript实现的轮播图
  • 【分层图】分层图学习笔记
  • [译] 理解 Service Workers
  • [译] 怎样写一个基础的编译器
  • Angular js 常用指令ng-if、ng-class、ng-option、ng-value、ng-click是如何使用的?
  • CSS居中完全指南——构建CSS居中决策树
  • es6--symbol
  • Flex布局到底解决了什么问题
  • GitUp, 你不可错过的秀外慧中的git工具
  • Redis学习笔记 - pipline(流水线、管道)
  • vue和cordova项目整合打包,并实现vue调用android的相机的demo
  • yii2中session跨域名的问题
  • 闭包,sync使用细节
  • 基于Android乐音识别(2)
  • 技术发展面试
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 入门到放弃node系列之Hello Word篇
  • 算法-图和图算法
  • PostgreSQL 快速给指定表每个字段创建索引 - 1
  • 积累各种好的链接
  • ​比特币大跌的 2 个原因
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • (js)循环条件满足时终止循环
  • (附表设计)不是我吹!超级全面的权限系统设计方案面世了
  • (规划)24届春招和25届暑假实习路线准备规划
  • (接口封装)
  • (三)centos7案例实战—vmware虚拟机硬盘挂载与卸载
  • (三分钟了解debug)SLAM研究方向-Debug总结
  • (十一)JAVA springboot ssm b2b2c多用户商城系统源码:服务网关Zuul高级篇
  • (原)Matlab的svmtrain和svmclassify
  • ./indexer: error while loading shared libraries: libmysqlclient.so.18: cannot open shared object fil
  • .NET Framework与.NET Framework SDK有什么不同?
  • .NET 中 GetHashCode 的哈希值有多大概率会相同(哈希碰撞)
  • .net 中viewstate的原理和使用
  • .NET/C# 避免调试器不小心提前计算本应延迟计算的值
  • .NET处理HTTP请求
  • .net反编译工具
  • .sh
  • /bin/rm: 参数列表过长"的解决办法
  • @Conditional注解详解
  • [ 第一章] JavaScript 简史
  • [AAuto]给百宝箱增加娱乐功能
  • [Android Pro] AndroidX重构和映射