1. 程式人生 > IOS開發 >iOS DarkMode適配

iOS DarkMode適配

iOS13中為我們帶來了系統級別的暗黑模式。

而我們居然沒能第一時間,系統全面的在我們專案中適配,實在是一大遺憾。

現在我們依然再等待相關UI標準的輸出,但是在工程和程式碼層面,我們已經做好了準備。

下面,就讓我們來熟悉一下怎麼優雅又全面系統的適配DarkMode吧。

一、標準的制定

DarkMode的核心是顏色的制定。

我們需要將正常模式的顏色一一對應到DarkMode的顏色。

雖然核心是顏色,但是也牽扯到圖片的轉換,圖片的本質也是色彩。

此部分工作,主要需要UI同學來制定。

一旦將我們的規則,或者標準制定完成,那麼後續主要工作,主要精力依然在正常模式下。

通過轉換規則,則可一一對應到暗黑模式下。

二、系統工程

UITraitCollection

在 iOS 13 中,我們可以通過 UITraitCollection 來判斷當前系統的模式。UIView 和 UIViewController 、UIScreen、UIWindow 都已經遵從了UITraitEnvironment 這個協議,因此這些類都擁有一個叫做 traitCollection 的屬性。

當DarkMode和正常模式來回切換的時候,會按照以下規則觸發以下方法:

KDPfaQ.png

其中,核心的UIColor則會遵從以下方法:

[UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) {
                return lightColor;
            }else {
                return darkColor;
            }
        }];
複製程式碼

而UIColor動態顏色block的呼叫有以下條件:

只有當UIColor物件賦值給相應的UIColor物件時,才會呼叫動態切換的block。

如:

UIColor *backColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleLight) {
                return lightColor;
            }else {
                return darkColor;
            }
        }];
self.view.backgroundColor = backColor;
複製程式碼

而,例如UIColor轉換為CGColor,或者利用UIColor生成圖片的方式,是無法通過UIColorDynamicProvider轉換的。

對於系統配置來說,我們可以應用以下方法,來動態根據模式變化相關內容:

系統顏色 iOS13支援

在 iOS 13中,蘋果引入了全新系統顏色,系統顏色是動態的,會根據當前系統是預設模式還是暗黑模式動態調整顏色。

蘋果還提供了一組動態的灰度顏色。

KDP2qS.png

**系統語義化顏色 **iOS13支援

KDieJA.png

Assets 配置 iOS11支援

KDPWVg.png

通過Asset我們可以使用管理顏色,也可以管理圖片,來達到動態切換。

三、我們的方案

根據系統的規則,並且結合我們的工程,我們需要進行以下的區分。

我們管理顏色全部使用程式碼來進行,儘量不使用Asset。

KDPcKf.png

我使用了以下三個類,來整體管理我們的UI效果。

YGUIMannger

@implementation YGUIManager
/// 目前是否是暗黑模式
+ (BOOL)isDarkMode {
    if (@available(iOS 13.0,*)) {
        return (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark);
    }
    return NO;
}

/// 會員金色漸變按鈕
+ (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.startPoint = CGPointMake(0,0.5);
    gradient.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;
}

@end
複製程式碼

此類主要用來書寫統一控制元件,以及一些統一方法。

YGColor

@implementation YGColor
/// 適配暗黑模式得到顏色,專案中所有顏色必須通過此方法
+ (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;
    }
}

/// 適配暗黑模式下的漸變色
+ (YGColor *)colorWithGradientNormalColors:(NSArray *)gradientNormalColors 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;
}

@end
複製程式碼

此類,主要用來管理專案中所有的顏色。

目前主要有兩個方法,一個單色,一個漸變色。

而,在此基礎上,定義了兩個Define,方便呼叫:

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

#define kYGAllGradientColor(nColors,dColors) [YGColor colorWithGradientNormalColors:nColors gradientDarkColors:dColors]
複製程式碼

通過YGColor,可以建立專案中使用的所有動態色彩。

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
複製程式碼

其中,我們使用了一個UIView的method swizzling,主要目的為將UIView切換DarkMode所呼叫的方法,轉換為Block,方面外部呼叫。

主要處理,無法使用UIColor動態處理的情況。

而基於此方法,我們分別建立了:

  1. UIView
  2. UIImageView
  3. UIButton

的Category,用來處理無法跟隨動態切換的情況。

而其中使用的Color,一定為YGColor,顏色永遠通過YGColor管理。

否則,我們處理此種情況會非常麻煩,需要在每一個頂層View的traitCollectionDidChange來管理其subview的變換規則。

四、寫在最後

相信通過以上方法,可以覆蓋到我們APP中80%的情況,在標準一定的情況下,完全可以坐到程式碼規整並優雅的一鍵切換。

Let’s think!