FluentDarkModeKit is a framework developed by Microsoft for iOS Dark mode.
use
Basic usage
UIColor
Swift
extension UIColor {
init(_: DMNamespace, light: UIColor, dark: UIColor)}let color = UIColor(.dm, light: .white, dark: .black)
Copy the code
Objective-C
@interface UIColor (FluentDarkModeKit)
- (UIColor *)dm_colorWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor;
@end
UIColor *color = [UIColor dm_colorWithLightColor:UIColor.whiteColor darkColor:UIColor.blackColor];
Copy the code
UIImage
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)
Copy the code
Objective-C
@interface UIImage (FluentDarkModeKit)
- (UIImage *)dm_imageWithLightImage:(UIImage *)lightImage darkImage:(UIImage *)darkImage;
@end
Copy the code
other
FluentDarkModeKit and Apple operation types (slightly different). FluentDarkModeKit contain a global DMTraitCollection, in the custom layout can through DMTraitCollection. The current visit.
FluentDarkModeKit notifies the views and View controllers on the current window when the theme changes using the following proxy methods.
Swift
protocol DMTraitEnvironment: NSObjectProtocol {
func dmTraitCollectionDidChange(_ previousTraitCollection: DMTraitCollection?)
}
Copy the code
Objective-C
@protocol DMTraitEnvironment <NSObject>
- (void)dmTraitCollectionDidChange:(nullable DMTraitCollection *)previousTraitCollection;
@end
Copy the code
Realize the principle of
Let’s analyze the concrete implementation of FluentDarkModeKit.
NSProxy
NSProxy is one of the few types that does not inherit from NSObject.
In this framework, NSProxy hosts different colors and different images in both modes.
Color UIColor
FluentDarkModeKit declares the DMDynamicColor class,
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
Copy the code
In the.h file, we can see that DMDynamicColor inherits UIColor, but in the.m file, we can see that it really creates a DMDynamicColorProxy.
@interface DMDynamicColorProxy : NSProxy <NSCopying>
- (UIColor *)initWithLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor {
return (DMDynamicColor *)[[DMDynamicColorProxy alloc] initWithLightColor:lightColor darkColor:darkColor];
}
@end
Copy the code
DMDynamicColorProxy inherits from NSProxy and forwards all events to the resolvedColor, which is a lightColor or darkColor returned based on the current system pattern. In this way, DMDynamicColorProxy behaves as a UIColor and can return the corresponding color according to the system mode.
@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
Copy the code
Note: For UIColor methods that return UIColor, DMDynamicColorProxy is implemented so that when UIColor calls these methods, the return type is still DMDynamicColorProxy.
Image UIImage
DMDynamicImageProxy is declared, and the resolvedImage returns lightImage or darkImage based on the current mode.
@interface DMDynamicImageProxy : NSProxy
@property (nonatomic.readonly) UIImage *resolvedImage;
- (instancetype)initWithLightImage:(UIImage *)lightImage darkImage:(UIImage *)darkImage;
@end
Copy the code
In the specific implementation, DMDynamicImageProxy also forwards events to resolvedImage, so that the expression of DMDynamicImageProxy is UIImage from the outside world, but it can return different images according to the current mode.
Note: For UIImage methods that return UIImage, DMDynamicImageProxy is implemented so that when UIImage calls these methods, the type returned is still DMDynamicImageProxy.
Alternative Setting method
Let’s look at a small test, the same color (actual type DMDynamicColorProxy) assigned to the view’s backgroundColor and the button’s titleColor, and then compared with the original color, is the result equal?
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")}Copy the code
Output:
not equal
equal
Copy the code
In other words, the color is assigned to the same value, but Apple handles it differently. Some of the values are consistent with the assigned value, while others are not. (some assignment will copy the color)
If you use DMDynamicColorProxy to assign a color and then take it out with a UIColor type, it loses its lightColor and darkColor. This property setting needs to be saved when the DMDynamicColorProxy is set.
So FluentDarkModeKit replaces these properties, such as setTintColor:
extension UIView {
private struct Constants {
static var dynamicTintColorKey = "dynamicTintColorKey"
}
// Convert the setter: tintColor method
// Record dm_dynamicTintColor when setting
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)
}
}
Copy the code
Alternatives to other methods
willMove(toWindow:)
The view displayed on the page can be obtained layer by layer through subviews, and then the color can be changed according to the current mode. For views that are not displayed on the page, you can only update the colors and images corresponding to the current mode when you add them to the window by replacing the willMove(toWindow) method.
extension UIView {
// Call 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)
ifwindow ! =nil {
dm_updateDynamicColors()
dm_updateDynamicImages()
}
}
}
Copy the code
setBackgroundColor
The replacement setBackgroundColor is a little special. The replacement code is as follows:
@implementation UIView (DarkModeKit)
static void (*dm_original_setBackgroundColor)(UIView *, SEL.UIColor *);
/// Set the background color
static void dm_setBackgroundColor(UIView *self.SEL _cmd, UIColor *color) {
/ / record
if ([color isKindOfClass:[DMDynamicColor class]]) {
self.dm_dynamicBackgroundColor = (DMDynamicColor *)color;
} else {
self.dm_dynamicBackgroundColor = nil;
}
/ / set
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
Copy the code
The namespace
FluentDarkModeKit extends the initialization methods of UIColor and UIImage. To avoid conflicts, add the prefix DM_ in object-c, and in swift, A custom enumeration DMNamespace parameter was added before the initialization method.
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
Copy the code
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
Copy the code
In the object-c code, use #if __swift__ to determine the compilation environment and NS_SWIFT_NAME(init(_:light:dark:)) to specify the aspect name in SWIFT.
Note: This form does not function as a namespace. In code, you can still define the same methods:
import FluentDarkModeKit
extension UIColor {
convenience init(_ name: DMNamespace, light: UIColor, dark: UIColor) {
self.init(white: 0, alpha: 1.0)}}Copy the code
This overrides the methods in the FluentDarkModeKit framework. Although you don’t do that in actual programming.