IOS13 brings us to system-level dark mode.

It is a great pity that we failed to adapt systematically and comprehensively in our project at the first time.

We are still waiting for the output of relevant UI standards, but at the engineering and code level, we are ready.

Now, let’s get familiar with how to gracefully and systematically adapt DarkMode.

First, the formulation of standards

At the heart of DarkMode is color formulation.

We need to map the normal mode colors to DarkMode colors one by one.

Although the core is color, it also involves the conversion of pictures, and the essence of pictures is color.

This part of the work, mainly needs UI students to formulate.

Once our rules or standards have been formulated, the subsequent main work, the main energy is still in the normal mode.

Through the transformation rules, can be one-to-one to the dark mode.

Systems engineering

UITraitCollection

In iOS 13, we can use UITraitCollection to determine the current system mode. UIView and UIViewController, UIScreen, and UIWindow all comply with the UITraitEnvironment protocol, so each of these classes has a property called traitCollection.

When DarkMode switches back and forth with normal mode, the following methods are triggered according to the following rules:

The core UIColor complies with the following methods:

[UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) {
                return lightColor;
            }else {
                return darkColor;
            }
        }];
Copy the code

UIColor dynamic color blocks are called with the following conditions:

A dynamically switched block is called only when a UIColor object is assigned to the corresponding UIColor object.

Such as:

UIColor *backColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) {
                return lightColor;
            }else {
                return darkColor;
            }
        }];
self.view.backgroundColor = backColor;
Copy the code

For example, UIColor can be converted to CGColor, or images generated using UIColor can not be converted by UIColorDynamicProvider.

For system configuration, we can apply the following methods to dynamically change the content based on the pattern:

System color supported by iOS13

In iOS 13, Apple introduced a new system color, which is dynamic and adjusts dynamically depending on whether the current system is in default mode or dark mode.

Apple also offers a dynamic set of grayscale colors.

** System semantic color **iOS13 support

Assets configure iOS11 support

With Asset we can manage colors, and we can also manage images to achieve dynamic switching.

Third, our plan

According to the rules of the system, and in conjunction with our project, we need to make the following distinctions.

We manage the colors entirely in code and try not to use Asset.

I use the following three classes to manage our UI as a whole.

YGUIMannger

+ (BOOL)isDarkMode {if (@available(iOS 13.0, *)) { return (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark); } return NO; } /// member color gradient button + (UIButton *)vipGradientLayerBtn:(CGRect)frame {UIButton * BTN = [UIButton buttonWithType:UIButtonTypeSystem]; btn.frame = frame; [btn setTitleColor:RGB(0x784720) forState:UIControlStateNormal]; btn.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; btn.layer.cornerRadius = CGRectGetHeight(frame)/2; CAGradientLayer *gradient = [CAGradientLayer layer]; gradient.frame = btn.bounds; Gradient. The startPoint = CGPointMake (0, 0.5); Gradient. The endPoint = CGPointMake (1, 0.5); gradient.colors = [NSArray arrayWithObjects: (id)RGB(0xfcdeb4).CGColor, (id)RGB(0xdaba87).CGColor, nil]; gradient.cornerRadius = CGRectGetHeight(frame)/2; [btn.layer insertSublayer:gradient atIndex:0]; return btn; } @endCopy the code

This class is mainly used to write unified controls, as well as some unified methods.

YGColor

@implementation YGColor // All colors in the project must use this method + (YGColor *)colorWithNormalColor:(UIColor *)normalColor darkColor:(UIColor *)darkColor {if (! normalColor) { normalColor = [UIColor whiteColor]; } if (@available(iOS 13.0, *)) {if (! darkColor) { return (YGColor *)normalColor; } return (YGColor *)[UIColor colorWithDynamicProvider:^UIColor *_Nonnull (UITraitCollection *_Nonnull traitCollection) {  if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { return darkColor; } else { return normalColor; }}]; } else { return (YGColor *)normalColor; }} / / / diablo mode gradient + (YGColor *) colorWithGradientNormalColors: (gradientNormalColors NSArray *) gradientDarkColors:(NSArray *)gradientDarkColors { YGColor *color = [YGColor new]; if (! IS_ARRAY(gradientNormalColors)) { gradientNormalColors = [NSArray arrayWithObject:[UIColor whiteColor]]; } if (! IS_ARRAY(gradientDarkColors)) { gradientDarkColors = gradientNormalColors; } color.gradientNormalColors = gradientNormalColors; color.gradientDarkColors = gradientDarkColors; return color; } @endCopy the code

This class is mainly used to manage all colors in the project.

There are two main methods, one monochromatic and one gradient.

On this basis, two Define are defined for easy call:

#define kYGAllColor(nColor,dColor) [YGColor colorWithNormalColor:nColor darkColor:dColor]

#define kYGAllGradientColor(nColors,dColors) [YGColor colorWithGradientNormalColors:nColors gradientDarkColors:dColors]
Copy the code

