iOS适配深色DarkMode模式

iOS在13的版本加入了对深色模式的支持,深色模式下App整体上呈现黑色UI界面,现在许多App都完成了深色模式的适配,但也有少量App未支持深色模式(这些App大多是内嵌较多的H5页面)例如支付宝等,本文主要介绍iOS深色模式的适配。

System Colors

Apple为了适配深色模式对UIKit中的UIColor进行了重新定义,例如将.red, .blue.yellow定义为.systemRed.systemBlue.systemYellow,这些新定义的System Colors在深色和浅色模式下表现为不同的颜色,具体的色值可以见苹果的官方文档

Semantic Colors

对于一些需要进行文字显示的控件Apple也做了深色模式的适配,Apple新加了Semantic Colors颜色方案,使用Semantic Colors时不需要纠结具体的值,只需要在合适的场景使用,例如当控件是Label时,在没有自定义字体颜色时,可以使用.label类型的的Semantic Colors,在浅色模式下显示黑色字体,在深色模式下显示白色字体;Semantic Colors包括.label.separator.link.systemBackground.systemFill

对于系统适配的以上深色模式下的颜色可以使用SemanticUI这个App进行查看,该App给出了二种模式下的系统颜色的对比:

Dynamic Colors

当然在实际开发中很多情况下我们都是需要自定义颜色的,有幸的是Apple也给出了相应的方案,那就是通过UIColor.init(dynamicProvider: @escaping (UITraitCollection) -> UIColor)这个方法进行创建自定义的semantic color

使用代码的方式:

import UIKit
infix operator |: AdditionPrecedence
public extension UIColor {
    static func | (lightMode: UIColor, darkMode: UIColor) -> UIColor {
        guard #available(iOS 13.0) else { return lightMode }          
        return UIColor { (traitCollection) -> UIColor in
            return traitCollection.userInterfaceStyle == .light ? lightMode : darkMode
        }
    }
}
// 使用
let dynamicColor = .black | .white

使用Asset Catalog方式

自动iOS11开始,可以在Asset Catalogs保存自定义颜色,并支持Interface Buildercode二种方式使用,自定义的color目前也支持深色模式。打开Assets.xcassets ,店家左下角的+号按钮新增一个Color Set,在Any Appearence(浅色模式)和Dark Appearence(深色模式)分别添加一种颜色即可。


当然也支持代码的使用:

Let view =  Uiview()
View.backdroundcolor = Color(named: Color)

Border colors

Border colors在当主题模式发生改变时并不会自动的进行适配,所以需要手动的进行处理,可以通过traitCollectionDidChange(_:)这个方法在进行处理:

import UIKit
extension UIColor {
    static let layer = UIColor(light: .init(red: 106 / 255, green: 32 / 255, blue: 119 / 255, alpha: 1),
                               dark: .init(red: 138 / 255, green: 76 / 255, blue: 146 / 255, alpha: 1))
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    traitCollection.hasDifferentColorAppearance(comparedTo: traitCollection) {
        layer.backgroundColor = UIColor.layer.cgColor
    }
}

引起TraitCollection变化有多种情况的,不只是系统显示模式的切换,例如当屏幕方向发生旋转时也会触发上面的方法,所以最好是做一个判断,当TraitCollection的更改影响color的值时才给layer赋值,在Xcode中可以添加一个launch argument:UITraitCollertionChangeLoggingEnabled = YES来检测TraitCollection的改变:

Dynamic Images

图片资源同样支持深色模式,需要使用Assets.xcassets,新建一个Assets.xcassets并在Attributes inspector点击Appearances选择Any, Dark,然后分别为Any AppearancesDark Appearances配置响应的图片。


实际开发中图片资源可能是后台返回的,这时候需要使用image assets

import UIKit
public extension UIImageAsset {
    convenience init(lightModeImage: UIImage?, darkModeImage: UIImage?) {
        self.init()
        register(lightModeImage: lightModeImage, darkModeImage: darkModeImage)
    }
    func register(lightModeImage: UIImage?, darkModeImage: UIImage?) {
        register(lightModeImage, for: .light)
        register(darkModeImage, for: .dark)
    }
    func register(_ image: UIImage?, for traitCollection: UITraitCollection) {
        guard let image = image else {
            return
        }
        register(image, with: traitCollection)
    }
    func image() -> UIImage {
        if #available(iOS 13.0, tvOS 13.0, *) {
            return image(with: .current)
        }
        return image(with: .light)
    }
}

