前言

换肤功能是在APP开发过程中遇到的比较多的场景,为了提供更好的用户体验,许多APP会为用户提供切换主题的功能。主题颜色管理涉及到的的步骤有

  • 颜色配置
  • 使用颜色
  • UI元素动态变更的能力
  • 动态修改配置
  • 主题包管理
  • 如何实施
  • 优化

效果如下:

DEMO代码:https://gitee.com/dhar/iosdemos/tree/master/YTThemeManagerDemo

颜色配置

因为涉及到多种配置,所以以代码的方式定义颜色实践和维护的难度是比较高的,一种合适的方案是--颜色的配置是通过配置文件的形式进行导入的。配置文件会经过转换步骤,最终形成代码层级的配置,以全局的方式提供给各个模块使用,这里会涉及到一个颜色管理者的概念,一般地这回事一个单例对象,提供全局访问的接口。同一个APP中在不同的模块中保存不同的主题颜色配置,在不同的层级中也可以存在不同的主题颜色配置,因为涉及到层级间的配置差异,所以颜色的配置需要引入一个等级的概念,一般地较高层级颜色的配置等级是高于较低层级的,存在相同的配置较高层级的配置会覆盖较低层级的配置。

我们采用的颜色配置的文件形如下面所示,为什么是在一个json文件的colorkey下面呢,是为了考虑到未来的扩展性,如果不同的主题会涉及到一些尺寸值的差异化,我们可以添加dimensionskey进行扩展配置。

{
 "color": {
 "Black_A":"323232",
 "Black_AT":"323232",
 "Black_B":"888888",
 "Black_BT":"888888",

 "White_A":"ffffff",
 "White_AT":"ffffff",
 "White_AN":"ffffff",

 "Red_A":"ff87a0",
 "Red_AT":"ff87a0",
 "Red_B":"ff5073",
 "Red_BT":"ff5073",

 "Colour_A":"377ce4",
 "Colour_B":"6aaafa",
 "Colour_C":"ff8c55",
 "Colour_D":"ffa200",
 "Colour_E":"c4a27a",
 }
}

有了以上的配置,颜色配置的工作主要就是解析该配置文件,把配置保存在一个单例对象中即可,这部分主要的步骤如下:

  • 配置文件类表根据等级排序
  • 获取每个配置文件中的配置,进行保存
  • 通知外部主题颜色配置发生改变

对应的代码如下,这里有个需要注意的地方是,加载配置文件的时候使用了文件读写锁进行读写的锁定操作,防止读脏数据的发生,直到配置文件加载完成,释放读写锁,这时读进程可以继续。

- (void)loadConfigWithFileName:(NSString *)fileName level:(NSInteger)level {
 if (fileName.length == 0) {
 return;
 }
 
 pthread_rwlock_wrlock(&_rwlock);
 __block BOOL finded = NO;
 [self.configFileQueue enumerateObjectsUsingBlock:^(YTThemeConfigFile *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
 if ([obj.fileName isEqualToString:fileName]) {
  finded = YES;
  *stop = YES;
 }
 }];
 if (!finded) {
 // 新增配置文件
 YTThemeConfigFile *file = [[YTThemeConfigFile alloc] init];
 file.fileName = fileName;
 file.level = level;
 [self.configFileQueue addObject:file];
 // 优先级排序
 [self.configFileQueue sortUsingComparator:^NSComparisonResult(YTThemeConfigFile *_Nonnull obj1, YTThemeConfigFile *_Nonnull obj2) {
  if (obj1.level > obj2.level) {
  return NSOrderedDescending;
  }
  return NSOrderedAscending;
 }];
 [self setupConfigFilesContainDefault:YES];
 }
 pthread_rwlock_unlock(&_rwlock);
}

- (void)setupConfigFilesContainDefault:(BOOL)containDefault {
 NSMutableDictionary *defaultColorDict = nil, *currentColorDict = nil;
 
 // 加载默认配置
 if (containDefault) {
 defaultColorDict = [NSMutableDictionary dictionary];
 [self loadConfigDataWithColorMap:defaultColorDict valueMap:nil isDefault:YES];
 
 self.defaultColorMap = defaultColorDict;
 }
 
 // 加载主题配置
 if (_themePath.length > 0) {
 currentColorDict = [NSMutableDictionary dictionary];
 [self loadConfigDataWithColorMap:currentColorDict valueMap:nil isDefault:NO];
 
 self.currentColorMap = currentColorDict;
 }
 
 // 发送主体颜色变更通知
 [self notifyThemeDidChange];
}

