阅读视图
Swift Concurrency:彻底告别“线程思维”,拥抱 Task 的世界
深入理解 Swift 中的 async/await:告别回调地狱,拥抱结构化并发
深入理解 SwiftUI 的 ViewBuilder:从隐式语法到自定义容器
在 async/throwing 场景下优雅地使用 Swift 的 defer 关键字
我差点失去了巴顿(我的狗狗) | 肘子的 Swift 周报 #098
当Swift Codable遇到缺失字段:优雅解决数据解码难题
我差点失去了巴顿(我的狗狗) - 肘子的 Swift 周报 #98
巴顿已经 13 岁了。尽管大多数时候他都表现出远超同龄狗狗的活力和状态,但随着年龄增长,各种健康问题也随之而来。不久前,巴顿被检查出肺动脉高压,医生给出了针对性的治疗方案。就在我为治疗似乎初见成效而欣慰时,上周一下午,巴顿突然无法站立,大量流口水,表现出明显的心脏不适。
iOS26适配指南之UIButton
SwiftUI 劝退实录:AI 都无能为力,你敢用吗?
Swift 结构体属性:let 与 var 的选择艺术
使用 Swift 的 defer 管理状态清理(译文)
把 GPT 塞进 iPhone:iOS 26 的 Foundation Models 框架全解析
用 SwiftUI 打造“会长大”的组件 —— 从一次性 Alert 到可扩展设计系统
原文链接
为什么旧写法撑不过三次迭代?
先来看一个“经典”写法
Alert(
title: "Title",
message: "Description",
type: .info,
showBorder: true,
isDisabled: false,
primaryButtonTitle: "OK",
secondaryButtonTitle: "Cancel",
primaryAction: { /* ... */ },
secondaryAction: { /* ... */ }
)
痛点一句话总结:初始化即地狱。
• 参数爆炸,阅读困难
• 布局/样式/行为耦合,一改全改
• 无法注入自定义内容,复用性 ≈ 0
目标:像原生一样的 SwiftUI 组件
我们想要的最终形态:
AlertView(title: "...", message: "...") {
AnyViewBuilder Content
}
.showBorder(true)
.disabled(isLoading)
为此,需要遵循 4 个关键词:
- Familiar APIs – 看起来像 SwiftUI 自带的
- Composability – 任意组合内容
- Scalability – 业务扩张不炸窝
- Accessibility – 无障碍不打补丁
三步重构法
Step 1:只保留「必须参数」
public struct AlertView: View {
private let title: String
private let message: String
public init(title: String, message: String) {
self.title = title
self.message = message
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(title).font(.headline)
Text(message).font(.subheadline)
}
.padding()
}
}
经验:先把最常用、不可省略的参数放进 init,其余全部踢出去。这一步就能干掉 70% 的参数。
Step 2:用 @ViewBuilder 把“内容”交出去
public struct AlertView<Footer: View>: View {
private let title: String
private let message: String
private let footer: Footer
public init(
title: String,
message: String,
@ViewBuilder footer: () -> Footer
) {
self.title = title
self.message = message
self.footer = footer()
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(title).font(.headline)
Text(message).font(.subheadline)
footer.padding(.top, 25)
}
.padding()
}
}
使用:
AlertView(title: "提示", message: "确定删除吗?") {
HStack {
Button("取消", role: .cancel) {}
Button("删除", role: .destructive) {}
}
}
Step 3:样式/行为用 环境值 + 自定义修饰符
我们想让边框可开关,但又不想回到“参数爆炸”。
struct ShowBorderKey: EnvironmentKey {
static let defaultValue = false
}
extension EnvironmentValues {
var showBorder: Bool {
get { self[ShowBorderKey.self] }
set { self[ShowBorderKey.self] = newValue }
}
}
extension View {
public func showBorder(_ value: Bool) -> some View {
environment(\.showBorder, value)
}
}
在 AlertView 内部读取
@Environment(\.showBorder) private var showBorder
// …
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.accentColor, lineWidth: showBorder ? 1 : 0)
)
至此,API 回归简洁:
AlertView(...) { ... }
.showBorder(true)
进阶:用 @resultBuilder 做「有约束的自由」
当设计规范新增“免责声明 + 倒计时”组合时,与其疯狂加 init,不如定义一个 InfoSectionBuilder:
@resultBuilder
public struct InfoSectionBuilder {
public static func buildBlock(_ disclaimer: Text) -> some View {
disclaimer.disclaimerStyle()
}
public static func buildBlock(_ timer: TimerView) -> some View {
timer
}
public static func buildBlock(
_ disclaimer: Text,
_ timer: TimerView
) -> some View {
VStack(alignment: .leading, spacing: 12) {
disclaimer.disclaimerStyle()
timer
}
}
}
把 AlertView 再升级一次:
public struct AlertView<Info: View, Footer: View>: View {
private let title, message: String
private let infoSection: Info
private let footer: Footer
public init(
title: String,
message: String,
@InfoSectionBuilder infoSection: () -> Info,
@ViewBuilder footer: () -> Footer
) {
self.title = title
self.message = message
self.infoSection = infoSection()
self.footer = footer()
}
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(title).font(.headline)
Text(message).font(.subheadline)
infoSection.padding(.top, 16)
footer.padding(.top, 25)
}
.padding()
}
}
用法:
AlertView(
title: "删除账户",
message: "此操作不可撤销",
infoSection: {
Text("余额将在 24 小时内退回")
TimerView(targetDate: .now + 100)
},
footer: {
Button("确认删除", role: .destructive) {}
}
)
无障碍:组件方 + 使用方 共同责任
组件内部负责结构级:
.accessibilityElement(children: .combine)
.accessibilityLabel("\(type.rawValue) alert: \(title). \(message)")
.accessibilityAddTraits(.isModal)
使用方负责内容级:
Button("延长会话") {}
.accessibilityHint("延长 30 分钟")
.accessibilityAction(named: "延长会话") { // 实际逻辑 }
写在最后的 checklist
维度 | ✅ 自检问题 |
---|---|
初始化 | 是否只有“最少必要参数”? |
可组合 | 是否使用 @ViewBuilder / @resultBuilder ? |
样式扩展 | 是否通过 EnvironmentKey + 自定义修饰符? |
无障碍 | 结构 + 内容 是否都提供了 label / hint / action? |
向后兼容 | 新增需求是否只“加 Builder 方法”而不是“改 init”? |
源码仓库
所有示例已整理到 GitHub(非官方镜像,可直接跑 playground): github.com/muhammadosa…
当你用 .disabled(true) 把一整块区域关掉,子组件自动变灰、按钮自动失效 —— 这种「像原生」的体验,正是可扩展设计系统给人的最大安全感。
苹果首次在中国永久关闭了一家 Apple Store | 肘子的 Swift 周报 #097
苹果首次在中国永久关闭了一家 Apple Store - 肘子的 Swift 周报 #97
上周六,苹果正式永久性关闭了位于中国大连的一家 Apple Store 零售店,这是苹果首次在中国关闭直营店。该店于 2015 年开业,距今正好十年。消息传出后,网络上出现了不少相关报道,其中一些将此事解读为苹果在中国经营状况的某种信号。由于该店正好位于我所在的城市,我对这个事件有一些实地了解,想分享一些不同的观察角度。