swiftui-design-principles by arjitj2/swiftui-design-principles
npx skills add https://github.com/arjitj2/swiftui-design-principles --skill swiftui-design-principles此技能编码了通过对比精良的生产级 SwiftUI 应用与构建不佳的应用所提炼出的设计原则。这里的模式代表了区分一个感觉"恰到好处"的应用与一个边距、间距和文本尺寸看起来"不对劲"的应用的关键所在。
在构建或修改 SwiftUI 界面、WidgetKit 小组件或任何原生 Apple UI 时,请应用这些原则。
克制优于装饰。 每个像素都必须有其存在的价值。一个精良的应用使用更少的颜色、更少的字体大小和更少的间距值——但始终如一地使用它们。过度设计视觉元素(自定义渐变、装饰性边框、定制的分隔线)会产生视觉噪音。原生组件和系统颜色能创造和谐。
关键:使用基于 4/8 的网格间距值。切勿使用任意值。
4, 8, 12, 16, 20, 24, 32, 40, 48
// 错误 - 这些数字彼此之间没有关联
.padding(.bottom, 26)
.padding(.bottom, 34)
.padding(.bottom, 36)
HStack(spacing: 18)
.padding(14)
// 正确 - 眼睛可以遵循的可预测节奏
.padding(.horizontal, 20)
.padding(.top, 8)
Spacer().frame(height: 32)
HStack(spacing: 4) // 或 8, 12, 16
.padding(.vertical, 12)
.padding(.horizontal, 16)
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
使用更少的字体大小和清晰的字重区分。较大尺寸使用较轻字重;较小尺寸使用中等/常规字重。这能营造精致感,而非视觉混乱。
| 角色 | 尺寸 | 字重 | 备注 |
|---|---|---|---|
| 主标题数字 | 36-42pt | .light | 大但视觉上轻——优雅,不沉重 |
| 次要统计数据 | 20-24pt | .light | 与主标题相同的字重系列,尺寸更小 |
| 正文 / 开关标签 | 15pt | .regular | 标准 iOS 正文尺寸 |
| 分区标题(大写) | 11pt | .medium | 配合字距/字母间距 |
| 说明文字 / 副标题 | 11-13pt | .regular | 次要信息 |
// 错误 - 7 种不同尺寸,没有清晰的系统
.font(.system(size: 60, weight: .ultraLight)) // 主标题
.font(.system(size: 44, weight: .regular)) // 统计数据(与主标题太接近)
.font(.system(size: 31, weight: .ultraLight)) // 百分比符号(奇怪的比例)
.font(.system(size: 18, weight: .regular)) // 标签(对于开关来说太大)
.font(.system(size: 14, weight: .regular)) // 标题
.font(.system(size: 13, weight: .regular)) // 另一个标题
.font(.system(size: 12, weight: .regular)) // 按钮(太小难以阅读)
// 正确 - 5 种尺寸,每种都有明确用途
.font(.system(size: 42, weight: .light, design: .monospaced)) // 主标题
.font(.system(size: 24, weight: .light, design: .monospaced)) // 统计值
.font(.system(size: 15, weight: .regular, design: .monospaced)) // 正文
.font(.system(size: 14, weight: .regular, design: .monospaced)) // 次要
.font(.system(size: 11, weight: .medium, design: .monospaced)) // 标签
选择一种字体设计并在各处使用——应用和小组件:
// 如果使用等宽字体,就在所有地方使用它
design: .monospaced // 应用视图、小组件、锁屏界面——全部使用
// 切勿在应用和小组件之间混合使用不同的设计
// 错误:应用中使用 .monospaced,锁屏小组件中使用 .rounded
最多使用 2 个值,并且仅用于大写标签:
.tracking(1.5) // 分区标签:"NOTIFICATIONS"、"DAY"、"LEFT"
.tracking(3) // 导航栏/工具栏标题
切勿使用 3 种以上不同的字距值,例如 kerning(4)、kerning(4.5)、kerning(5)——这些差异难以察觉,但潜意识里会感受到不一致。
年份和其他固定标识符不应进行本地化分组。
// 正确 - 稳定、未分组的标识符文本
Text(String(year)) // "2026"
Text(year, format: .number.grouping(.never))
// 错误 - 本地化分组可能显示为 "2,026"
Text("\(year)")
使用 SwiftUI 的语义颜色系统。它能自动处理浅色/深色模式、辅助功能,并且看起来是原生的。带有手动不透明度值的硬编码颜色会造成维护噩梦,并且看起来不自然。
// 错误 - 无法维护,无法适应浅色模式
Color.black.ignoresSafeArea() // 强制深色
Color.white.opacity(0.08) // 环形背景
Color.white.opacity(0.09) // 分隔线
Color.white.opacity(0.3) // 年份文本
Color.white.opacity(0.32) // 统计标签
Color.white.opacity(0.42) // 百分比符号
Color.white.opacity(0.44) // 开关色调
Color.white.opacity(0.72) // 按钮文本
Color.white.opacity(0.88) // 开关标签
Color.white.opacity(0.9) // 统计值
Color.white.opacity(0.94) // 环形填充
// 正确 - 自动适应,看起来原生,易于维护
Color(.systemBackground) // 主背景
Color(.secondarySystemBackground) // 卡片/分组背景
Color(.separator) // 分隔线(可选择不透明度)
Color.primary // 主要文本和 UI 元素
.foregroundStyle(.secondary) // 次要文本
.foregroundStyle(.tertiary) // 标签、说明文字
限制在 2-3 个具有明确用途的值:
.opacity(0.15) // 微妙的背景描边
.opacity(0.3) // 分隔线
// 仅此而已。如果需要更多,你可能是在硬编码语义颜色本应处理的内容。
// 应用主视图:200x200,细描边
.frame(width: 200, height: 200)
Circle().stroke(..., lineWidth: 3)
// 小组件(systemSmall):90x90,相同描边
.frame(width: 90, height: 90)
Circle().stroke(..., lineWidth: 3)
// 错误:过大的环,描边粗细不一致
.frame(width: 260, height: 260) // 太大,主导屏幕
Circle().stroke(..., lineWidth: 9) // 背景
Circle().stroke(..., lineWidth: 8) // 填充——为什么与背景不同?
对于同一元素的背景和前景描边,始终使用相同的 lineWidth:
// 正确
Circle().stroke(background, lineWidth: 3)
Circle().trim(from: 0, to: fraction).stroke(fill, lineWidth: 3)
// 错误 - 造成视觉错位
Circle().stroke(background, lineWidth: 9)
Circle().trim(from: 0, to: fraction).stroke(fill, lineWidth: 8)
// 正确 - 自然尺寸,带有适当的内边距
Toggle(isOn: $value) {
Text(title)
.font(.system(size: 15, weight: .regular, design: .monospaced))
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
// 错误 - 固定的过大高度
HStack {
Text(label)
.font(.system(size: 18)) // 对于开关标签来说太大
Spacer()
Toggle("", isOn: $isOn)
.labelsHidden() // 为什么要隐藏标签?正确使用 Toggle
}
.frame(height: 70) // 太高了
// 错误 - 自定义渐变、覆盖边框、巨大的圆角半径
VStack { ... }
.padding(.vertical, 4) // 太紧凑
.background(
RoundedRectangle(cornerRadius: 22) // 太圆
.fill(LinearGradient( // 不必要的渐变
colors: [Color(white: 0.10), Color(white: 0.085)],
startPoint: .topLeading, endPoint: .bottomTrailing
))
)
.overlay(
RoundedRectangle(cornerRadius: 22)
.stroke(Color.white.opacity(0.08), lineWidth: 1) // 装饰性边框
)
// 正确 - 简单、原生、适用于浅色和深色模式
VStack(spacing: 0) {
row1
Divider().padding(.leading, 16)
row2
Divider().padding(.leading, 16)
row3
}
.background(Color(.secondarySystemBackground))
.clipShape(.rect(cornerRadius: 10))
Divider() 并配合 .padding(.leading, 16) 实现 iOS 标准缩进。切勿构建自定义分隔线结构体。Color(.secondarySystemBackground)——切勿为标准卡片使用自定义渐变。// 正确 - 适当的导航,工具栏极简
NavigationStack {
ScrollView {
content
}
.toolbar {
ToolbarItem(placement: .principal) {
Text("标题")
.font(.system(size: 13, weight: .medium, design: .monospaced))
.tracking(3)
.foregroundStyle(.tertiary)
}
}
.navigationBarTitleDisplayMode(.inline)
}
// 错误 - 没有导航结构,只有 ZStack
ZStack {
Color.black.ignoresSafeArea()
ScrollView {
VStack {
Text("2026").font(...) // 手动放置的"标题"
content
}
}
}
// 正确 - 使用 Gauge,它是为此目的而构建的
Gauge(value: entry.fraction) {
Text("")
} currentValueLabel: {
Text("\(Int(entry.percentage))%")
.font(.system(size: 12, weight: .medium, design: .monospaced))
}
.gaugeStyle(.accessoryCircular)
.containerBackground(.fill.tertiary, for: .widget)
// 错误 - 为锁屏界面手动绘制圆形
ZStack {
Circle().stroke(Color.primary.opacity(0.18), lineWidth: 4)
Circle().trim(from: 0, to: progress).stroke(...)
Text(percentText)
.font(.system(size: 14, weight: .bold, design: .rounded)) // 错误的字体设计!
}
// 正确 - 使用带有 linearCapacity 的 Gauge
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(year).font(.system(size: 13, weight: .semibold, design: .monospaced))
Spacer()
Text(percentage).font(.system(size: 13, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
}
Gauge(value: fraction) { Text("") }
.gaugeStyle(.linearCapacity)
.tint(.primary)
HStack {
Spacer()
Text("\(dayOfYear)/\(totalDays)")
.font(.system(size: 11, weight: .regular, design: .monospaced))
.foregroundStyle(.secondary)
}
}
.containerBackground(.fill.tertiary, for: .widget)
// 错误 - 自定义 GeometryReader 进度条
GeometryReader { proxy in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 2).fill(Color.primary.opacity(0.16))
RoundedRectangle(cornerRadius: 2).fill(Color.primary)
.frame(width: max(2, proxy.size.width * progress))
}
}
.frame(height: 6)
// 正确
.containerBackground(.fill.tertiary, for: .widget)
// 错误 - 硬编码颜色
.containerBackground(.black, for: .widget)
支持所有相关系列——不要跳过常见的系列:
.supportedFamilies([
.accessoryCircular, // 锁屏圆形
.accessoryRectangular, // 锁屏矩形
.accessoryInline, // 锁屏内联文本
.systemSmall, // 主屏幕小尺寸
.systemMedium, // 主屏幕中尺寸
.systemLarge, // 主屏幕大尺寸
])
中尺寸和大尺寸主屏幕小组件应共享相同的结构布局:
日/总数 右对齐除非有严格的尺寸限制,否则不要为每个系列重新设计层级。
始终在主屏幕小组件上包含明确的内边距,以避免在圆角边缘附近被裁剪:
.padding(.horizontal, 12)
.padding(.vertical, 12)
小组件扩展有严格的内存预算(通常约为 30 MB)。如果由太多嵌套视图构建,密集的可视化内容可能会因 EXC_RESOURCE 而被终止。
// 正确 - 一次性绘制密集的点网格
Canvas { context, size in
// 在此处绘制 365/366 个点
}
// 错误 - 数百个嵌套子视图(高内存开销)
LazyVGrid(columns: columns) {
ForEach(1...366, id: \.self) { day in
ZStack { Circle(); partialFillLayer }
}
}
// 正确 - 对于日级数据,在午夜刷新
let tomorrow = calendar.startOfDay(for: calendar.date(byAdding: .day, value: 1, to: now)!)
Timeline(entries: [entry], policy: .after(tomorrow))
// 正确 - 对于依赖于一天中时间的百分比/部分填充,进行周期性刷新
let refresh = Calendar.current.date(byAdding: .minute, value: 15, to: now)!
Timeline(entries: [entry], policy: .after(refresh))
// 错误 - 对于静态的每日数据,进行分钟级刷新
let tooFrequent = Calendar.current.date(byAdding: .minute, value: 1, to: now)!
Timeline(entries: [entry], policy: .after(tooFrequent))
// 正确 - 使用 Toggle 及其内置标签,使用单一强调色进行着色
Toggle(isOn: $value) {
Text(title)
.font(.system(size: 15, weight: .regular, design: .monospaced))
}
.tint(.green)
// 错误 - 隐藏标签,使用手动 HStack 布局
HStack {
Text(label).font(.system(size: 18))
Spacer()
Toggle("", isOn: $isOn)
.labelsHidden()
.tint(Color.white.opacity(0.44)) // 低对比度色调
}
当选项互斥时(例如,每日/每周/每月频率),使用一个选中的值,而不是三个独立的开关。
// 正确 - 单一事实来源
enum Cadence: String, CaseIterable { case daily, weekly, monthly }
@State private var cadence: Cadence = .daily
ForEach(Cadence.allCases, id: \.rawValue) { option in
Button {
cadence = option
} label: {
HStack {
Image(systemName: cadence == option ? "checkmark.circle.fill" : "circle")
Text(option.rawValue.capitalized)
}
}
}
// 正确 - 共享内容时只有一个预览操作
Button("预览") { sendPreview() }
// 错误 - 独立的开关允许矛盾的状态
Toggle("每日", isOn: $daily)
Toggle("每周", isOn: $weekly)
Toggle("每月", isOn: $monthly)
// 添加到任何显示变化数字值的 Text
Text(String(format: "%.2f", percentage))
.contentTransition(.numericText())
// 正确 - 一个模型用于所有地方
struct YearProgress {
// 共享的计算逻辑
static func current() -> YearProgress { ... }
}
// 由 ContentView 和小组件 TimelineProvider 使用
// 如果百分比显示为实时进度,则在共享计算中包含一天中的时间
let dayProgress = elapsedInCurrentDay / totalSecondsInDay
let elapsedDays = Double(dayOfYear - 1) + dayProgress
let fraction = elapsedDays / Double(totalDays)
// 错误 - 具有重复日期计算逻辑的独立快照结构体
struct YearProgressSnapshot { ... } // 在应用中
struct YearProgressWidgetSnapshot { ... } // 在小组件扩展中(重复!)
在发布任何 SwiftUI 视图之前,请验证:
Color(.secondarySystemBackground) 和 10pt 圆角半径Divider() 并带有 leading 内边距.labelsHidden())Gauge(而非手动绘制圆形).containerBackground(.fill.tertiary, for: .widget)Canvas 或类似的轻量级渲染方式minimumScaleFactor 这种变通方法——而是修复布局每周安装数
41
仓库
首次出现
2026年2月24日
安全审计
安装于
gemini-cli41
amp41
github-copilot41
codex41
kimi-cli41
opencode41
This skill encodes design principles derived from comparing polished, production-quality SwiftUI apps against poorly-built ones. The patterns here represent what separates an app that feels "right" from one where the margins, spacing, and text sizes just look "off."
Apply these principles whenever building or modifying SwiftUI interfaces, WidgetKit widgets, or any native Apple UI.
Restraint over decoration. Every pixel must earn its place. A polished app uses fewer colors, fewer font sizes, and fewer spacing values — but uses them consistently. Over-engineering visual elements (custom gradients, decorative borders, bespoke dividers) creates visual noise. Native components and system colors create harmony.
CRITICAL : Use spacing values from a base-4/base-8 grid. Never use arbitrary values.
4, 8, 12, 16, 20, 24, 32, 40, 48
// WRONG - these numbers have no relationship to each other
.padding(.bottom, 26)
.padding(.bottom, 34)
.padding(.bottom, 36)
HStack(spacing: 18)
.padding(14)
// RIGHT - predictable rhythm the eye can follow
.padding(.horizontal, 20)
.padding(.top, 8)
Spacer().frame(height: 32)
HStack(spacing: 4) // or 8, 12, 16
.padding(.vertical, 12)
.padding(.horizontal, 16)
Use fewer font sizes with clear weight differentiation. Lighter weights at larger sizes; medium/regular at smaller sizes. This creates sophistication rather than visual chaos.
| Role | Size | Weight | Notes |
|---|---|---|---|
| Hero number | 36-42pt | .light | Large but visually light -- elegant, not heavy |
| Secondary stat | 20-24pt | .light | Same weight family as hero, smaller |
| Body / toggle label | 15pt | .regular | Standard iOS body size |
| Section header (uppercase) | 11pt | .medium | With tracking/letter-spacing |
// WRONG - 7 different sizes with no clear system
.font(.system(size: 60, weight: .ultraLight)) // hero
.font(.system(size: 44, weight: .regular)) // stat (too close to hero)
.font(.system(size: 31, weight: .ultraLight)) // percent symbol (odd ratio)
.font(.system(size: 18, weight: .regular)) // label (too big for a toggle)
.font(.system(size: 14, weight: .regular)) // header
.font(.system(size: 13, weight: .regular)) // another header
.font(.system(size: 12, weight: .regular)) // button (too small to read)
// RIGHT - 5 sizes, clear purpose for each
.font(.system(size: 42, weight: .light, design: .monospaced)) // hero
.font(.system(size: 24, weight: .light, design: .monospaced)) // stat value
.font(.system(size: 15, weight: .regular, design: .monospaced)) // body
.font(.system(size: 14, weight: .regular, design: .monospaced)) // secondary
.font(.system(size: 11, weight: .medium, design: .monospaced)) // label
Pick ONE font design and use it everywhere -- app AND widgets:
// If using monospaced, use it everywhere
design: .monospaced // app views, widgets, lock screen -- all of them
// NEVER mix designs between app and widgets
// BAD: .monospaced in app, .rounded in lock screen widget
Use at most 2 values, and only on uppercase labels:
.tracking(1.5) // section labels: "NOTIFICATIONS", "DAY", "LEFT"
.tracking(3) // navigation/toolbar titles
Never use 3+ different tracking values like kerning(4), kerning(4.5), kerning(5) -- the differences are imperceptible but the inconsistency registers subconsciously.
Years and other fixed identifiers should not be locale-grouped.
// RIGHT - stable, non-grouped identifier text
Text(String(year)) // "2026"
Text(year, format: .number.grouping(.never))
// WRONG - locale grouping can render "2,026"
Text("\(year)")
Use SwiftUI's semantic color system. It automatically handles light/dark mode, accessibility, and looks native. Hardcoded colors with manual opacity values create maintenance nightmares and look artificial.
// WRONG - impossible to maintain, doesn't adapt to light mode
Color.black.ignoresSafeArea() // forced dark
Color.white.opacity(0.08) // ring background
Color.white.opacity(0.09) // divider
Color.white.opacity(0.3) // year text
Color.white.opacity(0.32) // stat label
Color.white.opacity(0.42) // percent symbol
Color.white.opacity(0.44) // toggle tint
Color.white.opacity(0.72) // button text
Color.white.opacity(0.88) // toggle label
Color.white.opacity(0.9) // stat value
Color.white.opacity(0.94) // ring fill
// RIGHT - adapts automatically, looks native, easy to maintain
Color(.systemBackground) // main background
Color(.secondarySystemBackground) // card/group backgrounds
Color(.separator) // dividers (with optional opacity)
Color.primary // primary text and UI elements
.foregroundStyle(.secondary) // secondary text
.foregroundStyle(.tertiary) // labels, captions
Limit to 2-3 values with clear purposes:
.opacity(0.15) // subtle background strokes
.opacity(0.3) // separator lines
// That's it. If you need more, you're probably hardcoding what semantic colors handle.
// App main view: 200x200 with thin stroke
.frame(width: 200, height: 200)
Circle().stroke(..., lineWidth: 3)
// Widget (systemSmall): 90x90, same stroke
.frame(width: 90, height: 90)
Circle().stroke(..., lineWidth: 3)
// WRONG: oversized ring with thick inconsistent strokes
.frame(width: 260, height: 260) // too large, dominates screen
Circle().stroke(..., lineWidth: 9) // background
Circle().stroke(..., lineWidth: 8) // fill -- WHY different from background?
Always use the same lineWidth for background and foreground strokes of the same element:
// RIGHT
Circle().stroke(background, lineWidth: 3)
Circle().trim(from: 0, to: fraction).stroke(fill, lineWidth: 3)
// WRONG - creates visual misalignment
Circle().stroke(background, lineWidth: 9)
Circle().trim(from: 0, to: fraction).stroke(fill, lineWidth: 8)
// RIGHT - natural sizing with proper padding
Toggle(isOn: $value) {
Text(title)
.font(.system(size: 15, weight: .regular, design: .monospaced))
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
// WRONG - fixed oversized height
HStack {
Text(label)
.font(.system(size: 18)) // too big for a toggle label
Spacer()
Toggle("", isOn: $isOn)
.labelsHidden() // why hide the label? Use Toggle properly
}
.frame(height: 70) // way too tall
// WRONG - custom gradient, overlay border, huge corner radius
VStack { ... }
.padding(.vertical, 4) // too tight
.background(
RoundedRectangle(cornerRadius: 22) // too round
.fill(LinearGradient( // unnecessary gradient
colors: [Color(white: 0.10), Color(white: 0.085)],
startPoint: .topLeading, endPoint: .bottomTrailing
))
)
.overlay(
RoundedRectangle(cornerRadius: 22)
.stroke(Color.white.opacity(0.08), lineWidth: 1) // decorative border
)
// RIGHT - simple, native, works in light and dark mode
VStack(spacing: 0) {
row1
Divider().padding(.leading, 16)
row2
Divider().padding(.leading, 16)
row3
}
.background(Color(.secondarySystemBackground))
.clipShape(.rect(cornerRadius: 10))
Divider() with .padding(.leading, 16) for iOS-standard inset. Never build custom divider structs.Color(.secondarySystemBackground) -- never custom gradients for standard cards.// RIGHT - proper navigation with minimal toolbar
NavigationStack {
ScrollView {
content
}
.toolbar {
ToolbarItem(placement: .principal) {
Text("Title")
.font(.system(size: 13, weight: .medium, design: .monospaced))
.tracking(3)
.foregroundStyle(.tertiary)
}
}
.navigationBarTitleDisplayMode(.inline)
}
// WRONG - no navigation structure, just a ZStack
ZStack {
Color.black.ignoresSafeArea()
ScrollView {
VStack {
Text("2026").font(...) // manually placed "title"
content
}
}
}
// RIGHT - use Gauge, it's purpose-built for this
Gauge(value: entry.fraction) {
Text("")
} currentValueLabel: {
Text("\(Int(entry.percentage))%")
.font(.system(size: 12, weight: .medium, design: .monospaced))
}
.gaugeStyle(.accessoryCircular)
.containerBackground(.fill.tertiary, for: .widget)
// WRONG - manual circle drawing for lock screen
ZStack {
Circle().stroke(Color.primary.opacity(0.18), lineWidth: 4)
Circle().trim(from: 0, to: progress).stroke(...)
Text(percentText)
.font(.system(size: 14, weight: .bold, design: .rounded)) // wrong font design!
}
// RIGHT - use Gauge with linearCapacity
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(year).font(.system(size: 13, weight: .semibold, design: .monospaced))
Spacer()
Text(percentage).font(.system(size: 13, weight: .medium, design: .monospaced))
.foregroundStyle(.secondary)
}
Gauge(value: fraction) { Text("") }
.gaugeStyle(.linearCapacity)
.tint(.primary)
HStack {
Spacer()
Text("\(dayOfYear)/\(totalDays)")
.font(.system(size: 11, weight: .regular, design: .monospaced))
.foregroundStyle(.secondary)
}
}
.containerBackground(.fill.tertiary, for: .widget)
// WRONG - custom GeometryReader progress bar
GeometryReader { proxy in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 2).fill(Color.primary.opacity(0.16))
RoundedRectangle(cornerRadius: 2).fill(Color.primary)
.frame(width: max(2, proxy.size.width * progress))
}
}
.frame(height: 6)
// RIGHT
.containerBackground(.fill.tertiary, for: .widget)
// WRONG - hardcoded color
.containerBackground(.black, for: .widget)
Support all relevant families -- don't skip common ones:
.supportedFamilies([
.accessoryCircular, // lock screen circle
.accessoryRectangular, // lock screen rectangle
.accessoryInline, // lock screen inline text
.systemSmall, // home screen small
.systemMedium, // home screen medium
.systemLarge, // home screen large
])
Medium and large home widgets should share the same structural layout:
day/total right alignedDo not re-invent hierarchy per family unless there is a hard size constraint.
Always include explicit internal padding on home widgets to avoid clipping near rounded edges:
.padding(.horizontal, 12)
.padding(.vertical, 12)
Widget extensions have a tight memory budget (commonly around 30 MB). Dense visualizations can be killed by EXC_RESOURCE if built from too many nested views.
// RIGHT - draw dense dot grids in one pass
Canvas { context, size in
// draw 365/366 dots here
}
// WRONG - hundreds of nested subviews (high memory overhead)
LazyVGrid(columns: columns) {
ForEach(1...366, id: \.self) { day in
ZStack { Circle(); partialFillLayer }
}
}
// RIGHT - refresh at midnight for day-level data
let tomorrow = calendar.startOfDay(for: calendar.date(byAdding: .day, value: 1, to: now)!)
Timeline(entries: [entry], policy: .after(tomorrow))
// RIGHT - periodic refresh for time-of-day dependent percentages/partial fills
let refresh = Calendar.current.date(byAdding: .minute, value: 15, to: now)!
Timeline(entries: [entry], policy: .after(refresh))
// WRONG - minute-level refresh for static daily data
let tooFrequent = Calendar.current.date(byAdding: .minute, value: 1, to: now)!
Timeline(entries: [entry], policy: .after(tooFrequent))
// RIGHT - use Toggle with its built-in label, tint with a single accent color
Toggle(isOn: $value) {
Text(title)
.font(.system(size: 15, weight: .regular, design: .monospaced))
}
.tint(.green)
// WRONG - hidden label with manual HStack layout
HStack {
Text(label).font(.system(size: 18))
Spacer()
Toggle("", isOn: $isOn)
.labelsHidden()
.tint(Color.white.opacity(0.44)) // low-contrast tint
}
When options are exclusive (e.g. daily/weekly/monthly cadence), use one selected value, not three independent toggles.
// RIGHT - single source of truth
enum Cadence: String, CaseIterable { case daily, weekly, monthly }
@State private var cadence: Cadence = .daily
ForEach(Cadence.allCases, id: \.rawValue) { option in
Button {
cadence = option
} label: {
HStack {
Image(systemName: cadence == option ? "checkmark.circle.fill" : "circle")
Text(option.rawValue.capitalized)
}
}
}
// RIGHT - one preview action when content is shared
Button("Preview") { sendPreview() }
// WRONG - independent toggles allow contradictory state
Toggle("Daily", isOn: $daily)
Toggle("Weekly", isOn: $weekly)
Toggle("Monthly", isOn: $monthly)
// Add to any Text that displays a changing numeric value
Text(String(format: "%.2f", percentage))
.contentTransition(.numericText())
// RIGHT - one model used everywhere
struct YearProgress {
// shared calculation logic
static func current() -> YearProgress { ... }
}
// Used by both ContentView and widget TimelineProvider
// If percentage is shown as live progress, include time-of-day in shared math
let dayProgress = elapsedInCurrentDay / totalSecondsInDay
let elapsedDays = Double(dayOfYear - 1) + dayProgress
let fraction = elapsedDays / Double(totalDays)
// WRONG - separate snapshot structs with duplicated date math
struct YearProgressSnapshot { ... } // in app
struct YearProgressWidgetSnapshot { ... } // in widget extension (duplicated!)
Before shipping any SwiftUI view, verify:
Color(.secondarySystemBackground) with 10pt corner radiusDivider() with leading padding.labelsHidden())Gauge (not manual circle drawing).containerBackground(.fill.tertiary, for: .widget)Canvas or similarly lightweight renderingWeekly Installs
41
Repository
First Seen
Feb 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli41
amp41
github-copilot41
codex41
kimi-cli41
opencode41
Flutter布局指南:构建响应式UI的约束规则与自适应设计模式
1,200 周安装
scikit-bio:Python生物信息学分析库,处理序列、比对、系统发育与多样性分析
163 周安装
Loom视频转录获取器 - 自动提取Loom视频字幕与文本,支持GraphQL API
163 周安装
bioRxiv数据库Python工具:高效搜索下载预印本,支持关键词/作者/日期/类别筛选
163 周安装
Magento 2 Hyvä CMS 组件创建器 - 快速构建自定义CMS组件
163 周安装
项目文档协调器 - 自动化文档生成与上下文管理工具
163 周安装
GPUI 布局与样式:Rust 类型安全的 CSS 样式库,Flexbox 布局与链式 API
163 周安装
| Caption / subtitle | 11-13pt | .regular | Secondary information |
minimumScaleFactor hacks -- fix the layout instead