Widget 的核心是 SwiftUI 视图。由于 Widget 在多种尺寸、系统模式和设备上展示,做好视图适配是打造高质量 Widget 的关键。
本篇将介绍 Widget 中 SwiftUI 视图的多尺寸适配、暗黑模式、动态字体和布局技巧。
1. 多尺寸适配策略
通过 widgetFamily 获取当前尺寸
swift
struct AdaptiveWidgetView: View {
@Environment(\.widgetFamily) var family
var entry: MyEntry
var body: some View {
switch family {
case .systemSmall: smallView
case .systemMedium: mediumView
case .systemLarge: largeView
case .accessoryCircular: circularView
case .accessoryRectangular: rectangularView
case .accessoryInline: inlineView
default: smallView
}
}
}
系统小尺寸设计要点
systemSmall (约 170×170 pt) 空间有限,设计原则:
swift
var smallView: some View {
VStack(spacing: 4) {
Image(systemName: entry.iconName)
.font(.title)
.foregroundColor(.accentColor)
Text(entry.title)
.font(.caption)
.lineLimit(1)
Text(entry.value)
.font(.title2)
.fontWeight(.bold)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(8)
}
设计原则:
- 突出一个核心数据(大字号)
- 图片与文字结合,别纯文字
- 避免超过 3 行信息
- 充分利用 SF Symbol 的图标表达能力
系统中尺寸设计要点
systemMedium (约 364×170 pt) 适合左右分栏:
swift
var mediumView: some View {
HStack(spacing: 12) {
// 左侧主内容
VStack(alignment: .leading, spacing: 4) {
Text(entry.title)
.font(.headline)
Text(entry.subtitle)
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text(entry.value)
.font(.title)
.fontWeight(.bold)
}
Spacer()
// 右侧辅助信息(如列表)
VStack(alignment: .leading, spacing: 2) {
ForEach(entry.items.prefix(4)) { item in
HStack(spacing: 4) {
Circle().fill(item.color).frame(width: 6, height: 6)
Text(item.name)
.font(.caption2)
.lineLimit(1)
}
}
}
}
.padding(12)
}
系统大尺寸设计要点
systemLarge (约 364×382 pt) 可以展示更丰富的内容:
swift
var largeView: some View {
VStack(alignment: .leading, spacing: 12) {
// 顶部概览
headerSection
Divider()
// 列表内容
ForEach(entry.items.prefix(6)) { item in
itemRow(item)
}
Spacer()
// 底部信息
footerSection
}
.padding(16)
}
2. 锁屏 Widget 的设计
accessoryCircular(圆形)
swift
var circularView: some View {
ZStack {
AccessoryWidgetBackground()
VStack(spacing: 0) {
Image(systemName: entry.iconName)
.font(.headline)
Text(entry.value)
.font(.system(size: 12, weight: .bold))
}
}
}
accessoryRectangular(矩形)
swift
var rectangularView: some View {
HStack {
VStack(alignment: .leading) {
Text(entry.title)
.font(.headline)
Text(entry.subtitle)
.font(.caption)
}
Spacer()
Text(entry.value)
.font(.title2)
}
}
accessoryInline(内联文本)
swift
var inlineView: some View {
ViewThatFits {
Text("\(Image(systemName: entry.iconName)) \(entry.title): \(entry.value)")
Text("\(Image(systemName: entry.iconName)) \(entry.value)")
}
}
注意:锁屏 Widget 的背景需要使用
AccessoryWidgetBackground(),系统会自动适配暗色和着色。
3. 暗黑模式适配
SwiftUI 会自动适配 Dark Mode,但在 Widget 中需要注意以下特殊点:
使用语义化颜色
swift
// 推荐:语义化颜色,自动适配 dark/light
Text("标题")
.foregroundColor(.primary)
Text("副标题")
.foregroundColor(.secondary)
// 推荐:使用 Color Asset,支持 dark/light 变体
Color("WidgetBackground") // 在 Asset Catalog 中定义
// 避免:硬编码颜色
Text("标题")
.foregroundColor(.black) // dark mode 下不可见
自定义 Asset Catalog 颜色
在 Widget Extension 的 Assets 中添加 Color Set,分别为 Dark/Light 指定颜色,然后通过名称引用。
锁屏 Widget 的特殊色彩
锁屏 Widget 在不同锁屏壁纸上可能被系统着色,使用 .widgetAccentable() 控制:
swift
Image(systemName: "star.fill")
.widgetAccentable() // 允许系统着色
Text("重要信息")
.foregroundColor(.primary) // 主色不受着色影响
4. 动态字体适配
Widget 应尊重用户的系统字体大小偏好:
swift
// 使用 dynamic type 支持的字体大小
Text("标题")
.font(.headline) // ✅ 支持动态字体
Text("正文")
.font(.body) // ✅ 支持动态字体
// 避免硬编码字体大小
Text("标题")
.font(.system(size: 16)) // ❌ 不会动态缩放
// 如果有特殊需求,至少使用相对大小
Text("标题")
.font(.system(.headline, design: .rounded))
ViewThatFits 自动选择合适布局
swift
var dynamicContentView: some View {
ViewThatFits(in: .vertical) {
// 尝试完整布局
VStack {
Text(entry.title).font(.headline)
Text(entry.subtitle).font(.caption)
Text(entry.detail).font(.caption2)
}
// 降级到紧凑布局
VStack {
Text(entry.title).font(.headline)
Text(entry.subtitle).font(.caption)
}
// 最精简
Text(entry.title).font(.headline)
}
}
5. iOS 17+ containerBackground
从 iOS 17 开始,Apple 强制要求使用 .containerBackground 代替 .background():
swift
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(...) { entry in
MyWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
}
}
如果还需要支持 iOS 16 及以下,使用条件编译:
swift
if #available(iOS 17.0, *) {
MyWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
MyWidgetView(entry: entry)
.padding()
.background(.regularMaterial)
}
6. 布局调试技巧
使用 SwiftUI Preview 多尺寸预览
swift
#Preview("Small", as: .systemSmall) {
MyWidget()
} timeline: {
MyEntry.sample
}
#Preview("Medium", as: .systemMedium) {
MyWidget()
} timeline: {
MyEntry.sample
}
#Preview("Large", as: .systemLarge) {
MyWidget()
} timeline: {
MyEntry.sample
}
#Preview("Circular", as: .accessoryCircular) {
MyWidget()
} timeline: {
MyEntry.sample
}
使用背景色调试布局区域
swift
// 开发时临时使用背景色查看布局区域
Color.blue.opacity(0.2) // 查看视图所占空间
Color.red.opacity(0.2) // 查看父容器边界
7. 设计检查清单
| 检查项 | 状态 |
|---|---|
| 所有支持的尺寸都有对应视图 | |
| Dark Mode 下颜色可辨识 | |
| 大字体(辅助功能)下不错乱 | |
| systemSmall 下信息密度合理 | |
| 锁屏 Widget 使用 AccessoryWidgetBackground | |
| iOS 17+ 使用 containerBackground | |
| 使用 SF Symbols 而非图片资源 | |
| 点击区域区分清晰(Link) |
小结
- 通过
@Environment(\.widgetFamily)获取当前尺寸,针对性设计 - 锁屏 Widget 注意使用
AccessoryWidgetBackground()和.widgetAccentable() - 使用语义化颜色和动态字体,支持 Dark Mode 和 Accessibility
- iOS 17+ 必须使用
.containerBackground - 充分使用 SwiftUI Preview 在开发阶段做多尺寸验证
上一篇 :iOS Widget 开发-17:Widget 错误处理与空状态设计
下一篇 :iOS Widget 开发-19:Widget 调试与单元测试