JHHK

欢迎来到我的个人网站
行者常至 为者常成

深色模式适配

目录

深色模式

针对深色模式的推出,Apple官方推荐所有三方App尽快适配。目前并没有强制App进行深色模式适配。因此深色模式适配范围现在可采用以下三种策略:

  • 全局关闭深色模式

  • 指定页面关闭深色模式

  • 全局适配黑暗模式

全局关闭深色模式

方案一:在项目 Info.plist 文件中,添加一条内容,Key为 User Interface Style,值类型设置为String并设置为 Light 即可。

方案二:代码强制关闭黑暗模式,将当前 window 设置为 Light 状态。

if(@available(iOS 13.0,*)){
    self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}

指定页面关闭深色模式

从Xcode11、iOS13开始,UIViewController与View新增属性 overrideUserInterfaceStyle,若设置View对象该属性为指定模式,则强制该对象以及子对象以指定模式展示,不会跟随系统模式改变。

设置 Window 该属性,将会影响窗口中的所有内容都采用该样式,包括根视图控制器和在该窗口中显示内容的所有控制器

设置 ViewController 该属性,将会影响视图控制器的视图以及子视图控制器都采用该模式。

设置 View 该属性,将会影响视图及其所有子视图采用该模式。

全局适配黑暗模式

一、iPhone提供两种方式设置手机当前外观模式

设置 –> 显示与亮度

控制中心, 长按亮度调节按钮

二、获取当前模式

各种枚举值

typedef NS_ENUM(NSInteger, UIUserInterfaceStyle) {
    UIUserInterfaceStyleUnspecified,
    UIUserInterfaceStyleLight,
    UIUserInterfaceStyleDark,
} API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);

获取方式一:

if (@available(iOS 13.0, *)) {
    UIUserInterfaceStyle mode = UITraitCollection.currentTraitCollection.userInterfaceStyle;
    if (mode == UIUserInterfaceStyleDark) {
        NSLog(@"深色模式");
    } else if (mode == UIUserInterfaceStyleLight) {
        NSLog(@"浅色模式");
    } else {
        NSLog(@"未知模式");
    }
}

获取方式二:

-(void)getCurrentMode2{
    if(@available(iOS 13.0, *)){
        UIUserInterfaceStyle mode = self.traitCollection.userInterfaceStyle;
        if (mode == UIUserInterfaceStyleDark) {
            NSLog(@"深色模式");
        } else if (mode == UIUserInterfaceStyleLight) {
            NSLog(@"浅色模式");
        } else {
            NSLog(@"未知模式");
        }
    }
}

那么UITraitCollection.currentTraitCollection与self.traitCollection有什么区别呢?

UITraitCollection.currentTraitCollection是从父控制器或父视图传递过来的taitCollection,而self.traitCollection是自身的traitCollection。举个例子window->navigationController->tabController->viewController.在这个序列上各个控制器的UITraitCollection.currentTraitCollection是相同的都是window的traitColllection.但各自的self.traitCollection各自拥有并不相同。

另外这两个traitCollection都是一个动态分配的在不同的模式下其地址是不同的。

三、强行设置App模式

当系统设置为Light Mode时,对某些App的个别页面希望一直显示Dark Mode下的样式,这个时候就需要强行设置当前ViewController的模式了

// 设置当前view或viewCongtroller的模式
@property(nonatomic) UIUserInterfaceStyle overrideUserInterfaceStyle;

if (@available(iOS 13.0, *)){
    self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
}

当我们强行设置当前viewController为Dark Mode后,这个viewController下的view都是Dark Mode,由这个ViewController present出的ViewController不会受到影响,依然跟随系统的模式.

要想一键设置App下所有的ViewController都是Dark Mode,请直接在Window上执行overrideUserInterfaceStyle,对window.rootViewController强行设置Dark Mode也不会影响后续present出的ViewController的模式.

if (@available(iOS 13.0, *)){
    [self.window setOverrideUserInterfaceStyle:UIUserInterfaceStyleLight];
}

四、监听模式切换

有时我们需要监听系统模式的变化,并作出响应,那么我们就需要在需要监听的viewController中,重写下列函数

