原文链接
为什么旧写法撑不过三次迭代?
先来看一个"经典"写法
swift
Alert(
title: "Title",
message: "Description",
type: .info,
showBorder: true,
isDisabled: false,
primaryButtonTitle: "OK",
secondaryButtonTitle: "Cancel",
primaryAction: { /* ... */ },
secondaryAction: { /* ... */ }
)
痛点一句话总结:初始化即地狱。
• 参数爆炸,阅读困难
• 布局/样式/行为耦合,一改全改
• 无法注入自定义内容,复用性 ≈ 0
目标:像原生一样的 SwiftUI 组件
我们想要的最终形态:
swift
AlertView(title: "...", message: "...") {
AnyViewBuilder Content
}
.showBorder(true)
.disabled(isLoading)
为此,需要遵循 4 个关键词:
- Familiar APIs -- 看起来像 SwiftUI 自带的
- Composability -- 任意组合内容
- Scalability -- 业务扩张不炸窝
- Accessibility -- 无障碍不打补丁
三步重构法
Step 1:只保留「必须参数」
swift
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 把"内容"交出去
swift
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()
}
}
使用:
swift
AlertView(title: "提示", message: "确定删除吗?") {
HStack {
Button("取消", role: .cancel) {}
Button("删除", role: .destructive) {}
}
}
Step 3:样式/行为用 环境值 + 自定义修饰符
我们想让边框可开关,但又不想回到"参数爆炸"。
swift
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
swift
// ...
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.accentColor, lineWidth: showBorder ? 1 : 0)
)
至此,API 回归简洁:
swift
AlertView(...) { ... }
.showBorder(true)
进阶:用 @resultBuilder 做「有约束的自由」
当设计规范新增"免责声明 + 倒计时"组合时,与其疯狂加 init,不如定义一个 InfoSectionBuilder:
swift
@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 再升级一次:
swift
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()
}
}
用法:
swift
AlertView(
title: "删除账户",
message: "此操作不可撤销",
infoSection: {
Text("余额将在 24 小时内退回")
TimerView(targetDate: .now + 100)
},
footer: {
Button("确认删除", role: .destructive) {}
}
)
无障碍:组件方 + 使用方 共同责任
组件内部负责结构级:
swift
.accessibilityElement(children: .combine)
.accessibilityLabel("\(type.rawValue) alert: \(title). \(message)")
.accessibilityAddTraits(.isModal)
使用方负责内容级:
swift
Button("延长会话") {}
.accessibilityHint("延长 30 分钟")
.accessibilityAction(named: "延长会话") { // 实际逻辑 }
写在最后的 checklist
维度 | ✅ 自检问题 |
---|---|
初始化 | 是否只有"最少必要参数"? |
可组合 | 是否使用 @ViewBuilder / @resultBuilder ? |
样式扩展 | 是否通过 EnvironmentKey + 自定义修饰符? |
无障碍 | 结构 + 内容 是否都提供了 label / hint / action? |
向后兼容 | 新增需求是否只"加 Builder 方法"而不是"改 init"? |
源码仓库
所有示例已整理到 GitHub(非官方镜像,可直接跑 playground): github.com/muhammadosa...
当你用 .disabled(true) 把一整块区域关掉,子组件自动变灰、按钮自动失效 ------ 这种「像原生」的体验,正是可扩展设计系统给人的最大安全感。