iOS知识分享 — iOS 13上的暗模式

我们将本周的文章献给那些庆祝这个受人尊敬的假期的人,无论是同时还是在iOS 13进入通用汽车的过程中。我们希望一些快速提示可以帮助您避开即将到来的阴影。

iOS知识分享 — iOS 13上的暗模式_第1张图片

暗模式是一种外观偏好,告诉系统和参与的应用程序采用较暗的调色板。虽然应用程序默认情况下可能会在浅色背景上显示深色文本,但它可能会在深色背景上显示白色文本。

去年,Dark Mode是macOS 10.14 Mojave 杀手级功能,它与Safari 12的同期发布在整个万维网上掀起,在网站(如你的网站)和 其他浏览器中得到了稳定的采用。

在等待永恒的感觉(但仅仅是一年)之后,Dark Mode现在终于进入iPhone和iPad iOS 13.我们不再需要修改 显示调整 以限制浏览时的光污染Reddit深夜。

在iOS上采用黑暗模式

Apple在设计灵活,方便的API方面做得非常出色,并提供了优秀的文档。

当然,苹果技术面临的挑战是,如果没有告诉你“你说错了”,他们很少会承认现有技术或替代品的存在,更不用说提供一个过渡文件,它在任何方面都与每个人的方式相似在得到官方批准的API之前,我正在做的事情。(那么,你能真的责怪他们吗?

如果您遵循Apple对该信的100%指导,您几乎不必更改一行或代码以使您的应用为下周的特殊活动做好准备 。但是大多数应用程序都建立在我们为自己构建的解决方案的基础上,以弥合SDK中的差距,并且可能不清楚如何 从那里开始新的 快乐路径。

Apple对于采用Dark Mode的指导对于新项目来说非常棒,但是在为iOS 13准备现有应用程序时,并没有完全达到你应该注意的所有要点。所以不用多说了,这里有6个关于如何获取应用程序的动作项

Cancel Color Literals

在Xcode中,颜色文字 是具有前缀的代码,该前缀 在编辑器中呈现为颜色样本。例如,代码 在Xcode中呈现为#colorLiteral#colorLiteral(red: 1, green: 0.67, blue: 0, alpha: 1)。可以从Xcode 10中的媒体浏览器拖放颜色文字,Xcode 10已经与Snippets合并到Xcode 11中的新库面板中。

为了支持Xcode Playgrounds,我们引入了颜色文字和他们的堂兄,图像文字。但是不要让他们的外表欺骗你:你的应用程序的代码库中没有一个位置。

由于它们的flakey渲染,几乎可以立即排除图像文字,但只有出现黑暗模式,我们有一个强有力的理由给它们启动: 颜色文字不支持动态/命名颜色。

尝试将命名颜色拖到编辑器中会导致两个相邻的表达式 - Swift中的无效语句。如果你错误地将一个设置为视图的颜色属性,它将不会改变它在暗或亮模式下的外观。colorLiteral

$ find . -name '*.swift'  \
    -exec sed -i '' -E 's/#colorLiteral\(red: (.*), green: (.*), blue: (.*), alpha: (.*)\)/UIColor(red: \1, green: \2, blue: \3, alpha: \4)/ {}\;

但是在你这样做之前,你最好还是做一个更好的东西,用经得起时间考验的东西代替它。

Nix UIColor十六进制初始化器

在瑞士军刀式CocoaPod中你会发现的最常见的扩展是一个UIColor从十六进制字符串初始化的类别。有点像这样:

import SwiftySwiftColorSwift
let orange = UIColor(hex: "#FB8C00") // ?

抛开关于他们通常如何使用和实施的任何疑虑,建议您按照我们在上一节中描述的相同原因使用这些符合颜色文字的方式。

但不要担心!您仍然可以使用设计师发送的十六进制代码来定义颜色,我们将在讨论命名颜色时看到。

###查找并替换固定颜色

UIColor定义了几个按其通用名称返回颜色的类属性。这些属性在iOS 13中存在问题,因为它们不会自动调整亮度或暗度外观。例如,将标签的颜色设置为.black在默认背景下看起来很好,但是当启用暗模式时背景变为黑色时,它会难以辨认。UITableCell

为了让你的应用程序在iOS 13上为Dark Mode做好准备,你很可能想要替换以下UIColor类属性的任何实例 :

  • red

  • orange

  • yellow

  • brown

  • green

  • cyan

  • blue

  • purple

  • magenta

  • white

  • lightGray

  • gray

  • darkGray

  • black

希望除了偶尔的布局调试之外你没有使用内置的 ROYGBIV UIColor常量,但是你可能会 在某个地方的代码库中找到一些.black或者几个实例.white。

在任何情况下,支持暗模式的最简单的更改是使用system下面相应的-prefixed自适应颜色替换任何上述固定颜色属性:
iOS知识分享 — iOS 13上的暗模式_第2张图片

您可能会注意到此表不提供黑色或白色(或棕色,但暂时忽略它)的直接对应关系。

黑色和白色没有自适应颜色,因为它们的名称在黑暗模式下不再具有描述性; 如果存在的话,几乎必须要 在深色托盘中可见。.systemBlack``.white

在黑暗模式的时代,它更深入地了解色彩管理…

使用语义颜色

确保在任何外观模式下在任何设备上一致呈现UI的最佳方法是使用语义颜色,根据其功能而不是外观命名。

与动态类型如何 使用“标题”和“正文”等语义标签自动为用户的显示首选项设置最合适的字体类似,语义颜色 - 或Apple的调用 UI元素颜色 - 为您的视图和控件提供面向未来的行为。

样式化组件时,将颜色设置为UIColor下面最近的类属性:

标签颜色 填充颜色
label systemFill
secondaryLabel secondarySystemFill
tertiaryLabel tertiarySystemFill
quaternaryLabel quaternarySystemFill
文字颜色 背景颜色
placeholderText systemBackground
secondarySystemBackground
tertiarySystemBackground
链接颜色 分组的背景颜色
link systemGroupedBackground
secondarySystemGroupedBackground
tertiarySystemGroupedBackground
分色器颜色
separator
opaqueSeparator

升级本土语义颜色调色板

如果您已经考虑过在应用程序中进行颜色管理,那么您可能会采用以下策略的某种形式,即根据命名空间内或扩展名中的固定颜色定义语义颜色UIColor

例如,以下示例显示应用程序如何定义 Material UI颜色系统中的UIColor常量 并从语义类属性引用它们:UIColor

import UIKit

extension UIColor {
    static var customAccent: UIColor { return MaterialUI.red500 }
    ...
}

fileprivate enum MaterialUI {
    static let orange600 = UIColor(red:   0xFB / 0xFF,
                                   green: 0x8C / 0xFF,
                                   blue:  0x00 / 0xFF,
                                   alpha: 1) // #FB8C00
    ...
}

如果您的应用使用这样的模式,您可以使用iOS 13中的新初始化程序使其与暗模式兼容 ,如下所示:init(dynamicProvider:) UIColor

import UIKit

extension UIColor
    static var customAccent: UIColor {
        if #available(iOS 13, *) {
            return UIColor { (traitCollection: UITraitCollection) -> UIColor in
                if traitCollection.userInterfaceStyle == .dark {
                    return MaterialUI.orange300
                } else {
                    return MaterialUI.orange600
                }
            }
        } else {
            return MaterialUI.orange600
        }
    }