//7、监听模式改变(系统调用)
-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection{
    //需要调用父类方法
    [super traitCollectionDidChange:previousTraitCollection];
    
    if (@available(iOS 13.0, *)) {
        if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
            NSLog(@"[lilog]:模式发生改变");
        }
        
        if (self.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
            NSLog(@"[lilog]-监听:深色模式");
        }else{
            NSLog(@"[lilog]-监听:浅色模式");
        }
    }
}

当系统模式切换,或当前控制器的模式切换时会调用该方法,比如执行下面代码:

self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;

五、颜色适配

iOS13 之前 UIColor只能表示一种颜色,而从 iOS13 开始UIColor是一个动态的颜色,在Light Mode和Dark Mode可以分别设置不同的颜色。

@property (class, nonatomic, readonly) UIColor *systemBrownColor        API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemIndigoColor       API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemGray2Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray3Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray4Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray5Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray6Color        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *labelColor              API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *secondaryLabelColor     API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *tertiaryLabelColor      API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *quaternaryLabelColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *linkColor               API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *placeholderTextColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *separatorColor          API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *opaqueSeparatorColor    API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemBackgroundColor                   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemBackgroundColor          API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemBackgroundColor           API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGroupedBackgroundColor            API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemGroupedBackgroundColor   API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemGroupedBackgroundColor    API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemFillColor                         API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemFillColor                API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemFillColor                 API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *quaternarySystemFillColor               API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);

示例代码如下:

[self.view setBackgroundColor:[UIColor systemBackgroundColor]];
[self.titleLabel setTextColor:[UIColor labelColor]];
[self.detailLabel setTextColor:[UIColor placeholderTextColor]];

效果如下:

img

在实际开发过程,系统提供的这些颜色还远远不够,因此我们需要创建更多的动态颜色,初始化动态UIColor方法。

-(void)colorAdaper{
    UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(20, 200, 200, 100)];
    label.text = @"测试模式";
    self.label = label;

    if(@available(iOS 13.0, *)){
        
        label.backgroundColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                return [UIColor blueColor];
            }else{
                return [UIColor greenColor];
            }
        }];
        
        label.textColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                return [UIColor blackColor];
            }else{
                return [UIColor whiteColor];
            }
        }];
        
        
        
        
    }else{
        label.backgroundColor = [UIColor greenColor];
        label.textColor =  [UIColor greenColor];
    }
    
   
    
    [self.view addSubview:label];
}

当系统切换模式时,会回调到自定义动态颜色的回调里更改颜色显示。

UIColor能够表示动态颜色,但是CGColor依然只能表示一种颜色,那么对于CALayer等对象如何适配暗黑模式呢?富文本如何适配深色模式?

//监听模式改变(系统调用)
-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection{
    //需要调用父类方法
    [super traitCollectionDidChange:previousTraitCollection];
    
    if (@available(iOS 13.0, *)) {
        //CGColor的适配
        UIColor * borderColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
            
            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                return [UIColor yellowColor];
            }else{
                return [UIColor purpleColor];
            }
        }];
        
        _label.layer.borderWidth = 3;
        //_label.layer.borderColor = [borderColor resolvedColorWithTraitCollection:self.traitCollection].CGColor;
        _label.layer.borderColor = borderColor.CGColor;
        
        

        //富文本的适配
        NSDictionary * dic = @{NSForegroundColorAttributeName:borderColor,
                               NSFontAttributeName : [UIFont fontWithName:@"Helvetica-Bold" size:17]};
        [_label setTitleTextAttributes:dic];
    }
}

六、图片适配

在xx.xcassets文件内新建一个Image set,

img

打开右侧工具栏,点击最后一栏,找到Appearances,选择Any,Dark

img

将两种模式下不同的图片资源都拖进去

img

使用该图片

[_logoImage setImage:[UIImage imageNamed:@"icon_logo"]];

img

工程适配思路

对现有工程如何适配的几点思考:

1、简单适配原则,不需要产品另外提供一套深色模式下的色值和图片。

iOS13下一些系统的view控件是支持在不同模式下显示不同的背景色的。我们可以默认使用系统提供的颜色。将自己的工程运行起来,切换不同的模式,根据不同的页面显示效果,发现哪里显示有问题就改哪里,进行局部修改。这样做能快速适配暗黑模式,而不至于使APP在暗黑模式下显得很奇怪。缺点是不够定制化,适配的效果不够完美。

