iOS DarkMode適配
iOS13中為我們帶來了系統級別的暗黑模式。
而我們居然沒能第一時間,系統全面的在我們專案中適配,實在是一大遺憾。
現在我們依然再等待相關UI標準的輸出,但是在工程和程式碼層面,我們已經做好了準備。
下面,就讓我們來熟悉一下怎麼優雅又全面系統的適配DarkMode吧。
一、標準的制定
DarkMode的核心是顏色的制定。
我們需要將正常模式的顏色一一對應到DarkMode的顏色。
雖然核心是顏色,但是也牽扯到圖片的轉換,圖片的本質也是色彩。
此部分工作,主要需要UI同學來制定。
一旦將我們的規則,或者標準制定完成,那麼後續主要工作,主要精力依然在正常模式下。
通過轉換規則,則可一一對應到暗黑模式下。
二、系統工程
UITraitCollection
在 iOS 13 中,我們可以通過 UITraitCollection 來判斷當前系統的模式。UIView 和 UIViewController 、UIScreen、UIWindow 都已經遵從了UITraitEnvironment
這個協議,因此這些類都擁有一個叫做 traitCollection
的屬性。
當DarkMode和正常模式來回切換的時候,會按照以下規則觸發以下方法:
其中,核心的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中,蘋果引入了全新系統顏色,系統顏色是動態的,會根據當前系統是預設模式還是暗黑模式動態調整顏色。
蘋果還提供了一組動態的灰度顏色。
**系統語義化顏色 **iOS13支援
Assets 配置 iOS11支援
通過Asset我們可以使用管理顏色,也可以管理圖片,來達到動態切換。
三、我們的方案
根據系統的規則,並且結合我們的工程,我們需要進行以下的區分。
我們管理顏色全部使用程式碼來進行,儘量不使用Asset。
我使用了以下三個類,來整體管理我們的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動態處理的情況。
而基於此方法,我們分別建立了:
- UIView
- UIImageView
- UIButton
的Category,用來處理無法跟隨動態切換的情況。
而其中使用的Color,一定為YGColor,顏色永遠通過YGColor管理。
否則,我們處理此種情況會非常麻煩,需要在每一個頂層View的traitCollectionDidChange
來管理其subview的變換規則。
四、寫在最後
相信通過以上方法,可以覆蓋到我們APP中80%的情況,在標準一定的情況下,完全可以坐到程式碼規整並優雅的一鍵切換。
Let’s think!