With YGColor, you can create all the dynamic colors used in your project.

Category

@implementation UIView (YGUIManager)
+ (void)load {
    if (@available(iOS 13.0, *)) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Method presentM = class_getInstanceMethod(self.class, @selector(traitCollectionDidChange:));
            Method presentSwizzlingM = class_getInstanceMethod(self.class, @selector(dy_traitCollectionDidChange:));

            method_exchangeImplementations(presentM, presentSwizzlingM);
        });
    }
}

- (void)dy_traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
    if (self.didChangeTraitCollection) {
        self.didChangeTraitCollection(self.traitCollection);
    }
    [self dy_traitCollectionDidChange:previousTraitCollection];
}

- (void)setDidChangeTraitCollection:(void (^)(UITraitCollection *))didChangeTraitCollection {
    objc_setAssociatedObject(self, @"YGViewDidChangeTraitCollection", didChangeTraitCollection, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void (^)(UITraitCollection *))didChangeTraitCollection {
    return objc_getAssociatedObject(self, @"YGViewDidChangeTraitCollection");
}

/// 适配暗黑模式layer的back颜色,项目中必须通过此方法
- (void)setLayerBackColor:(YGColor *)color {
    @yg_weakify(self);
    [self setLayerColor:color changeColor:^(CGColorRef layerColor) {
        @yg_strongify(self);
        self.layer.backgroundColor = layerColor;
    }];
}

/// 适配暗黑模式layer的Border颜色,项目中必须通过此方法
- (void)setLayerBorderColor:(YGColor *)color {
    @yg_weakify(self);
    [self setLayerColor:color changeColor:^(CGColorRef layerColor) {
        @yg_strongify(self);
        self.layer.borderColor = layerColor;
    }];
}

/// 适配暗黑模式layer的shadow颜色,项目中必须通过此方法
- (void)setLayerShadowColor:(YGColor *)color {
    @yg_weakify(self);
    [self setLayerColor:color changeColor:^(CGColorRef layerColor) {
        @yg_strongify(self);
        self.layer.shadowColor = layerColor;
    }];
}

/// color一定是包含暗黑模式的color
- (void)setLayerColor:(YGColor *)color changeColor:(void (^)(CGColorRef layerColor))changeColor {
    if (@available(iOS 13.0, *)) {
        if (changeColor) {
            changeColor([color resolvedColorWithTraitCollection:self.traitCollection].CGColor);
        }
        self.didChangeTraitCollection = ^(UITraitCollection *traitCollection) {
            if (changeColor) {
                changeColor([color resolvedColorWithTraitCollection:traitCollection].CGColor);
            }
        };
    } else {
        // Fallback on earlier versions
        if (changeColor) {
            changeColor(color.CGColor);
        }
    }
}

/// color一定是包含暗黑模式的color
- (void)setGradientColor:(CAGradientLayer *)layer color:(YGColor *)color {
    if (@available(iOS 13.0, *)) {
        if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
            layer.colors = color.gradientDarkColors;
        } else {
            layer.colors = color.gradientNormalColors;
        }
        self.didChangeTraitCollection = ^(UITraitCollection *traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                layer.colors = color.gradientDarkColors;
            } else {
                layer.colors = color.gradientNormalColors;
            }
        };
    } else {
        layer.colors = color.gradientNormalColors;
    }
}

@end

@implementation UIImageView (YGUIManager)
/// 适配暗黑模式 使用路径生成的image
- (void)setImageWithNormalImagePath:(NSString *)normalImagePath darkImagePath:(NSString *)darkImagePath {
    if (!normalImagePath
        || [normalImagePath isEqualToString:@""]) {
        return;
    }
    UIImage *normalImage = [UIImage imageWithContentsOfFile:normalImagePath];
    if (@available(iOS 13.0, *)) {
        if (!darkImagePath
            || [darkImagePath isEqualToString:@""]) {
            darkImagePath = normalImagePath;
        }
        UIImage *darkImage = [UIImage imageWithContentsOfFile:darkImagePath];
        if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
            self.image = darkImage;
        } else {
            self.image = normalImage;
        }

        // UIImageView不会走traitCollectionDidChange
        UIView *superView = self.superview;
        if ([superView isKindOfClass:[UIImageView class]]) {
            superView = superView.superview;
        }
        @yg_weakify(self);
        superView.didChangeTraitCollection = ^(UITraitCollection *traitCollection) {
            @yg_strongify(self);
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                self.image = darkImage;
            } else {
                self.image = normalImage;
            }
        };
    } else {
        self.image = normalImage;
    }
}