2、深度适配,让产品提供深色模式下的色值和图片。根据上边的适配方法进行整体精致化的适配。

适配之前建议搞一个全局的颜色管理器,用来管理不同模式下的不同颜色,这样方便以后进行更改。同时有一点好处是假如以后要加入主题实现起来也是很方便的。

3、关于weex适配暗黑模式的思考。

原生端适配还是比较简单的,但对一些采用了weex工程的APP来说怎么适配呢?现在能想到的方案:

方案一: 由于weex最终会渲染成原生控件,所以跟上面提到的简单适配原则一样,哪里有问题改哪里。

方案二: 通过切换模式的回调内发出通知,weex工程内监听通知获知当前所处模式,进行颜色和图片的切换。

4、原生与weex统一适配方案

在适配暗黑模式的尝试过程中,与同事进行探讨,能不能在不改动weex代码的情况下来实现较好的适配效果?

由于weex最终都会渲染成原生控件所以我们从下面几个方面入手:

第一:同样要有一个颜色管理,比如创建一个plist文件,浅色模式的颜色值做key,深色模式的颜色值做value。

第二:给UIView、UILable、UIButton、… 添加分类,在分类里对设置背景色,字体颜色,高亮颜色,… 利用runtime进行交换,在自定义的方法内部设置动态颜色。

第三:关于CGColor的适配,暂时还未找到可行的统一处理的方法,有一个思路就是在UIView的分类里监听深色浅色模式回调,同时通过kvo监听view.layer.color是否有赋值,代码如下:

@interface UIView()
@property (nonatomic, strong) UIColor * lightColor;//
@property (nonatomic, strong) UIColor * darkColor;//
@end


@implementation UIView (LKLThemeColor)


-(UIColor *)lightColor{
    return objc_getAssociatedObject(self, _cmd);
}

-(void)setLightColor:(UIColor *)lightColor{
    return objc_setAssociatedObject(self, @selector(lightColor), lightColor,OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

-(UIColor *)darkColor{
    return objc_getAssociatedObject(self, _cmd);
}

-(void)setDarkColor:(UIColor *)darkColor{
    return objc_setAssociatedObject(self, @selector(darkColor), darkColor,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

//在设置frame的时候添加kvo监听
- (void)lkl_setFrame:(CGRect)frame{
    [self lkl_setFrame:frame];
    [self.layer addObserver:self forKeyPath:@"borderColor" options:NSKeyValueObservingOptionNew context:nil];
}



-(void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                          context:(void *)context{
    


//    self.recordColor1 = [UIColor colorWithCGColor:(__bridge CGColorRef)change[@"new"]];

    //这里需要根据plist文件内色值的获取对浅色和深色分别赋值,简单举个例子就直接写死了。
    self.lightColor = [UIColor redColor];
    self.darkColor = [UIColor yellowColor];

    if (@available(iOS 13.0, *)) {
        //CGColor的适配
        UIColor * borderColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {

            if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                return self.darkColor;
            }else{
                return self.lightColor;
            }
        }];

        self.layer.borderColor = borderColor.CGColor;
    }
}


+ (void)load{
    Method method1 = class_getInstanceMethod([self class], @selector(setFrame:));
    Method method2 = class_getInstanceMethod([self class], @selector(lkl_setFrame:));
    method_exchangeImplementations(method1, method2);
}

//监听模式改变(系统调用)
-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection{
    //需要调用父类方法
//    [super traitCollectionDidChange:previousTraitCollection];
    
    
    if (self.lightColor&&self.darkColor) {

        if (@available(iOS 13.0, *)) {
            //CGColor的适配
            UIColor * borderColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
                
                if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
                    return self.darkColor;
                }else{
                    return self.lightColor;
                }
            }];
            
            self.layer.borderColor = borderColor.CGColor;
        }
    }
}
@end

但是这样有一个问题就是当类内,比如UILabel重载了下面的方法后UILabel的颜色变换就会失效

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;

但是这样也解决了大部分的CGColor的适配,剩下的重载了上面两个方法的类在单独处理下工作量也不大。


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.