Gaudí是一个用于UIKit主题管理的框架。它允许在运行时轻松切换主题,撤销通过UIAppearance
代理应用的主题。
Gaudí还提供了用于UIAppearance
规则和NSAttributedString
的DSL(领域特定语言)。
此框架使用语义颜色名称以便更好地适应暗黑模式以及其他可能在同一应用中存在的其他主题。
索引
显式颜色名称有什么问题?
没有问题,除非阅读red
时,人们会期望得到一个红色的色调,而阅读primary
时则没有期待。
此框架旨在使主题化变得简单。如果您使用黑色为文本着色,那么在暗黑模式主题中实际渲染为白色颜色会显得很奇怪。因此,我决定采用苹果关于使用语义颜色的建议,不仅是为了支持暗黑模式,而且还允许不同的主题协同工作,同时保持了一个从主题颜色和实际渲染颜色中的语义抽象层。
语义颜色
与设计师合作以确保这些内容的正确性。像苹果建议的那样,不要走捷径,也不要改变语义颜色的语义含义。高迪的SemanticColor
枚举清晰地提示了该颜色实际是什么
public enum SemanticColor: CaseIterable {
case label(LabelColor)
case fill(FillColor)
case background(BackgroundColor)
case groupedBackground(GroupedContentBackgroundColor)
case separator(SeparatorColor)
}
每个这些LabelColor
、FillColor
、BackgroundColor
、GroupedContentBackgroundColor
都有不同的特定语义颜色,如primary
、secondary
、tertiary
等。
不要用LabelColor
作为填充颜色。这将在您的项目中引入熵。与设计师密切合作,遵守此规范。当在代码中使用正确的SemanticColor
时,重skin 应用程序将只需更改20行代码。您还可以通过创建一个新的主题对象并更换新颜色来A/B 测试不同的主题。
自定义语义颜色
如果每个类别的特殊颜色不够用,您可以使用每个颜色枚举的特殊custom
情况创建自己的自定义颜色。为了避免代码中的重复,我建议扩展类别并定义一个静态变量,如下所示
public extension LabelColor {
static var myCustomSemanticColor: LabelColor {
return .custom(color: .color(fromHex: "#123456"))
}
}
如何创建主题
创建主题就像创建符合ThemeProtocol
协议的类一样简单。
public protocol ThemeProtocol: class {
var appearanceRules: AppearanceRuleSet { get }
// MARK: Colors
func color(forSemanticColor semanticColor: SemanticColor) -> UIColor
// MARK: Fonts
func font(forStyle style: FontStyle) -> UIFont
func fontSize(forStyle style: FontStyle) -> CGFloat
func kern(forStyle style: FontStyle) -> CGFloat
}
ThemeProtocol
的唯一要求是从SemanticColor
到UIColor
的映射函数,以及用于FontStyle
的等效映射函数。
外观规则集
外观规则集是通过使用UIAppearance
代理获得的协同出现规则集。
AppearanceRuleSet {
// UINavigationBar rules
UINavigationBar[\.barTintColor, self.color(forSemanticColor: .background(.primary))]
UINavigationBar[\.titleTextAttributes, [
.font: self.font(forStyle: .caption(attribute: .regular)),
.foregroundColor: self.color(forSemanticColor: .label(.primary))
]
]
// UITabBar rules
UITabBar[\.barTintColor, self.color(forSemanticColor: .background(.primary))]
UITabBarItem[\.badgeColor, self.color(forSemanticColor: .fill(.primary))]
UITabBarItem[
get: { $0.titleTextAttributes(for: .selected) },
set: { $0.setTitleTextAttributes($1, for: .selected) }
value: [
NSAttributedString.Key.font: self.font(forStyle: .caption(attribute: .regular)),
NSAttributedString.Key.foregroundColor: self.color(forSemanticColor: .label(.primary))
]
]
}
这是一个自定义应用中所有导航栏、所有标签栏及标签栏项外观的规则集。该领域特定语言(DSL)允许通过使用 KeyPath
来创建指向 UIAppearance
对象的自定义属性的规则。
您还可以使用嵌套的 AppearanceRuleSet
将其分组。
AppearanceRuleSet {
AppearanceRuleSet {
UINavigationBar[\.barTintColor, self.color(forSemanticColor: .background(.primary))]
UINavigationBar[\.titleTextAttributes, [
.font: self.font(forStyle: .caption(attribute: .regular)),
.foregroundColor: self.color(forSemanticColor: .label(.primary))
]
]
}
AppearanceRuleSet {
UITabBar[\.barTintColor, self.color(forSemanticColor: .background(.primary))]
UITabBarItem[\.badgeColor, self.color(forSemanticColor: .fill(.primary))]
UITabBarItem[
get: { $0.titleTextAttributes(for: .selected) },
set: { $0.setTitleTextAttributes($1, for: .selected) }
value: [
NSAttributedString.Key.font: self.font(forStyle: .caption(attribute: .regular)),
NSAttributedString.Key.foregroundColor: self.color(forSemanticColor: .label(.primary))
]
]
}
}
外观规则 DSL 也支持 if 和 else 语句。
外观规则集是可逆的。这意味着您可以在运行时将主题还原到默认设置。
如果您不需要全局外观为主题,则可以使用 .empty
外观规则集。
设置主题
一旦创建 Theme 对象,您就可以使用它了。将您的 Theme 分配到 AppDelegate 中的 ThemeContainer。
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
ThemeContainer.currentTheme = YourTheme()
}
高迪 提供了许多 UIKit 扩展,以轻松获取颜色和字体,轻松配置标签、按钮和字符串(NSAttributedString
)。例如,要设置一个标题标签,您可以使用以下方式:
label.applyLabelStyle(.title(.regular), semanticColor: .label(.primary))
这将更改 UILabel
文本的字体(和大致)和颜色。要获取语义颜色的颜色,也可以使用 UIColor 扩展:UIColor.semanticColor(.fill(.primary))
更改主题
与默认主题的初始化类似,您可以在代码的任何地方通过使用 ThemeContainer.currentTheme
变量来切换主题。
ThemeContainer.currentTheme = YourOtherTheme()
当发生这种情况时,Gaudí 会自动还原之前的 UIAppearance
规则,然后应用新规则,并在所有 Themed
视图控制器上调用 applyTheme
。
主题化的
为了使 Gaudí 在多主题应用程序中正确地工作,要求您在主题化的视图控制器中实现 Themed
协议。如果没有实现此协议,则任何存在的非主题化视图控制器实例在主题更改时外观不会改变。
将所有外观自定义放在所需的 applyTheme
函数中。对于 UITableView
/UICollectionView
单元格,如果在相应的数据源方法中自定义了它们的样式,那么只需在 applyTheme
函数中简单地调用 reloadData
即可刷新它们的颜色和字体。
使用 Gaudí 支持暗黑模式
在 Gaudí 中支持暗黑模式非常简单。有两种方式支持暗黑模式
- 实现两种不同的主题,并在
userInterfaceStyle
特性集合改变时切换它们。 - 实现一种独特的主题,该主题返回动态颜色。
现在我们将解释如何实现这两者。
ThemedWindow
如果您决定采用两种独立主题并按需在运行时切换,Gaudí 提供了一个定制的 UIWindow
来实现这一点。在您的应用程序代理中初始化 ThemedWindow
实例,并传递光亮模式和暗黑模式的主题。
Gaudí 将负责在运行时根据需要切换这两个主题。
动态颜色
如果您决定使用单一主题支持亮和暗黑模式,您将需要在你的主题的 color(forSemanticColor:)
映射函数中返回动态颜色。此框架为 UIColor
提供方便的初始化器来支持此用例
UIColor(lightColor: ..., darkColor: ...)
和
UIColor(lightColorHex: "#123456", darkColorHex: "#654321")
NSAttributedString
Gaudí 提供了一系列实用工具,利用 Domain Specific Language (DSL) 以声明式方式构建和组合 NSAttributedString
,这使得代码既易读又易组合。
假设我们想这样组合这个 attributedString
使用标准 API
let hello = NSAttributedString(string: "Hello, ", attributes: [.foregroundColor: UIColor.red])
let swift = NSAttributedString(string: "Swift", attributes: [
.foregroundColor: UIColor.orange,
.font: UIFont.systemFont(ofSize: 18)
])
let final = NSMutableAttributedString(attributedString: hello)
final.append(swift)
一个的 NSAttributedString
属性的类型是 [NSAttributedString.Key: Any]
。这种 Any
类型与难以智能补全的键一起使用,使得代码非常冗长。此外,值得注意的是,NSAttributedString
不是很容易和其他的 NSAttributedString
组合,因此使用 NSMutableAttributedString
是必须的。
使用 Gaudí DSL
let final = NSAttributedString {
"Hello, ".foreground(color: .red)
"Swift".foreground(color: .orange)
.font(.systemFont(ofSize: 18))
}
这就完成了... Gaudí 允许以非常简洁、类型安全和无噪音的方式来组合 NSAttributedString
,从裸 String
类型开始。该 DSL 也支持 if 和 if-else 语句。
let final = NSAttributedString {
"Hello, ".foreground(color: .red)
if swiftGreeting {
"Swift".foreground(color: .orange)
.font(.systemFont(ofSize: 18))
} else {
"World".foreground(color: .green)
.font(.systemFont(ofSize: 15))
}
}
NSAttributedString 属性
有时需要仅仅为了在后续代码中使用而组合一个属性字典。Gaudí 也为此提供帮助,带有 StringAttributes
DSL。
与 NSAttributedString
例子类似
let attributes = StringAttributes {
fontAttribute(withFont: .systemFont(ofSize: 25))
foregroundAttribute(withColor: .red)
}.attributes