项目介绍
FluentDarkModeKit was designed and developed before Apple‘s official dark mode release. It provides a mechanism to support dark mode for apps on iOS 11+ (including iOS 13).
FluentDarkModeKit是在苹果正式发布黑暗模式之前设计和开发的。它为iOS 11+上的应用程序(包括iOS 13)提供了一种支持暗模式的机制。
架构图
源码理解
该源码主要分为2部分
DarkModeCore
- 实现动态颜色初始化和获取
- 实现动态图片初始化和获取
- 简化方法交换流程
- 为swift提供命名空间,防止和UIColor和UIImage冲突
- 为UIColor、UIImage、UIView添加扩展,实现初始化方法等
- 类pch头文件引入
FluentDarkModeKit
- 黑暗模式管理器
- 初始化模式,做方法交换的工作
- 更新当前所有显示的视图的mode
- 扩展
- 为view及其子类绑定dynamicProperty和更新方法
- 为controller等提供dmTraitCollectionDidChange的处理方法
简单介绍每个文件
DarkModeCore
FluentDarkModeKit供外部调用 引入了头文件 定义了版本号和版本描述DMDynamicColor创建DMDynamicColorProxy类继承自NSProxy 实现初始化方法 动态获取当前主题色 实现NSProxy必须实现的消息转发方法 判断某类型是否是当前定义类型 为copy和copyWithZones返回当前队形DMDynamicImageDMDynamicImageProxy动态提供适用于当前主题的UIImage对象 其中NSProxy的使用与Color基本一致 对于 UIImage 的方法中返回值为 UIImage 的,DMDynamicImageProxy 都进行了实现,目的就是当 UIImage 在调用这些方法时,返回的类型依然为 DMDynamicImageProxy。 如:resizable、imageWithRenderingMode等 同样实现copy和copyWithZoneDMTraitCollention全局的特征集合 即当前用户的style是什么 枚举:未指明、亮、暗 三种模式 定义协议DMTraitEnviroment 用来更新界面NSObject+DarkModeKit提供方法交换的简单调用方式UIColor+DarkModeKit提供ios和swift的创建对象方式UIImage+DarkModeKit同上 在iOS 13上,UIImage'isEqual:'在内部发生了一些变化, 但对'NSProxy'不起作用, 在这里,我们手动将消息转发到内部图像 实现 - (BOOL)dm_isEqual:(UIImage *)other {}UIView+DarkModeKit绑定dm_dynamicBackgroundColor属性给UIView 交换设置背景色的方法实现
FluentDarkModeKit
DarkModeManager- Extensions
UIView+DarkModeKit实现setup方法 方法交换 实现更新属性 扩展DMTraitEnviroment 实现updateAppearance方法 遍历传入视图所有子视图 更新界面UIToolBar+DarkModeKit重写dm_updateDynamicColors方法 super.dm_updateDynamicColors() 实现barTintColor的动态替换UIButton+DarkModeKit重写dm_updateDynamicColors方法 super.dm_updateDynamicColors() 实现setTitleColor的动态替换UIWindow+DarkModeKit实现dmTraitCollectionDidChange方法 根视图rootViewController也调用dmTraitCollectionDidChange进行更新UIProgressView+DarkModeKit重写dm_updateDynamicColors方法 super.dm_updateDynamicColors() 实现preogressTintColor和trackTintColor的动态替换UIScrollView+DarkModeKit重写dm_updateDynamicColors方法 super.dm_updateDynamicColors() indicatorStyle的动态返回UITextView+DarkModeKit重写dm_updateDynamicColors方法 super.dm_updateDynamicColors() keyboardAppearance模式的动态返回,决定键盘状态UIImageView+DarkModeKit为UIImageView绑定一个dm_dynamicImage属性 交换初始化init方法为dm_init 交换设置图片方法setImage为dm_setImage 实现dm_updateDynamicImages方法 super.dm_updateDynamicImages() 如果有dynamicImage则将image设置为dynamicImageUIPageControl+DarkModeKit重写dm_updateDynamicColors()方法 为pageIndicatorTintColor和currentPageIndicatorTintColor 返回动态的dynamicColorUITableView+DarkModeKit重写dm_updateDynamicColors()方法 为sectionIndexColor和separatorColor 返回动态的dynamicColorUINavigationBar+DarkModeKit重写dm_updateDynamicColors()方法 为barTintColor 返回动态的dynamicColoUITabBar+DarkModeKitextension UITabBar{} 重写dmTraitCollectionDidChange方法 因为tabbarController一般是根界面,上面还有好多其他Controller 除了实现super方法,还要遍历所有item进行动态的图片及文字颜色更换 super.dmTraitCollectionDidChange(previousTraitCollection) items?.forEach { $0.dmTraitCollectionDidChange(previousTraitCollection) } extension UITabBarItem: DMTraitEnviroment{} 为item绑定两个新属性:dm_dynamicImage、dm_dynamicSelectedImage 交换setImage方法、交换setSelectImage方法, 为他们提供dynamicImage的设置及获取方式UIViewController+DarkModeKit实现dmTraitCollectionDidChange()方法 设置状态栏变化 如果有present的Controller,那么调用presentController.dmTraitCollectionDidChange() 所有children界面也都调用dmTraitCollectionDidChange() 如果当前controller正在展示,则view调用dmTraitCollectionDidChange()UILabel+DarkModeKit实现dmTraitCollectionDidChange()方法 交换swizzleDidMoveToWindowOnce方法, 当展示的时候动态更换颜色 绑定currentUserInterfaceStyle属性 实现updateDynamicColorInAttributedText()方法 用来修改attributedText。为什么呢?因为如下: 在iOS 11中,setNeedsDisplay()不能完全完成文字绘制 如果设置UILabel.attributeText,则UILabel.text color将是属性文本索引0处的前景色 如果设置UILabel.text color,则整个属性文本将获得前景色 因此,如果一个标签有两个或多个颜色属性字符串,我们不能简单地重置文本颜色 我们只是更新属性文本.幸运的是,我们只需要在iOS 11中这样做。UISlider+DarkModeKit重写dm_updateDynamicColors()方法 为minimumTrackTintColor和maximumTrackTintColor提供dynamicColorUITextField+DarkModeKit重写dm_updateDynamicColors()方法 为textColor提供dynamicColor 动态修改keyboardAppearace属性 扩展UITextField{} UITextfield在他的视线里不会调用super.willMove(toWindow;) 所以我们需要交换他的实现方法,来调用willMoveUIApplication+DarkModeKit继承协议DMTraitEnvironment 实现dmTraitCollectionDidChange方法 遍历windows的所有子界面,调用dmTraitCollectionDidChange()状态变更方法
补充知识讲解
NSProxy
NSProxy: 是一个抽象基类,它为一些表现的像是其它对象替身或者并不存在的对象定义API。
一般的,发送给代理的消息被转发给一个真实的对象或者代理本身引起加载(或者将本身转换成)一个真实的对象。
NSProxy的基类可以被用来透明的转发消息或者耗费巨大的对象的lazy 初始化。
NSProxy实现了包括NSObject协议在内基类所需的基础方法,但是作为一个抽象的基类并没有提供初始化的方法。
它接收到任何自己没有定义的方法他都会产生一个异常,
所以一个实际的子类必须提供一个初始化方法或者创建方法,
并且重载forwardInvocation:方法和methodSignatureForSelector:方法来处理自己没有实现的消息。
一个子类的forwardInvocation:实现应该采取所有措施来处理invocation,比如转发网络消息,或者加载一个真实的对象,并把invocation转发给他。
methodSignatureForSelector:需要为给定消息提供参数类型信息,子类的实现应该有能力决定他应该转发消息的参数类型,并构造相对应的NSMethodSignature对象。
详细信息可以查看NSDistantObject, NSInvocation, and NSMethodSignature的类型说明。
相信看了这些描述我们应该能对NSProxy有个初步印象,它仅仅是个转发消息的场所,至于如何转发,取决于派生类到底如何实现的。
比如我们可以在内部hold住(或创建)一个对象,然后把消息转发给该对象。那我们就可以在转发的过程中做些手脚了。
甚至也可以不去创建这些对象,去做任何你想做的事情,但是必须要实现他的forwardInvocation:和methodSignatureForSelector:方法。
消息转发机制
当程序运行时调用一个没有实现的方法,会采用三个消息转发步骤如果这三个步骤都不能成功那么此时程序会抛出一个异常。
- 添加方法到类对象,对于实例方法调用respondsToSelector:,对于类方法调用resolveClassMethod:。
+ (BOOL)resolveClassMethod:(SEL)sel; - (BOOL)respondsToSelector:(SEL)aSelector; - 查找forwardingTargetForSelector:方法,该方法返回一个新对象,如果返回nil那么将跳转到下一步骤。
- (id)forwardingTargetForSelector:(SEL)aSelector; - 通过methodSignatureForSelector:方法获取一个NSMethodSignature类型的对象,调用forwardInvocation:方法。改方法传入一个封装了NSMethodSignature的NSInvocation对象。然后该对象通过invakeWithTarget:方法将消息转发给其它对象。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; - (void)forwardInvocation:(NSInvocation *)invocation;
NSProxy 和 NSObject 的比较
相比NSObject,NSProxy更轻量级, 做消息转发效率更高.
NSObject寻找方法顺序:本类->(父类-)>(动态方法解析)-> 消息转发;
NSproxy顺序:本类->消息转发
NSProxy的用途
- AOP面向切片编程:iOS中面向切片编程一般有两种方式 ,一个是直接基于runtime 的method-Swizzling.还有一种就是基于NSProxy
- 解决NSTimer, CADisplayLink等强引用target引起的无法释放问题。如NSTimer:利用消息转发来断开NSTimer对象与视图之间的强引用关系。初始化NSTimer时把触发事件的target替换成一个单独的对象,然后这个对象中NSTimer的SEL方法触发时让这个方法在当前的视图self中实现。
- 多重继承,实现类似CAAnimation类族.
如何使用
UILabel
Swift
extension UIColor {
init(_: DMNamespace, light: UIColor, dark: UIColor)
}
let color = UIColor(.dm, light: .white, dark: .black)
Objective-C
@interface UIColor (FluentDarkModeKit)
- (UIColor *)dm_colorWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor;
@end
UIColor *color = [UIColor dm_colorWithLightColor:UIColor.whiteColor darkColor:UIColor.blackColor];
Images
Swift
extension UIImage {
init(_: DMNamespace, light: UIImage, dark: UIImage)
}
let lightImage = UIImage(named: "Light")!
let darkImage = UIImage(named: "Dark")!
let image = UIImage(.dm, light: lightImage, dark: darkImage)
Objective-C
@interface UIImage (FluentDarkModeKit)
- (UIImage *)dm_imageWithLightImage:(UIImage *)lightImage darkImage:(UIImage *)darkImage;
@end
在Objective-C中如何集成和使用
众所周知,国内在使用Swift的进度上一直很喜人。
绝大部分项目都是由OC构建为主;
接下来我们探讨一下如何在OC项目中集成该组件。
纯OC项目
- 新建一个swift文件,这时候会同步创建一个
xxx-Bridging-Header.h文件 - 修改Target的BuildSettings中
Defines Module为Yes
Podfile
修改Podfile
target 'dmSample' do
# Comment the next line if you don't want to use dynamic frameworks
# 我们这里是swift混编项目所以,需要使用这个动态库标识
use_frameworks!
# Pods for dmSample
pod "FluentDarkModeKit"
end
在终端调用
cd xxx文件夹
pod install
新增DarkModeInit.swift文件
import Foundation
import FluentDarkModeKit
class DarkModeInit: NSObject {
@objc func useToInit() {
DarkModeManager.setup()
}
}
在AppDelegate中调用初始化方式
首先引入xxx-Swift.h,可以在BuildSetting中看到
#import "dmSample-Swift.h"
在DidFinishLanuchingWithOptions中调用初始化
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[DarkModeInit new] useToInit];
return YES;
}
实现一个简单得到界面
#import "ViewController.h"
// 引入iOS
#import <FluentDarkModeKit/FluentDarkModeKit.h>
// 引入swift调用
@import FluentDarkModeKit;
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor dm_colorWithLightColor:[UIColor whiteColor] darkColor:[UIColor lightGrayColor]];
UILabel *showLabel = [UILabel new];
showLabel.frame = CGRectMake(30, 88, 300, 200);
showLabel.text = @"深色模式使用的很舒服哈罗闪蝶恋蜂狂哈迪斯发了哈开始的合法ad控件是否雷克萨货到付款哈市的";
showLabel.numberOfLines = 0;
// 设置颜色
showLabel.textColor = [UIColor dm_colorWithLightColor:[UIColor blackColor] darkColor:[UIColor redColor]];
[self.view addSubview:showLabel];
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(30, 200, 100, 100)];
[btn setTitle:@"切换" forState:UIControlStateNormal];
[btn setTitleColor:[UIColor dm_colorWithLightColor:[UIColor grayColor] darkColor:[UIColor blueColor]] forState:UIControlStateNormal];
[self.view addSubview:btn];
[btn addTarget:self action:@selector(changeStyle) forControlEvents:UIControlEventTouchUpInside];
}
- (void)changeStyle {
NSLog(@"切换模式");
if (DMTraitCollection.currentTraitCollection.userInterfaceStyle == DMUserInterfaceStyleDark) {
DMTraitCollection.currentTraitCollection = [DMTraitCollection traitCollectionWithUserInterfaceStyle:DMUserInterfaceStyleLight];
} else {
DMTraitCollection.currentTraitCollection = [DMTraitCollection traitCollectionWithUserInterfaceStyle:DMUserInterfaceStyleDark];
}
// 更新界面
[DarkModeManager updateAppearanceFor:[UIApplication sharedApplication] animated:YES];
}
@end
补充:
如何支持iOS9
作者这两天在基于这个框架进行主题定制、暗黑适配的开发。
目前寺内很多项目是最低版本支持到9.而微软这套推荐iOS10+
所以笔者仔细查看了源码,不支持iOS9的是因为项目内使用了
UIViewPropertyAnimation这个API是iOS10以后才可以使用。
具体用处是在DarkModeManager.swift文件内。我们只需要将此文件修改为UIViewAnimation代替即可。
然后在自己寺内的gitlab或其它地方,将podspec进行修改,上传到自己的公有库或私有库,即可完成对iOS9的支持;
如果有的同学想知道如何适配iOS8或更早的版本!
虽然以上可以支持iOS9的项目集成,但是笔者测试之后发现,大家还是最好做个版本限制;
因为Apple在iOS11之后对很多视图进行了层级上面的修正。如果你的团队没有大量的时间进行定制化开发,听微软的!
if @avaliable(ios11, *) {
play.time!!!!
}
同学,劝劝老板放弃老版本用户吧。因为他们不喜欢新时代!
修改的源码如下:
// MARK: -
extension DMTraitEnvironment {
/// Trigger `themeDidChange()`.
///
/// - Parameters:
/// - views: Views visiable by user, will be snapshoted if use animation.
/// - animated: Use animation or not.
fileprivate func updateAppearance(with views: [UIView], animated: Bool) {
assert(Thread.isMainThread)
if animated {
var snapshotViews: [UIView] = []
views.forEach { view in
guard let snapshotView = view.snapshotView(afterScreenUpdates: false) else {
return
}
view.addSubview(snapshotView)
snapshotViews.append(snapshotView)
}
dmTraitCollectionDidChange(nil)
UIView.animate(withDuration: 0.25, animations: {
snapshotViews.forEach { $0.alpha = 0}
}) { _ in
snapshotViews.forEach { $0.removeFromSuperview() }
}
// 10.0才有的新api,动画会更顺畅,但是支持效果比较低。
// UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.25, delay: 0, options: [], animations: {
// snapshotViews.forEach { $0.alpha = 0 }
// }) { _ in
// snapshotViews.forEach { $0.removeFromSuperview() }
// }
}
else {
dmTraitCollectionDidChange(nil)
}
}
}