- (void)notifyThemeDidChange {
 NSArray *allActionObjects = self.actionMap.objectEnumerator.allObjects;
 for (YTThemeAction *action in allActionObjects) {
 [action notifyThemeDidChange];
 }
}

- (void)loadConfigDataWithColorMap:(NSMutableDictionary *)colorMap valueMap:(NSMutableDictionary *)valueMap isDefault:(BOOL)isDefault {
 // 每一次新增一个配置文件,所有配置文件都得重新计算一次,这里有很多重复多余的工作
 [self.configFileQueue enumerateObjectsUsingBlock:^(YTThemeConfigFile *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
 NSDictionary *dict = nil;
 if (isDefault) {
  dict = obj.defaultDict;
 } else {
  dict = obj.currentDict;
 }
 if (dict.count > 0) {
  [self loadThemeColorTo:colorMap from:dict]; // 将所有配置表中的color字段的数据都放到colorMap中
 }
 }];
}

- (void)loadThemeColorTo:(NSMutableDictionary *)dictionary from:(NSDictionary *)from {
 NSDictionary<NSString *, NSString *> *colors = from[@"color"];
 [colors enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) {
 // 十六进制字符串转为UIColor
 UIColor *color = [UIColor yt_nullcolorWithHexString:obj];
 if (color) {
  [dictionary setObject:color forKey:key];
 } else {
  [dictionary setObject:obj forKey:key];
 }
 }];
}

管理者处理处理配置之外,还需要暴露外部接口给客户端使用,以用于获取不同主题下对应的颜色色值、图片资源、尺寸信息等和主题相关的信息。比如我们会提供一个colorForKey方法获取不同主题下的同一个key对应的颜色色值,获取色值的大致步骤如下:

  • 从当前的主题配置中获取
  • 从默认的主题配置中获取
  • 从预留的主题配置中获取
  • 如果重定向的配置,递归处理
  • 以上步骤都完成还未找到返回默认黑色

这里使用了读写锁的写锁,如果同时有写操作获取了该锁,读取进程会阻塞直到写操作的完成释放锁。

/**
 获取颜色值
 */
- (UIColor *)colorForKey:(NSString *)key {
 pthread_rwlock_rdlock(&_rwlock);
 UIColor *color = [self colorForKey:key isReserveKey:NO redirectCount:0];
 pthread_rwlock_unlock(&_rwlock);
 return color;
}

- (UIColor *)colorForKey:(NSString *)key isReserveKey:(BOOL)isReserveKey redirectCount:(NSInteger)redirectCount {
 if (key == nil) {
 return nil;
 }
 
 ///正常获取色值
 id colorObj = [_currentColorMap objectForKey:key];
 if (colorObj == nil) {
 colorObj = [_defaultColorMap objectForKey:key];
 }
 
 if (isReserveKey && colorObj == nil) {
 return nil;
 }
 
 ///看看是否有替补key
 if (colorObj == nil) {
 NSString *reserveKey = [_reserveKeyMap objectForKey:key];
 if (reserveKey) {
  colorObj = [self colorForKey:reserveKey isReserveKey:YES redirectCount:redirectCount];
 }
 }
 
 ///查看当前key 能否转成 color
 if (colorObj == nil) {
 colorObj = [UIColor yt_colorWithHexString:key];
 }
 
 if ([colorObj isKindOfClass:[UIColor class]]) {
 ///如果是 重定向 或者 替补 key 的color 要设置到 当前 colorDict 里面
 // 重定向的配置形如:"Red_A":"Red_B",
 if (redirectCount > 0 || isReserveKey) {
  [_currentColorMap ?: _defaultColorMap setObject:colorObj forKey:key];
 }
 return colorObj;
 } else {
 if (redirectCount < 3) { // 重定向递归
  return [self colorForKey:colorObj isReserveKey:NO redirectCount:redirectCount   1];
 } else {
  return [UIColor blackColor];
 }
 }
}