/// 适配暗黑模式 使用颜色生成的image
- (void)setImageWithColor:(YGColor *)color {
    if (@available(iOS 13.0, *)) {
        self.image = [UIImage imageWithColor:[color resolvedColorWithTraitCollection:self.traitCollection]];
        // UIImageView不会走traitCollectionDidChange
        UIView *superView = self.superview;
        if ([superView isKindOfClass:[UIImageView class]]) {
            superView = superView.superview;
        }
        @yg_weakify(self);
        superView.didChangeTraitCollection = ^(UITraitCollection *traitCollection) {
            @yg_strongify(self);
            self.image = [UIImage imageWithColor:[color resolvedColorWithTraitCollection:traitCollection]];
        };
    } else {
        self.image = [UIImage imageWithColor:color];
    }
}

@end

@implementation UIButton (YGUIManager)
/// 适配暗黑模式 使用路径生成的image
- (void)setImageWithNormalImagePath:(NSString *)normalImagePath darkImagePath:(NSString *)darkImagePath forState:(UIControlState)state {
    if (!normalImagePath
        || [normalImagePath isEqualToString:@""]) {
        return;
    }
    UIImage *normalImage = [UIImage imageWithContentsOfFile:normalImagePath];
    if (@available(iOS 13.0, *)) {
        if (!darkImagePath
            || [darkImagePath isEqualToString:@""]) {
            darkImagePath = normalImagePath;
        }
        UIImage *darkImage = [UIImage imageWithContentsOfFile:darkImagePath];
        if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
            [self setImage:darkImage forState:state];
        } else {
            [self setImage:normalImage forState:state];
        }
        @yg_weakify(self);
        self.didChangeTraitCollection = ^(UITraitCollection *traitCollection) {
            @yg_strongify(self);
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                [self setImage:darkImage forState:state];
            } else {
                [self setImage:normalImage forState:state];
            }
        };
    } else {
        [self setImage:normalImage forState:state];
    }
}

/// 适配暗黑模式 使用颜色生成的image
- (void)setImageWithColor:(YGColor *)color size:(CGSize)size forState:(UIControlState)state {
    if (@available(iOS 13.0, *)) {
        [self setImage:[UIImage imageWithColor:[color resolvedColorWithTraitCollection:self.traitCollection] size:size] forState:state];
        @yg_weakify(self);
        self.didChangeTraitCollection = ^(UITraitCollection *traitCollection) {
            @yg_strongify(self);
            [self setImage:[UIImage imageWithColor:[color resolvedColorWithTraitCollection:traitCollection]] forState:state];
        };
    } else {
        [self setImage:[UIImage imageWithColor:color] forState:state];
    }
}

/// 适配暗黑模式 使用路径生成的image
- (void)setBackgroundImageWithNormalImagePath:(NSString *)normalImagePath darkImagePath:(NSString *)darkImagePath forState:(UIControlState)state {
    if (!normalImagePath
        || [normalImagePath isEqualToString:@""]) {
        return;
    }
    UIImage *normalImage = [UIImage imageWithContentsOfFile:normalImagePath];
    if (@available(iOS 13.0, *)) {
        if (!darkImagePath
            || [darkImagePath isEqualToString:@""]) {
            darkImagePath = normalImagePath;
        }
        UIImage *darkImage = [UIImage imageWithContentsOfFile:darkImagePath];
        if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
            [self setBackgroundImage:darkImage forState:state];
        } else {
            [self setBackgroundImage:normalImage forState:state];
        }
        @yg_weakify(self);
        self.didChangeTraitCollection = ^(UITraitCollection *traitCollection) {
            @yg_strongify(self);
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                [self setBackgroundImage:darkImage forState:state];
            } else {
                [self setBackgroundImage:normalImage forState:state];
            }
        };
    } else {
        [self setBackgroundImage:normalImage forState:state];
    }
}

/// 适配暗黑模式 使用颜色生成的image
- (void)setBackgroundImageWithColor:(YGColor *)color forState:(UIControlState)state {
    if (@available(iOS 13.0, *)) {
        [UIImage imageWithColor:[color resolvedColorWithTraitCollection:self.traitCollection] completion:^(UIImage *image) {
            [self setBackgroundImage:image forState:state];
        }];
        @yg_weakify(self);
        self.didChangeTraitCollection = ^(UITraitCollection *traitCollection) {
            @yg_strongify(self);
            [self setBackgroundImage:[UIImage imageWithColor:[color resolvedColorWithTraitCollection:traitCollection]] forState:state];
        };
    } else {
        [self setBackgroundImage:[UIImage imageWithColor:color] forState:state];
    }
}

@end
Copy the code

Among them, we use a METHOD swizzling of UIView, the main purpose is to change the method called by UIView switch DarkMode to Block, external call.

Mainly handle situations that cannot be processed dynamically using UIColor.

Based on this method, we created:

  1. UIView
  2. UIImageView
  3. UIButton

To handle situations where dynamic switching cannot be followed.

And the Color used must be YGColor, Color is always managed by YGColor.

Otherwise, we would have trouble handling this situation and would need traitCollectionDidChange on each top-level View to manage its subview transformation rules.

Write at the end

We believe that 80% of the cases in our APP can be covered by the above methods. Under certain standards, the code can be neat and elegantly switched with one key.

Let’s think!