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.