使用颜色

颜色的使用也是经由管理者的,为了方便,定义一个颜色宏提供给客户端使用

#define YTThemeColor(key) ([[YTThemeManager sharedInstance] colorForKey:key])

客户端使用的代码如下:

UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, 200, 40)];
label.text = @"Text";
label.textColor = YTThemeColor(kCK_Red_A);
label.backgroundColor = YTThemeColor(kCK_Black_H);
[self.view addSubview:label];

另外,因为颜色配置的key为字符串类型,直接使用字符串常量并不是个好办法,所以把对应的字符串转换为宏定义是一个相对好的办法。第一个是方便使用,可以使用代码提示;第二个是不容易出错,特别是长的字符串;第三个也会一定程度上的提高效率。

YTColorDefine类的宏定义

// .h 中的声明
///Black
FOUNDATION_EXTERN NSString *kCK_Black_A;
FOUNDATION_EXTERN NSString *kCK_Black_AT;
FOUNDATION_EXTERN NSString *kCK_Black_B;
FOUNDATION_EXTERN NSString *kCK_Black_BT;

// .m 中的定义
NSString *kCK_Black_A = @"Black_A";
NSString *kCK_Black_AT = @"Black_AT";
NSString *kCK_Black_B = @"Black_B";
NSString *kCK_Black_BT = @"Black_BT";

主题包管理

在实际的落地项目中,主题包管理涉及到的事项包括主题包下载和解压和动态加载主题包等内容,最后的一步是更换主题配置文件所在的配置路径,为了演示的方便,我们会把不同主题的资源放置在bundle中某一个特定的文件夹下,通过切换管理者中的主题路径配置来达到切换主题的效果,和动态下载更换主题的步骤是一样的。

管理者提供一个设置主题配置的配置路径的方法,在该方法中改变配置路径的同时,重新加载配置即可,代码如下

/**
 设置主题文件的路径
 @param themePath 文件的路径
 */
- (void)setupThemePath:(NSString *)themePath {
 pthread_rwlock_wrlock(&_rwlock);
 
 _themePath = [themePath copy];
 
 self.currentColorMap = nil;
 
 if ([_themePath.lowercaseString isEqualToString:[[NSBundle mainBundle] resourcePath].lowercaseString]) {
 _themePath = nil;
 }
 
 self.currentThemePath = _themePath;
 
 for (int i = 0; i < self.configFileQueue.count; i  ) {
 YTThemeConfigFile *obj = [self.configFileQueue objectAtIndex:i];
 [obj resetCurrentDict];
 }
 [self setupConfigFilesContainDefault:NO];
 
 pthread_rwlock_unlock(&_rwlock);
}

如何实施

以上的流程涉及到的只是iOS平台下的一个技术解决方案,真实的实践过程中会涉及到安卓平台、Web页面、UI出图的标注,这些是要进行统一处理的,才能在各个端上有一致的体验。第一步就是制定合理的颜色规范,把规范同步给各个端的利益相关人员;第二部是UI出图颜色是规范的颜色定义值,而不是比如#ffffff这样的颜色,需要是比如White_A这样规范的颜色定义值,这样客户端处理使用的就是White_A这个值,不用管在不同主题下不同的颜色表现形式。

优化

loadConfigDataWithColorMap方法调用的优化

如果模块很多,每个模块都会调用loadConfigWithFileName加载配置文件,那么loadConfigDataWithColorMap方法处理文件的时间复杂度是O(N*N),会重复处理很多多余的工作,理想的做法是底层保存一份公有的颜色配置,然后在APP层加载一份定制化的配置,在模块中不用再加载主题配置文件,这样会提高效率。

附:读写锁pthread_rwlock_t的使用

读写锁是用来解决读者写者问题的,读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。

具有强读者同步和强写者同步两种形式

强读者同步:当写者没有进行写操作,读者就可以访问;

强写者同步:当所有写者都写完之后,才能进行读操作,读者需要最新的信息,一些事实性较高的系统可能会用到该所,比如定票之类的。

读写锁的操作:

