FluentDarkModeKit 框架是微軟開發的一個用於適配 iOS 暗黑模式的框架。git
UIColorgithub
Swift編程
extension UIColor {
init(_: DMNamespace, light: UIColor, dark: UIColor)
}
let color = UIColor(.dm, light: .white, dark: .black)
複製代碼
Objective-Cswift
@interface UIColor (FluentDarkModeKit)
- (UIColor *)dm_colorWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor;
@end
UIColor *color = [UIColor dm_colorWithLightColor:UIColor.whiteColor darkColor:UIColor.blackColor];
複製代碼
UIImageapp
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
複製代碼
FluentDarkModeKit
和 Apple 的操做類型(有些許差別)。FluentDarkModeKit
包含一個全局的 DMTraitCollection
,在自定義的佈局中能夠經過 DMTraitCollection.current
來訪問。測試
當主題改變時, FluentDarkModeKit
經過下面的代理方法來通知當前的 window 上 views 和 view controllers。ui
Swiftatom
protocol DMTraitEnvironment: NSObjectProtocol {
func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?)
}
複製代碼
Objective-C
@protocol DMTraitEnvironment <NSObject>
- (void)dmTraitCollectionDidChange:(nullable DMTraitCollection *)previousTraitCollection;
@end
複製代碼
下面來分析一下 FluentDarkModeKit 的具體實現。
NSProxy 是少數不繼承自 NSObject 的類型。
在該框架中 NSProxy 承載了兩種模式下的不一樣顏色和不一樣圖片。
FluentDarkModeKit
聲明瞭DMDynamicColor
類,
NS_SWIFT_NAME(DynamicColor)
@interface DMDynamicColor : UIColor
@property (nonatomic, readonly) UIColor *lightColor;
@property (nonatomic, readonly) UIColor *darkColor;
- (instancetype)initWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor;
@end
複製代碼
在 .h 文件中,咱們能夠看出 DMDynamicColor 繼承子 UIColor,可是在 .m 中,咱們能夠看出它真正建立的是一個 DMDynamicColorProxy
。
@interface DMDynamicColorProxy : NSProxy <NSCopying>
- (UIColor *)initWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor {
return (DMDynamicColor *)[[DMDynamicColorProxy alloc] initWithLightColor:lightColor darkColor:darkColor];
}
@end
複製代碼
DMDynamicColorProxy 繼承自 NSProxy
,它將全部的事件轉發到 resolvedColor
,而 resolvedColor
是根據當前系統的模式返回的 lightColor
或者 darkColor
。這樣 DMDynamicColorProxy 對外的表現就是一個 UIColor,而且能夠根據系統的模式返回對應的顏色。
@interface DMDynamicColorProxy : NSProxy <NSCopying>
@property (nonatomic, strong) UIColor *lightColor;
@property (nonatomic, strong) UIColor *darkColor;
@property (nonatomic, readonly) UIColor *resolvedColor;
@end
@implementation DMDynamicColorProxy
// TODO: We need a more generic initializer.
- (instancetype)initWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor {
self.lightColor = lightColor;
self.darkColor = darkColor;
return self;
}
- (UIColor *)resolvedColor {
if (DMTraitCollection.currentTraitCollection.userInterfaceStyle == DMUserInterfaceStyleDark) {
return self.darkColor;
} else {
return self.lightColor;
}
}
// MARK: UIColor
- (UIColor *)colorWithAlphaComponent:(CGFloat)alpha {
return [[DMDynamicColor alloc] initWithLightColor:[self.lightColor colorWithAlphaComponent:alpha]
darkColor:[self.darkColor colorWithAlphaComponent:alpha]];
}
// MARK: NSProxy
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.resolvedColor methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.resolvedColor];
}
// MARK: NSObject
- (BOOL)isKindOfClass:(Class)aClass {
static DMDynamicColor *dynamicColor = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dynamicColor = [[DMDynamicColor alloc] init];
});
return [dynamicColor isKindOfClass:aClass];
}
// MARK: NSCopying
- (id)copy {
return [self copyWithZone:nil];
}
- (id)copyWithZone:(NSZone *)zone {
return [[DMDynamicColorProxy alloc] initWithLightColor:self.lightColor darkColor:self.darkColor];
}
@end
複製代碼
注意:對於 UIColor 的方法中返回值爲 UIColor 的,DMDynamicColorProxy 都進行了實現,目的就是當 UIColor 在調用這些方法時,返回的類型依然爲 DMDynamicColorProxy。
和顏色的實現原理同樣,也聲明瞭 DMDynamicImageProxy,由 resolvedImage 根據當前的模式返回 lightImage 或者 darkImage。
@interface DMDynamicImageProxy : NSProxy
@property (nonatomic, readonly) UIImage *resolvedImage;
- (instancetype)initWithLightImage:(UIImage *)lightImage darkImage:(UIImage *)darkImage;
@end
複製代碼
在具體的實現中,DMDynamicImageProxy 也是將事件轉發到 resolvedImage,這樣在外界看來 DMDynamicImageProxy 的表現就是 UIImage,可是能夠根據當前的模式返回不一樣的 Image。
注意:對於 UIImage 的方法中返回值爲 UIImage 的,DMDynamicImageProxy 都進行了實現,目的就是當 UIImage 在調用這些方法時,返回的類型依然爲 DMDynamicImageProxy。
咱們先來看一個小測試,同一個顏色(實際類型爲 DMDynamicColorProxy)賦值給 view 的 backgroundColor 和 button 的 titleColor 後,再和原來的顏色進行對比,結果是否相等?
let color = UIColor(.dm, light: .white, dark: .black)
view.backgroundColor = color
if view.backgroundColor == color {
debugPrint("equal")
} else {
debugPrint("not equal")
}
let button = UIButton()
button.setTitleColor(color, for: .normal)
if button.titleColor(for: .normal) == color {
debugPrint("equal")
} else {
debugPrint("not equal")
}
複製代碼
輸出:
not equal
equal
複製代碼
也就是說,一樣是給顏色進行賦值,可是 Apple 的處理是不同的,有的和被賦予的值一致,有的則不一致。(應該是有些賦值會對顏色進行拷貝)
若是使用 DMDynamicColorProxy 對一個顏色進行賦值,再取出時類型卻變成 UIColor 的,它就丟失了 lightColor 和 darkColor。對於這種屬性設置,須要在設置 DMDynamicColorProxy 時進行保存。
因此 FluentDarkModeKit
對這類的屬性進行了替換,例如 setTintColor
:
extension UIView {
private struct Constants {
static var dynamicTintColorKey = "dynamicTintColorKey"
}
// 轉化 setter: tintColor 的方法
// 設置的時候,記錄 dm_dynamicTintColor
static let swizzleSetTintColorOnce: Void = {
if !dm_swizzleInstanceMethod(#selector(setter: tintColor), to: #selector(dm_setTintColor)) {
assertionFailure(DarkModeManager.messageForSwizzlingFailed(class: UIView.self, selector: #selector(setter: tintColor))) } }() private var dm_dynamicTintColor: DynamicColor? {
get {
return objc_getAssociatedObject(self, &Constants.dynamicTintColorKey) as? DynamicColor
}
set {
objc_setAssociatedObject(self, &Constants.dynamicTintColorKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
@objc private dynamic func dm_setTintColor(_ color: UIColor) {
dm_dynamicTintColor = color as? DynamicColor
dm_setTintColor(color)
}
}
複製代碼
頁面上顯示的 view 能夠經過 subviews,一層一層的獲取到,而後根據當前的模式進行修改顏色。對於不在頁面上顯示的 view,只能經過替換 willMove(toWindow:)
方法,在添加到 window 時更新當前模式對應的顏色和圖片。
extension UIView {
// 調用 willMove(toWindow:) 的時候:
// 1. dm_updateDynamicColors
// 2. dm_updateDynamicImages
static let swizzleWillMoveToWindowOnce: Void = {
if !dm_swizzleInstanceMethod(#selector(willMove(toWindow:)), to: #selector(dm_willMove(toWindow:))) {
assertionFailure(DarkModeManager.messageForSwizzlingFailed(class: UIView.self, selector: #selector(willMove(toWindow:)))) } }() @objc private dynamic func dm_willMove(toWindow window: UIWindow?) {
dm_willMove(toWindow: window)
if window != nil {
dm_updateDynamicColors()
dm_updateDynamicImages()
}
}
}
複製代碼
替換 setBackgroundColor 有點特殊,替換代碼以下:
@implementation UIView (DarkModeKit)
static void (*dm_original_setBackgroundColor)(UIView *, SEL, UIColor *);
/// 設置背景色
static void dm_setBackgroundColor(UIView *self, SEL _cmd, UIColor *color) {
// 記錄
if ([color isKindOfClass:[DMDynamicColor class]]) {
self.dm_dynamicBackgroundColor = (DMDynamicColor *)color;
} else {
self.dm_dynamicBackgroundColor = nil;
}
// 設置
dm_original_setBackgroundColor(self, _cmd, color);
}
// https://stackoverflow.com/questions/42677534/swizzling-on-properties-that-conform-to-ui-appearance-selector
+ (void)dm_swizzleSetBackgroundColor {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method method = class_getInstanceMethod(self, @selector(setBackgroundColor:));
dm_original_setBackgroundColor = (void *)method_getImplementation(method);
method_setImplementation(method, (IMP)dm_setBackgroundColor);
});
}
- (DMDynamicColor *)dm_dynamicBackgroundColor {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setDm_dynamicBackgroundColor:(DMDynamicColor *)dm_dynamicBackgroundColor {
objc_setAssociatedObject(self,
@selector(dm_dynamicBackgroundColor),
dm_dynamicBackgroundColor,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
複製代碼
FluentDarkModeKit
對 UIColor 和 UIImage 的初始化方法進行了擴展,爲了不衝突,在 Object-C 中添加了 dm_
的前綴,在 swift 中,在初始化方法前面添加了一個自定義的枚舉 DMNamespace
參數。
UIColor
NS_ASSUME_NONNULL_BEGIN
@interface UIColor (DarkModeKit)
+ (UIColor *)dm_colorWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor
NS_SWIFT_UNAVAILABLE("Use init(_:light:dark:) instead.");
#if __swift__
+ (UIColor *)dm_namespace:(DMNamespace)namespace
colorWithLightColor:(UIColor *)lightColor
darkColor:(UIColor *)darkColor NS_SWIFT_NAME(init(_:light:dark:));
#endif
@end
NS_ASSUME_NONNULL_END
複製代碼
UIImage
NS_ASSUME_NONNULL_BEGIN
@interface UIImage (DarkModeKit)
+ (UIImage *)dm_imageWithLightImage:(UIImage *)lightImage darkImage:(UIImage *)darkImage
NS_SWIFT_UNAVAILABLE("Use init(_:light:dark:) instead.");
#if __swift__
+ (UIImage *)dm_namespace:(DMNamespace)namespace
imageWithLightImage:(UIImage *)lightImage
darkImage:(UIImage *)darkImage NS_SWIFT_NAME(init(_:light:dark:));
#endif
@end
NS_ASSUME_NONNULL_END
複製代碼
在 Object-C 的代碼中,經過 #if __swift__
來判斷編譯環境,經過 NS_SWIFT_NAME(init(_:light:dark:))
來指定在 swift 中的方面名稱。
注意:這種形式,並無起到命名空間的做用。在代碼中,依然能夠定義相同的方法:
import FluentDarkModeKit
extension UIColor {
convenience init(_ name: DMNamespace, light: UIColor, dark: UIColor) {
self.init(white: 0, alpha: 1.0)
}
}
複製代碼
這樣就覆蓋了 FluentDarkModeKit
框架中的方法。雖然在實際的編程中都不會這樣作。