在大多数情况下,可以说现代的iOS和Mac应用程序被期望优雅地适应用户的设备是在浅色或深色模式下运行,这往往需要我们在我们构建的UI中使用更多的动态颜色。
虽然苹果公司使用Xcode的资产目录系统来声明这种动态颜色是相当直接的,但有时我们可能想在Swift代码中定义我们的颜色。因此,让我们来看看使用 SwiftUI 或 UIKit 来实现这一目的的几种方法。
使用系统颜色
也许确保我们的颜色适应各种用户偏好的最简单方法是尽可能使用作为 UIKit 和 SwiftUI 一部分的预定义颜色。所有 SwiftUI 的内置颜色在默认情况下都是自适应的,所有UIColor API 也是如此,其前缀为system 。
// SwiftUI
label.foregroundColor(.orange)
// UIKit
label.textColor = .systemOrange
尽管上述标签总是有一个橙色的文本颜色,但具体使用的橙色阴影会有所不同,这取决于用户的设备是使用深色还是浅色模式,以及是否启用了某些可访问性设置(如增加对比度)。
SwiftUI和UIKit也都提供了一套更抽象的颜色,然后在运行时解析为特定的、与环境相适应的颜色。例如,在使用SwiftUI时,primary 和secondary 颜色在处理文本时往往特别有用,因为它们会使我们的文本颜色与整个系统使用的颜色相匹配。
struct ArticleListItem: View {
var article: Article
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(article.title)
.font(.headline)
.foregroundColor(.primary)
Text(article.description)
.foregroundColor(.secondary)
}
.padding()
}
}
💡 提示:使用PREVIEW 按钮,看看上面的代码样本在使用浅色和深色模式渲染时是什么样子。
UIKit包含一套更全面的上下文颜色,与某些系统组件使用的默认颜色有一定的联系。因此,相当于SwiftUI的primary 和secondary 的颜色在使用UIColor 时被称为label 和secondaryLabel 。
label.textColor = .label
detailLabel.textColor = .secondaryLabel
view.backgroundColor = .systemBackground
自定义颜色
虽然系统提供的颜色绝对是一个很好的起点,但我们很可能还想在每个项目中使用一些完全自定义的颜色,而且我们可能还需要让这些颜色适应用户当前的色彩方案。
做到这一点的一个方法是,让我们的UI代码观察到每当配色方案被改变时,然后更新任何需要适应的自定义颜色,只要发生这种情况。例如像这样在使用SwiftUI时:
struct ArticleListItem: View {
var article: Article
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(article.title)
.font(.headline)
.foregroundColor(titleColor)
Text(article.description)
.foregroundColor(.secondary)
}
.padding()
}
private var titleColor: Color {
switch colorScheme {
case .light:
return Color(white: 0.2)
case .dark:
return Color(white: 0.8)
@unknown default:
return Color(white: 0.2)
}
}
}
当使用UIKit时,我们可以通过重写我们的一个视图或视图控制器中的traitCollectionDidChange(_:) 方法来执行同样的观察。然后我们可以切换到传递的特质集合的userInterfaceStyle 。
然而,虽然上述技术确实有效,但如果我们需要在许多不同的视图中执行同样的观察,事情很快就会变得相当混乱和重复。在使用SwiftUI时,解决这个问题的一个方法是创建一个可重复使用的视图修改器,让我们为浅色和深色模式指定单独的前景颜色,然后让我们的修改器在内部观察当前的颜色方案--像这样:
struct AdaptiveForegroundColorModifier: ViewModifier {
var lightModeColor: Color
var darkModeColor: Color
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
content.foregroundColor(resolvedColor)
}
private var resolvedColor: Color {
switch colorScheme {
case .light:
return lightModeColor
case .dark:
return darkModeColor
@unknown default:
return lightModeColor
}
}
}
extension View {
func foregroundColor(
light lightModeColor: Color,
dark darkModeColor: Color
) -> some View {
modifier(AdaptiveForegroundColorModifier(
lightModeColor: lightModeColor,
darkModeColor: darkModeColor
))
}
}
有了上面的修改器,我们现在可以很容易地在我们的视图中在线指定我们的自适应前景颜色,而不需要在每个调用站点进行任何观察或其他复杂的操作。
struct ArticleListItem: View {
var article: Article
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(article.title)
.font(.headline)
.foregroundColor(
light: Color(white: 0.2),
dark: Color(white: 0.8)
)
Text(article.description)
.foregroundColor(.secondary)
}
.padding()
}
}
如果我们只想以有限的方式处理颜色,上述技术是非常有用的,但如果我们想定制各种颜色--前景、背景、着色、形状描边和填充等等--那么我们可能会想出一个稍微通用的解决方案。
为此,我们可以求助于UIColor ,它提供了一种用动态闭合来初始化颜色的方法,每当当前活动的UITraitCollection 被改变时,系统就会调用该方法。这又使我们能够实现一个自定义的初始化器--就像我们上面使用的模式--让我们指定单独的明暗模式的颜色,然后根据当前特征集合的userInterfaceStyle 。
extension UIColor {
convenience init(
light lightModeColor: @escaping @autoclosure () -> UIColor,
dark darkModeColor: @escaping @autoclosure () -> UIColor
) {
self.init { traitCollection in
switch traitCollection.userInterfaceStyle {
case .light:
return lightModeColor()
case .dark:
return darkModeColor()
@unknown default:
return lightModeColor()
}
}
}
}
上述解决方案的一大好处是,它可以很容易地扩展到考虑到其他类型的特征(如各种可访问性设置),因为我们传递的UITraitCollection 实例包含了更多的信息,而不仅仅是使用什么颜色方案。
真正伟大的是,SwiftUI的Color 和UIColor 可以很容易地进行桥接--这意味着我们也可以用很少的额外代码使上述解决方案与SwiftUI完全兼容。
extension Color {
init(
light lightModeColor: @escaping @autoclosure () -> Color,
dark darkModeColor: @escaping @autoclosure () -> Color
) {
self.init(UIColor(
light: UIColor(lightModeColor()),
dark: UIColor(darkModeColor())
))
}
}
请注意,在iOS 15 SDK中,上述的Color 初始化器已经被弃用,而采用了一个名为init(uiColor:) 的新版本,其工作方式完全相同。
有了上述内容,我们现在可以在任何我们想去的地方创建自适应的Color 和UIColor 实例,只需这样做:
struct ArticleListItem: View {
var article: Article
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text(article.title)
.font(.headline)
.foregroundColor(Color(
light: Color(white: 0.2),
dark: Color(white: 0.8)
))
Text(article.description)
.foregroundColor(.secondary)
}
.padding()
}
}
如果我们想更进一步,我们甚至可以把颜色定义从我们的视图中完全抽象出来,把它们移到静态属性中,与primary 、secondary 和其他作为系统一部分的动态颜色一样。
extension Color {
static var title: Self {
Self(light: Color(white: 0.2),
dark: Color(white: 0.8))
}
}
以上绝对是我在构建应用程序时喜欢使用的架构。通过将每种颜色的上下文转化为title,background,appTint 等属性,我发现随着时间的推移,更容易保持应用程序的颜色一致和井然有序。
总结
定义自定义颜色最初可能看起来是一个简单的问题,但随着我们的应用程序运行的执行环境继续变得越来越动态,我们必须使我们的颜色适应这些不同的环境的方式可能会继续增加复杂性。
谢谢你的阅读!