读写锁的初始化:

        定义读写锁:          pthread_rwlock_t  m_rw_lock;

        函数原型:              pthread_rwlock_init(pthread_rwlock_t * ,pthread_rwattr_t *);

        返回值:0,表示成功,非0为一错误码

读写锁的销毁:

        函数原型:             pthread_rwlock_destroy(pthread_rwlock_t* );

        返回值:0,表示成功,非0表示错误码

获取读写锁的读锁操作:分为阻塞式获取和非阻塞式获取,如果读写锁由一个写者持有,则读线程会阻塞直至写入者释放读写锁。

        阻塞式:

                            函数原型:pthread_rwlock_rdlock(pthread_rwlock_t*);

        非阻塞式:

                            函数原型:pthread_rwlock_tryrdlock(pthread_rwlock_t*);

       返回值: 0,表示成功,非0表示错误码,非阻塞会返回ebusy而不会让线程等待

获取读写锁的写锁操作:分为阻塞和非阻塞,如果对应的读写锁被其它写者持有,或者读写锁被读者持有,该线程都会阻塞等待。

      阻塞式:

                           函数原型:pthread_rwlock_wrlock(pthread_rwlock_t*);

      非阻塞式:

                           函数原型:pthread_rwlock_trywrlock(pthread_rwlock_t*);

       返回值: 0,表示成功

释放读写锁:

                         函数原型:pthread_rwlock_unlock(pthread_rwlock_t*);

总结(转):

互斥锁与读写锁的区别:

当访问临界区资源时(访问的含义包括所有的操作:读和写),需要上互斥锁;

当对数据(互斥锁中的临界区资源)进行读取时,需要上读取锁,当对数据进行写入时,需要上写入锁。

读写锁的优点:

对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。

读写锁描述:

获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为共享-独占上锁。

有关这种类型问题(多个读出者和一个写入者)的其它说法有读出者与写入者问题以及多读出者-单写入者锁。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对Devmax的支持。

iOS实现换肤功能的简单处理框架(附源码)的更多相关文章

  1. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  2. iOS实现拖拽View跟随手指浮动效果

    这篇文章主要为大家详细介绍了iOS实现拖拽View跟随手指浮动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  4. ios – Testflight无法安装应用程序

    我有几个测试人员注册了testflight并连接了他们的设备……他们有不同的ios型号……但是所有这些都有同样的问题.当他们从“safari”或“testflight”应用程序本身单击应用程序的安装按钮时……达到约90%并出现错误消息…

  5. ibm-mobilefirst – 在iOS 7.1上获取“无法安装应用程序,因为证书无效”错误

    当我的客户端将他们的设备更新到iOS7.1,然后尝试从AppCenter更新我们的应用程序时,我收到了上述错误.经过一番搜索,我找到了一个类似问题的帖子here.但是后来因为我在客户端使用AppCenter更新应用程序的环境中,我无法使用USB插件并为他们安装应用程序.在发布支持之前,是否有通过AppCenter进行下载的解决方法?

  6. ios – 视图的简单拖放?

    我正在学习iOS,但我找不到如何向UIView添加拖放行为.我试过了:它说“UIView没有可见的接口声明选择器addTarget”此外,我尝试添加平移手势识别器,但不确定这是否是我需要的它被称为,但不知道如何获得事件的坐标.在iOS中注册移动事件回调/拖放操作的标准简单方法是什么?

  7. xcode6.1 – Xcode 6.1中项目模板中缺少类前缀

    项目模板上曾经有一个类前缀字段,这有助于区分项目类和框架类.Xcode6.1项目模板中不再提供此功能.这背后的意图是什么?

  8. ios – 什么控制iTunes中iPhone应用程序支持的语言列表?

    什么控制iPhone应用程序的iTunes页面中支持的语言?

  9. ios – 获得APNs响应BadDeviceToken或Unregistered的可能原因是什么?

    我知道设备令牌在某些时候是有效的.用户如何使其设备令牌变坏?从关于“未注册”的文档:Thedevicetokenisinactiveforthespecifiedtopic.这是否意味着应用程序已被删除?.您应该看到四种分发方法:如果您选择AppStore或Enterprise,您将在后面的对话框中看到Xcode将APNS权利更改为生产:如果选择AdHoc或Development,则aps-environment下的文本将是开发,然后应与后端的配置匹配.

  10. ios – 伞框架

    错误.应用程序,通常位于…错误仍然存在你也可以在这里添加(子)框架的路径.

