在 Swift 中定义动态颜色

在大多数情况下,可以公平地说,现代 iOS 和 Mac 应用程序有望优雅地适应用户的设备是在明暗模式下运行,这通常需要我们在我们构建的 UI 中使用更多动态颜色。

虽然 Apple 确实让使用 Xcode 的资产目录系统声明这种动态颜色变得相当简单,但有时我们可能希望在 Swift 代码中内联定义我们的颜色。因此,让我们来看看使用 SwiftUI 或 UIKit 的几种方法来做到这一点。

使用系统颜色

也许确保我们的颜色适应各种用户偏好的最简单方法是尽可能多地使用作为 UIKit 和 SwiftUI 的一部分提供的预定义颜色。默认情况下,SwiftUI 的所有内置颜色都是自适应的,所有UIColor以 为前缀的 API也是如此system

// SwiftUI
label.foregroundColor(.orange)

// UIKit
label.textColor = .systemOrange

尽管上述标签将始终具有橙色文本颜色,但所使用的橙色的确切阴影会有所不同,这取决于用户的设备是使用深色模式还是浅色模式,以及是否启用了某些辅助功能设置(例如增加对比度)。

SwiftUI 和 UIKit 还提供了一套更抽象的颜色,然后将在运行时解析为特定的、适合上下文的颜色。例如,在使用 SwiftUI 时,primarysecondary颜色在处理文本时通常特别有用,因为它们会使我们的文本颜色与整个系统中使用的颜色相匹配:

预习
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的等效primarysecondary颜色被称为labelsecondaryLabel与工作时UIColor

label.textColor = .label
detailLabel.textColor = .secondaryLabel
view.backgroundColor = .systemBackground

如需完整的颜色列表,请查看 Apple 的“UI 元素颜色”文档页面。

自定义颜色

虽然系统提供的颜色绝对是一个很好的起点,但我们可能还想在每个项目中使用一些完全自定义的颜色,我们可能还需要使这些颜色适应用户当前的配色方案。

一种方法是让我们的 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我们传递的实例包含的信息远不止所使用的配色方案。

真正很棒的是 SwiftUIColor并且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:),其工作方式完全相同。

有了上述内容,我们现在可以在任何我们想要的地方创建自适应ColorUIColor实例,只需执行以下操作:

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()
    }
}

如果我们当时就想,甚至采取进一步的事情,我们可以从我们的观点,将它们移到成采取同样的方式静态属性甚至完全抽象的我们对颜色的定义primarysecondary以及其他动态颜色的船作为系统的一部分:

extension Color {
    static var title: Self {
        Self(light: Color(white: 0.2),
             dark: Color(white: 0.8))
    }
}

以上绝对是我在构建应用程序时更喜欢使用的架构。每种颜色归纳整理成样特性titlebackgroundappTint,等等,我觉得这是更容易保持一个应用程序的颜色一致,组织良好的一段时间。

结论

定义自定义颜色最初似乎是一个很容易解决的问题,但随着我们的应用程序运行的执行环境变得越来越动态,我们必须使颜色适应这些不同环境的方式可能会继续增加在复杂性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值