尽量减少图片

为了适配深色模式,无限的增加图片资源最终会导致包的大小会增加很多,为了减少包的体积,在非必要添加图片的情况下有以下二种方案:

  • 使用tint color
    对于那些像toolbarstab bars的图片,可以用tint color去渲染这些图片以满足深色模式,首先需要让图片以模板的形式渲染:

或者:

let iconImage = UIImage()
let imageView = UIImageView()
imageView.image = iconImage.withRenderingMode(.alwaysTemplate)

使用:

public static var tint: UIColor = {
    if #available(iOS 13, *) {
        return UIColor { (UITraitCollection: UITraitCollection) -> UIColor in
            if UITraitCollection.userInterfaceStyle == .dark {
                return Colors.osloGray
            } else {
                return Colors.dataRock
            }
        }
    } else {
        return Colors.dataRock
    }
}()
imageView.tintColor = Style.Colors.tint
  • 反转图片颜色
    反转图片颜色也是一种好的选择,只是不是对所有的图片都合适,可以使用如下代码:
extension UIImage {
    func invertedColors() -> UIImage? {
        guard let ciImage = CIImage(image: self) ?? ciImage, let filter = CIFilter(name: "CIColorInvert") else { return nil }
        filter.setValue(ciImage, forKey: kCIInputImageKey)
        guard let outputImage = filter.outputImage else { return nil }
        return UIImage(ciImage: outputImage)
    }
}

自动更新 Dark Mode

现在支持深色模式的App都会提供浅色和深色模式的选择,同时支持跟谁系统自动切换,当用户选择相应模式后需要用Userdefaults保存用户的选择。

import UIKit
public extension UserDefaults {
    var overridedUserInterfaceStyle: UIUserInterfaceStyle {
        get {
            UIUserInterfaceStyle(rawValue: integer(forKey: #function)) ?? .unspecified
        }
        set {
            set(newValue.rawValue, forKey: #function)
        }
    }
}

当保存了用户选择后同时需要更新当前App的显示:

public extension UIApplication {
    func override(_ userInterfaceStyle: UIUserInterfaceStyle) {
        // iPad支持多窗口,不支持iPad的话可以删除这段判断
        if #available(iOS 13.0, *), supportsMultipleScenes {
            for connectedScene in connectedScenes {
                if let scene = connectedScene as? UIWindowScene {
                    scene.windows.override(userInterfaceStyle)
                }
            }
        }
        else {
            windows.override(userInterfaceStyle)
        }
    }
}
public extension UIWindow {
    func override(_ userInterfaceStyle: UIUserInterfaceStyle) {
        if #available(iOS 13.0, tvOS 13.0, *) {
            overrideUserInterfaceStyle = userInterfaceStyle
        }
    }
}
public extension Array where Element: UIWindow {
    func override(_ userInterfaceStyle: UIUserInterfaceStyle) {
        for window in self {
            window.override(userInterfaceStyle)
        }
    }
}
    UIApplication.shared.override(style)
    UserDefaults.standard.overridedUserInterfaceStyle = style

退出或禁用深色模式

当采用Xcode 11新建项目时会默认开启深色模式的,当没时间或者暂不支持深色模式时可以禁用掉,简单的方法时在Info.plist中添加一个UIUserInterfaceStyle = Light,也支持为某个View或者ViewController单独设置相应的模式,但是这种设置不会影响模态弹出的VC的模式。

let view = UIView()
view.overrideUserInterfaceStyle = .dark
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        overrideUserInterfaceStyle = .dark
    }
}

总结:

本文详细介绍了iOS深色模式的适配,最主要的是通过UITraitCollection来拿到当前的样式来进行相应的颜色上的适配,Apple也提供的新定义的几种系统颜色方案并能自动适配深色模式,深色模式适配代码不多,就是需要将项目所有的界面都要进行适配,工作量巨大。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容