使用此方法无法改变固定的材质UI颜色常量。相反,语义颜色属性提供动态颜色,使用最适合当前渲染上下文的颜色。启用暗模式时,使用浅橙色来对比较暗的调色板; 否则,行为与原始实现不变。customAccent

额外的#available检查会在实现中产生一些膨胀,但这种方法提供的灵活性是一个很小的代价。

对于奖励积分,您可以扩展此方法以支持默认和高对比度模式下的明暗模式:

// iOS >= 13
UIColor { (traitCollection: UITraitCollection) -> UIColor in
    switch(traitCollection.userInterfaceStyle,
           traitCollection.accessibilityContrast)
    {
        case (.dark, .high): return MaterialUI.orangeA200
        case (.dark, _):     return MaterialUI.orange300
        case (_, .high):     return MaterialUI.orangeA700
        default:             return MaterialUI.orange600
    }
}

不幸的是,以这种方式使用颜色属性有一个至关重要的缺点: 它们不能从Interface Builder中引用

如果您的应用使用Storyboards或XIB,最好的方法是使用颜色资源。

使用资产目录管理颜色

通过颜色资源,您可以像处理图像,数据或其他资源一样管理Xcode Asset Catalog中的颜色。
iOS知识分享 — iOS 13上的暗模式_第3张图片
要创建颜色集,请在Xcode项目中打开资产目录,单击左下角的 + 按钮,然后选择“新颜色集”。在“属性”检查器中,选择“任意,黑暗”外观。颜色经常以表格形式表示(“#RRGGBB”); 您可以通过从“输入法”下拉列表中选择“8位十六进制”来输入此表单中的颜色。
iOS知识分享 — iOS 13上的暗模式_第4张图片
在这里,我们和以前一样,除了UIColor不像orange300代码中那样定义固定常量,我们在颜色集本身中设置它们。现在,当需要通过现有的语义类属性引用颜色资源时,我们可以使用UIColor命名的初始化程序:

extension UIColor {
    @available(iOS 11, *)
    var customAccent: UIColor! {
        return UIColor(named: "Accent")
    }
}

单个应用可以有多个资产目录。考虑为您的颜色资产创建一个单独的颜色

您对颜色资产的看法很大程度上取决于您对专业Xcode文档编辑的偏好或不尊重。有些人喜欢在代码中拼写出所有内容,而其他人则喜欢定制用户界面提供的功能,例如Xcode为彩色套装提供的功能。

事实上,您对色彩资产的看法可能伴随着您对Storyboards的感受 - 这很方便,因为色彩资产的杀手级功能是它们可以在Interface Builder中引用。 (如果你不在IB团队,那么你可以安全地跳过整个讨论。)

替换XIB和Storyboard中的“自定义颜色”实例

Interface Builder中的“自定义颜色”选项调出了macOS系统原生颜色选择器,遇到的问题与我们之前谈到的颜色文字和固定颜色相同。如果您希望X13驱动的视图在iOS 13上看起来很好,则需要迁移到命名颜色。

这可以轻松完成:选择场景中的任何组件,并且可以使用资产目录中定义的相同命名颜色设置其颜色属性。
iOS知识分享 — iOS 13上的暗模式_第5张图片

对于一个小项目,这可以在一小时内手动完成所有屏幕。但是,对于更大的应用程序,这是一个您想要自动化的过程。

XIB解剖学

在幕后,XIB和Storyboard文件只是像任何其他文件一样的XML文件:


>
    
        
    
    
        
        
        
        
    
    
        
        
            
                
                    
                        
                        
                    
                
                
            
        
    

我们不想手工写这个,但这里没有什么太神秘的了。

因此,请考虑使用Xcode将主视图的自定义背景颜色切换为命名颜色时会发生什么:


>
    />
    
        <#...#>
         
    
    
        
        <#...#>
        
            <#...#>
             
        
    
    
         
            
        
    

  • "Named colors"添加了一个新功能作为打开文档的依赖项(Xcode使用它来确定它是否可以编辑由较新版本创建的文件)。
  • 的red,green,blue,和alpha所述上部件color元件由单个替换name属性。
  • 相应的元素已添加到顶级元素中。namedColorresources

基于这样的认识,我们应该知道足以让这个改变集体 用我们自己的工具!

查找所有自定义颜色

以下示例使用XMLStarlet命令行工具使用XPath查询XIB和Storyboard文件 。
您可以自己安装它并使用Homebrew跟随 :$ brew install xmlstarlet

迁移故事板和XIB用于暗模式时的第一个业务是查找自定义颜色的所有实例。您可以手动浏览每个并单击每个可视元素…或者您可以运行以下命令:

$ find . -name '*.xib' -or -name '*.storyboard' \
    -exec echo {} \;        \
    -exec xmlstarlet sel -t \
            -m "//color[@colorSpace='custom']" -c . -n  {} \;

Main.storyboard

此命令打印每个文件的名称,后跟它color找到的每个自定义未命名元素(由属性表示)。colorSpace="custom"

结果列表可作为下一阶段攻击的战斗计划:

寻找每种不同的自定义颜色

应用程序倾向于在其视图中重复使用一小组颜色 - 正如他们应该的那样!通过运行以下命令,您可以在每个XIB或Storyboard中生成每个自定义颜色的已排序,唯一的列表:

$ find . -name '*.xib' -or -name '*.storyboard' \
    -exec xmlstarlet sel -t \
            -m "//color[@colorSpace='custom']"  \
            -v "concat( @red,'  ',@green,'  ',@blue,'  ',@alpha)" -n  {} \; \
    | sort -u

1  0.69019607839999997  0.0  1

有些条目可能是相同的,在彼此的小三角形内(因为,你知道…浮点数)。为了解释这一点,并将输出转换为更容易使用的东西,您可以将输出写入文件并使用类似这样的Ruby脚本处理它:

colors = File.readlines('distinct_colors.txt').map do |line|
    components = line.strip.split(/\s+/).flat_map(&:to_f)
    red, green, blue = components[0..2].map{ |c| (c * 255).floor }
    alpha = (components.last * 100).floor
    [red, green, blue, alpha]
end

colors.uniq.each do |color|
    puts "#%02X%02X%02X (%d%%)" % color
end

从这里开始,最后一步是将每组RGBA值映射 到要替换它的相应命名颜色。

如果您的数据集很大,或者您只是不相信您的眼睛正确量化这些颜色,您可以使用聚类算法为您执行此操作。
您可能熟悉 k -means聚类,但这需要我们提前指定目标簇数。如果您不知道有多少组有先验, “基于密度的噪声应用空间聚类” DBSCAN 可以是一个不错的选择。
Ruby方便地有一个 适合我们目的的宝石*

require 'dbscan'

dbscan = DBSCAN(colors, epsilon: 8, min_points: 1, distance: :euclidean_distance)
puts dbscan.results

为获得最佳效果,请 在聚类前将RGBA值转换为 具有感知均匀性的色彩空间。

替换自定义颜色
在这一点上,我们已经远远超出了shell单行似乎是个好主意的地步。所以这里是我们编写的Ruby脚本,它使我们理解的所有更改都在Interface Builder中用命名颜色替换自定义颜色时发生:

我无法保证此脚本的正确性,因此使用它需要您自担风险。

require 'nokogiri'

def name_for_rgba_components(red, green, blue, alpha)
    case format('#%02X%02X%02X%02X', red, green, blue, alpha)
    # Return named color matching RGBA components
    # e.g. "#F8CB00FF" => "Accent"
    end
end

def name_for_white_and_alpha_components(white, alpha)
    # Return named color matching white and alpha components
    # e.g. 0.9 => "Off-White"
end

# Process each Storyboard and XIB file
Dir['**/*.{storyboard,xib}'].each do |xib|
  doc = Nokogiri::XML(File.read(xib))

  names = []
  # Collect each custom color and assign it a name
  doc.xpath('//objects//color').each do |color|
    next if color['name']

    name = nil

    color_space = color['colorSpace']
    color_space = color['customColorSpace'] if color_space == 'custom'

    case color_space
    when 'sRGB', 'calibratedRGB'
      components = color.attributes
                        .values_at('red', 'green', 'blue', 'alpha')
                        .map(&:value)
                        .map(&:to_f)
                        .map { |c| c * 255 }
      name = name_for_rgba_components(*components)
    when 'genericGamma22GrayColorSpace', 'calibratedWhite'
      components = color.attributes
                        .values_at('white', 'alpha')
                        .map(&:value)
                        .map(&:to_f)
      name = name_for_white_and_alpha_components(*components)
    end

    next unless name

    named_color = doc.create_element('color',
                                     key: color['key'],
                                     name: name)
    color.replace(named_color)
    names << name
  end

  # Proceed to the next file if no named colors were found
  next if names.empty?

  # Add the named color capability as a document dependency
  dependencies = doc.at('/document/dependencies') ||
                 doc.root.add_child(doc.create_element('dependencies'))
  unless dependencies.at("capability[@name='Named colors']")
    dependencies << doc.create_element('capability',
                                       name: 'Named colors',
                                       minToolsVersion: '9.0')
  end

  # Add each named color to the document resources
  resources = doc.at('/document/resources') ||
              doc.root.add_child(doc.create_element('resources'))
  names.uniq.sort.each do |name|
    next if resources.at("namedColor[@name='#{name}']")
    resources << doc.create_element('namedColor', name: name)
  end

  # Save the changes
  File.write(xib, doc.to_xml(indent: 4, encoding: 'UTF-8'))
end

你怎么看?请通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈。

谢谢阅读~点个赞再走呗!?

原文地址 https://nshipster.com/dark-mode/

你可能感兴趣的:(iOS,iOS开发)