iOS开发——使用技术OC片&换肤处理(白天与黑夜)

换肤处理(白天与黑夜)

一:首先是使用简单的方法实现

不记得谁说的了,中国的用户大概是世界上最喜欢多皮肤功能的用户了。我很讨厌写安卓程序,图形界面设计工具及其难用,还不如手 写,编辑器慢 如蜗牛,智能提示总是跟不上我输入的速度,相同的功能,安卓的代码量至少是iOS的三倍,每写一行代码,都觉得自己的手指在滴血。可是安卓灵活统一的 style功能确实很贴心!5之前,iOS平台上实现相同的功能一直没有个比较好的办法。

  苹果将所有界面组件的设定,都绑定在一个叫UIAppearance的协议上了,你可以简单的通过UIAppearance设定组件的全局风格。

  

例如,我想把所有的UIButton的title都设成白色:

[[UIButton appearance] setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];

  

或者,我想所有的UIButton都有统一的背景图片

[[UIButton appearance] setBackgroundImage:aBackgroundImage forState:UIControlStateNormal];

  

当然,实际项目中,不同风格的组件,应该单独定义成一个类,然后在它的initialize方法设置它的UIAppearance,例如我定义了一个AFKButton类,我就可以写成下面这样,这样应用中所有的AFKButton类的Button都是一个风格的了。

 1 @implementation AFKButton
 2   ......
 3  + (void)initialize {
 4      if (self == [AFKButton self]) {
 5          [[self appearance] setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
 6          [[self appearance] setBackgroundImage:aBackgroundImage forState:UIControlStateNormal];
 7      }
 8  }
 9 ......
10 @end

  

修改UIAppearance,有个限制,就是如果想让它生效,必须在下次装载入app的主窗口时才能生效,所以,如果要通过UIAppearance动态修改组件的风格,我们就需要在UIAppDelegate中实现下面的代码

1 UIViewController *rootViewController = self.window.rootViewController;
2  self.window.rootViewController = nil;
3
4 [[UIButton appearance] setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
5 [[UIButton appearance] setBackgroundImage:aBackgroundImage forState:UIControlStateNormal];
6
7  self.window.rootViewController = rootViewController;

  

由上所述,我设计的动态切换皮肤的风格如下:

  • 1.首先按照风格建立相应的组件类,例如,你有几种Button,就继承实现几个Button类。
  • 2.设置全局风格标志。
  • 3.触发风格修改的地方,通过全局广播发送消息。
  • 4.UIAppDelegate重新装载window的rootViewController

二:使用第三方

在很多重阅读或者需要在夜间观看的软件其实都会把夜间模式当做一个 App 所需要具备的特性. 而如何在不改变原有的架构, 甚至不改变原有的代码的基础上, 就能为应用优雅地添加夜间模式就成为一个在很多应用开发的过程中不得不面对的一个问题.

就是以上事情的驱动, 使我思考如何才能使用一种优雅并且简洁的方法解决这一问题.

DKNightVersion 就是我带来的解决方案.

到目前为止, 这个框架的大部分的工作都已经完成了, 或许它现在不够完善, 不过我会持续地维护这个框架, 帮助饱受实现夜间模式之苦的工程师们解决这个

坑的一逼的

需求.

实现

现在我也终于有时间来

水一水

写一篇博客来说一下这个框架是如何实现夜间模式的, 它都有哪些特性.

在很长的一段时间我都在想如何才能在不覆写 UIKit 控件的基础上, 为 iOS App 添加夜间模式. 而 objc/runtime 为我带来了不覆写 UIKit 就能实现这一目的的希望.

为 UIKit 控件添加 nightColor 属性

因为我们并不会子类化 UIKit 控件, 然后使用 @property 为它的子类添加属性. 而是使用 Objective-C 中神奇的分类(Category) 和 objc/runtime , 为 UI 系列的控件添加属性.

使用 objc/runtime 为分类添加属性相信很多人都知道而且经常在开发中使用了. 如果不了解的话, 可以看 这里 .

DKNightVersion 为大多数常用的 color 比如说: backgroundColor tintColor 都添加了以 night 开头的夜间模式下的颜色, nightBackgroundColor nightTintColor .

1 - (UIColor *)nightBackgroundColor {
2   return objc_getAssociatedObject(self, &nightBackgroundColorKey) ? :self.backgroundColor);
3 }
4 - (void)setNightBackgroundColor:(UIColor *)nightBackgroundColor {
5   objc_setAssociatedObject(self, &nightBackgroundColorKey, nightBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
6 }

我们创建这个属性以保存夜间模式下的颜色, 这样当应用的主题切换到夜间模式时, 将 nightColor 属性存储的颜色赋值给对应的 color , 但是这会有一个问题. 当应用重新切换回正常模式时, 我们失去了原有正常模式的 color .

添加 normalColor 存储颜色

为了解决这一问题, 我们为 UIKit 控件添加了另一个属性 normalColor 来保存正常模式下的颜色.

1 - (UIColor *)normalBackgroundColor {
2   return objc_getAssociatedObject(self, &normalBackgroundColorKey);
3 }
4 - (void)setNormalBackgroundColor:(UIColor *)normalBackgroundColor {
5   objc_setAssociatedObject(self, &normalBackgroundColorKey, normalBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
6 }

但是保存这个颜色的时机是非常重要的, 在最开始的时候, 我的选择是直接覆写 setter 方法, 在保存颜色之前存储 normalColor .

- (void)setBackgroundColor:(UIColor *)backgroundColor {
    self.normalBackgroundColor = backgroundColor;
    _backgroundColor = backgroundColor;
}

然而这种看似可以运行的 setter 其实会导致视图不会被着色, 设置 color 包括正常的颜色都不会有任何的反应, 反而视图的背景颜色一片漆黑.

由于上面这种方法行不通, 我想换一种方法使用观察者模式来存储 normalColor , 将实例自己注册为 color 属性的观察者, 当 color 属性变化时, 通知 UIKit 控件本身, 然后, 把属性存到 normalColor 属性中.

然而在什么时候将自己注册为观察者这一问题, 又使我放弃了这一解决方案. 最终选择 方法调剂 来解决原有 color 的存储问题.

使用方法调剂为原有属性的 setter 方法添加钩子, 在方法调用之前, 将属性存储起来, 用于切换回 normal 模式时, 为属性赋值.

这是要与 setter 调剂的钩子方法:

1 - (void)hook_setBackgroundColor:(UIColor*)backgroundColor {
2     if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {
3         [self setNormalBackgroundColor:backgroundColor];
4     }
5     [self hook_setBackgroundColor:backgroundColor];
6 }

如果当前是 normal 模式, 就会存储 color , 如果不是就会直接赋值, 如果你看不懂为什么这里好像会造成无限递归, 请看 这里 , 详细的解释了方法调剂是如何使用的.

DKNightVersionManager 实现 color 切换

我们已经为 UIKit 控件添加了 normalColornightColor , 接下来我们需要实现 color 在这两者之间的切换, 而这 DKNightVersionManager 就是为了处理模式切换的类.

通过为 DKNightVersionManager 创建一个单例来处理 模式转换 , 使用默认颜色 , 动画时间 等操作.

当调用 DKNightVersionManager 的类方法 nightFalling 或者 dawnComing 时, 我们首先会获取全局的 UIWindow , 然后通过递归调用 changeColor 方法, 使能够响应 changeColor 方法的视图改变颜色.

 1 - (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object {
 2   if ([object respondsToSelector:@selector(changeColor)]) {
 3     [object changeColor];
 4   }
 5   if ([object respondsToSelector:@selector(subviews)]) {
 6     if (![object subviews]) {
 7       // Basic case, do nothing.
 8       return;
 9     } else {
10       for (id subview in [object subviews]) {
11         // recursice darken all the subviews of current view.
12         [self changeColor:subview];
13         if ([subview respondsToSelector:@selector(changeColor)]) {
14           [subview changeColor];
15         }
16       }
17     }
18   }
19 }

因为我在这个类中并没有引入 category , 编译器不知道 id 类型具有这两个方法. 所以我声明了一个协议, 使 changeColor 中的方法来满足两个方法 changeColorsubViews . 不让编译器提示错误.

@protocol DKNightVersionChangeColorProtocol <NSObject>

- (void)changeColor;
- (NSArray *)subviews;

@end

然后让所有的 UIKit 控件遵循这个协议就可以了, 当然我们也可以不显示的遵循这个协议, 只要它能够响应这两个方法也是可以的.

实现默认颜色

我们要在 DKNightVersion 实现默认的夜间模式配色, 以便减少开发者的工作量.

但是因为我们对每种 color 只在父类中实现一次, 这样使得子类能够继承父类的实现, 但是同样 不想让 UIKit 系子类继承父类的默认颜色 .

- (UIColor *)defaultNightBackgroundColor {
  BOOL notUIKitSubclass = [self isKindOfClass:[UIView class]] && ![NSStringFromClass(self.class) hasPrefix:@"UI"];
  if ([self isMemberOfClass:[UIView class]] || notUIKitSubclass) {
    return UIColorFromRGB(0x343434);
  } else {
    UIColor *resultColor = self.normalBackgroundColor ?: [UIColor clearColor];
    return resultColor;
  }
}

通过使用 isMemberOfClass: 方法来判断 实例是不是当前类的实例, 而不是该类子类的实例. 然后才会返回默认的颜色. 但是非 UIKit 中的子类是可以继承这个特性的, 所以使用这段代码来判断该实例是否是非 UIKit 的子类:

[self isKindOfClass:[UIView class]] && ![NSStringFromClass(self.class) hasPrefix:@"UI"]

我们通过 NSStringFromClass(self.class) hasPrefix:@"UI" 巧妙地达到这一目的.

使用 erb 生成 Objective-C 代码

这个框架大多数的工作都是重复的, 但是我并不想为每一个类重复编写近乎相同的代码, 这样的代码十分不易阅读和维护, 所以使用了 erb 文件, 来为生成的 Objective-C 代码提供模板, 只将原数据进行解析然后传入每一个模板, 动态生成所有的代码, 再通过另一个脚本将所有的文件加入目录中.

DKNightVersion 的实现并不复杂. 它不仅使用了 erb 和 Ruby 脚本来减少了大量的工作量, 而且使用了 objc/runtime 的特性来魔改 UIKit 组件, 达到为 iOS 应用添加夜间模式的效果.

时间: 2024-10-10 14:32:24

iOS开发——使用技术OC片&换肤处理(白天与黑夜)的相关文章

iOS开发——高级技术OC篇&amp;运行时(Runtime)机制

运行时(Runtime)机制 本文将会以笔者个人的小小研究为例总结一下关于iOS开发中运行时的使用和常用方法的介绍,关于跟多运行时相关技术请查看笔者之前写的运行时高级用法及相关语法或者查看响应官方文档. 下面就来看看什么是运行时,我们要怎么在iOS开发中去使用它. 官方介绍: 这里我们主要关注的是最后一种! 下面来看看Runtime的相关总结 #pragma mark 获取属性成员 /********************************************************

iOS开发——使用技术OC篇&amp;项目实战总结之开发技巧

项目实战总结之开发技巧 本文收集了25个关于可以提升程序性能的提示和技巧 1.使用ARC进行内存管理 2.在适当的情况下使用reuseIdentifier 3.尽可能将View设置为不透明(Opaque) 4.避免臃肿的XIBs 5.不要阻塞主线程 6.让图片的大小跟UIImageView一样 7.选择正确的集合 8.使用GZIP压缩 9.重用和延迟加载View 10.缓存.缓存.缓存 11.考虑绘制 12.处理内存警告 13.重用花销很大的对象 14.使用Sprite Sheets 15.避免

iOS开发——实战技术OC篇&amp;应用帮助界面简单实现

应用帮助界面简单实现 有的时候我们可能遇到一些关于应用中的技术或者信息不够明确,所以在使用应用的时候可能会遇到一些不知道所措的情况,比如关于一些难一点的游戏应用的操作,关于应用中一些比较隐藏的功能疑惑是操作完成后需要用户去根据需求做其他相应操作的的功能,所以这里我们就实现一个简单的帮组界面,也许跟实际开发中有一些区别,但是思路理解基本上就没有什么问题了. 我们知道平时帮助界面来说,可能是使用一个掩饰案例,可能是网络连接,也能是说明文档,这里我们就简单一点使用网页结合JSON数据来实现,网页实现也

iOS开发——使用技术OC篇&amp;保存(获取)图片到(自定义)相册

保存(获取)图片到(自定义)相册 最近在学 iOS相关技术(绘图篇实现画板功能)的时候设计到了两个常用的知识点,那就是保存图片到相册和葱相册中获取图片. 只是个人比较好奇拓展一些技术,说的难听点叫做装牛角尖,好听点就是为了装逼而已,所以在保存相册的时候使用真及测试发现不能保存到我iPhone里 main的自定义相册里面,就查看文档和资料,也借鉴别人的分享实现了想要的功能,就把他给记录下来,这个虽然没有直接保存和获取常用但是也是一项很好的实用技术. 一:首先来看看怎么获取相册的图片: 1 // 弹

iOS开发——使用技术OC篇&amp;简单九宫格锁屏功能的实现与封装

简单九宫格锁屏功能的实现与封装 首先来看看最后的实现界面. 在这开始看下面的内容之前希望你能先大概思考活着回顾一下如果 你会怎么做,只要知道大概的思路就可以. 由于iphone5指纹解锁的实现是的这个功能呗淘汰,但是你可能会在想,都淘汰了你还在这里说个毛线啊,其实大家都知道,编程注重的思想,当然会了这个你不可能就会了指纹技术,哪还得等笔者在后面的学习中给大家分享,只是或许有一天这种功能或者思路在哪里要用到你不觉得是一件很开心的事情吗,而且如果你是不想自己敲的话直接可以拿来用. 好了不多废话直接上

iOS开发——使用技术OC篇&amp;剪切版的实现

剪切版的实现 在iOS中,可以使用剪贴板实现应用程序之中以及应用程序之间实现数据的共享.比如你可以从iPhone QQ复制一个url,然后粘贴到safari浏览器中查看这个链接的内容. 一.在iOS中下面三个控件,自身就有复制-粘贴的功能: 1.UITextView 2.UITextField 3.UIWebView 二.UIKit framework提供了几个类和协议方便我们在自己的应用程序中实现剪贴板的功能. 1.UIPasteboard:我们可以向其中写入数据,也可以读取数据 2.UIMe

iOS开发——使用技术OC篇&amp;视频和音频简单总结

视频和音频简单总结 1.音效播放(短时间的音频文件) 1> AudioServicesCreateSystemSoundID 2> AudioServicesPlaySystemSound 2.音乐播放(长时间的音频文件)1> AVAudioPlayer 只能播放本地的音频文件 >MPMusicPlayerControllerm 3.视频播放 1> AVPlayer(也可以播放音频) 能播放本地.远程的音频.视频文件 基于Layer显示,得自己去编写控制面板 2> MP

iOS开发——实战技术OC篇&amp;产品推荐界面的实现

产品推荐界面的实现 目前很多公司都仅仅只有一个项目,所以他们关于其他的项目肯定会做相应的推广,而在APp内部使用产品推荐实现APP的推广使一种很常见的现象,因为不需要什么成本,所以今天就简单介绍一下这么去搭建一个简单的产品推荐界面. 本章重点: 1:使用JSON数据:将JSON数据序列化,然后显示到界面上 2:根据设备是否安装对应的应用显示不同的信息,比如图片 3:根据设备是否安装对应的应用点击对应的按钮实现不同的功能 好了下面就来看看关于产品推荐界面是怎么去实现的! 1:JSON数据 2:对应

iOS开发——实战技术OC篇&amp;点击状态栏ScrollView(包括子控件)自动滚到顶部

点击状态栏ScrollView(包括子控件)自动滚到顶部 其实这种方式我们平时见的还是比较多的,而且适合用户的需求,所以就搬来琢磨了一下,感觉效果还不错 这里就直接将解决思路一一写出来不将代码分段展示了,在代码中我加了详细的注释objective-c的套路和swift基本一样,在最后会将Swift和objective-c的代码一起放上,如果需要直接解决问题的童鞋可以直接将代码拷贝到工程里即可 首先创建一个topWindow继承至NSObject,这里我们考虑将这个功能完全封装起来,所以所有的方法