随机推荐

  1. iOS实现拖拽View跟随手指浮动效果

    这篇文章主要为大家详细介绍了iOS实现拖拽View跟随手指浮动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  2. iOS – genstrings:无法连接到输出目录en.lproj

    使用我桌面上的项目文件夹,我启动终端输入:cd然后将我的项目文件夹拖到终端,它给了我路径.然后我将这行代码粘贴到终端中找.-name*.m|xargsgenstrings-oen.lproj我在终端中收到此错误消息:genstrings:无法连接到输出目录en.lproj它多次打印这行,然后说我的项目是一个目录的路径?没有.strings文件.对我做错了什么的想法?

  3. iOS 7 UIButtonBarItem图像没有色调

    如何确保按钮图标采用全局色调?解决方法只是想将其转换为根注释,以便为“回答”复选标记提供更好的上下文,并提供更好的格式.我能想出这个!

  4. ios – 在自定义相机层的AVFoundation中自动对焦和自动曝光

    为AVFoundation定制图层相机创建精确的自动对焦和曝光的最佳方法是什么?

  5. ios – Xcode找不到Alamofire,错误:没有这样的模块’Alamofire’

    我正在尝试按照github(https://github.com/Alamofire/Alamofire#cocoapods)指令将Alamofire包含在我的Swift项目中.我创建了一个新项目,导航到项目目录并运行此命令sudogeminstallcocoapods.然后我面临以下错误:搜索后我设法通过运行此命令安装cocoapodssudogeminstall-n/usr/local/bin

  6. ios – 在没有iPhone6s或更新的情况下测试ARKit

    我在决定下载Xcode9之前.我想玩新的框架–ARKit.我知道要用ARKit运行app我需要一个带有A9芯片或更新版本的设备.不幸的是我有一个较旧的.我的问题是已经下载了新Xcode的人.在我的情况下有可能运行ARKit应用程序吗?那个或其他任何模拟器?任何想法或我将不得不购买新设备?解决方法任何iOS11设备都可以使用ARKit,但是具有高质量AR体验的全球跟踪功能需要使用A9或更高版本处理器的设备.使用iOS11测试版更新您的设备是必要的.

  7. 将iOS应用移植到Android

    我们制作了一个具有2000个目标c类的退出大型iOS应用程序.我想知道有一个最佳实践指南将其移植到Android?此外,由于我们的应用程序大量使用UINavigation和UIView控制器,我想知道在Android上有类似的模型和实现.谢谢到目前为止,guenter解决方法老实说,我认为你正在计划的只是制作难以维护的糟糕代码.我意识到这听起来像很多工作,但从长远来看它会更容易,我只是将应用程序的概念“移植”到android并从头开始编写.

  8. ios – 在Swift中覆盖Objective C类方法

    我是Swift的初学者,我正在尝试在Swift项目中使用JSONModel.我想从JSONModel覆盖方法keyMapper,但我没有找到如何覆盖模型类中的Objective-C类方法.该方法的签名是:我怎样才能做到这一点?解决方法您可以像覆盖实例方法一样执行此操作,但使用class关键字除外:

  9. ios – 在WKWebView中获取链接URL

    我想在WKWebView中获取tapped链接的url.链接采用自定义格式,可触发应用中的某些操作.例如HTTP://我的网站/帮助#深层链接对讲.我这样使用KVO:这在第一次点击链接时效果很好.但是,如果我连续两次点击相同的链接,它将不报告链接点击.是否有解决方法来解决这个问题,以便我可以检测每个点击并获取链接?任何关于这个的指针都会很棒!解决方法像这样更改addobserver在observeValue函数中,您可以获得两个值

  10. ios – 在Swift的UIView中找到UILabel

    我正在尝试在我的UIViewControllers的超级视图中找到我的UILabels.这是我的代码:这是在Objective-C中推荐的方式,但是在Swift中我只得到UIViews和CALayer.我肯定在提供给这个方法的视图中有UILabel.我错过了什么?我的UIViewController中的调用:解决方法使用函数式编程概念可以更轻松地实现这一目标.

返回
顶部