在SwiftUI中,通常需要构建可配置的通用组件,例如按钮组件、Toggle组件等。
// 典型例子
HStack {
Text("iCloud")
Spacer()
Toggle("", isOn: $isOn)
}

如果每个视图的部分结构不一致、显示Toggle开关、图片、时间等内容。
逻辑需要通过if-else或者重复的View显示,增大维护难度,代码难以复用。
// 视图1
HStack {
Text("iCloud")
Spacer()
Toggle("", isOn: $isOn) // 显示 Toggle 开关
}
// 视图2
HStack {
Text("iCloud")
Spacer()
Image(systemName:"chevron.right") // 显示箭头
}
因此,就诞生了「枚举+@ViewBuilder」模式:
用 enum 表示「视图的不同状态」
用 @ViewBuilder 根据枚举动态生成 View
模式结构:枚举 + @ViewBuilder
1、定义视图枚举
下面是一个“视图”枚举,定义了组件的变化类型,用于不同的显示场景。
enum Accessory {
case toggle(Binding<Bool>, ModelConfigManager?)
case chevron
case none
}
根据视图的参数,定义视图枚举的关联值。
2、创建组件视图
在SwiftUI视图中,创建组件视图:
struct HomeSettingRow: View {
let color: String
let title: String
let accessory: Accessory
@ViewBuilder
private var accessoryView: some View {
switch accessory {
case .toggle(let isOn, let manager):
Toggle("", isOn: isOn)
.onChange(of: isOn.wrappedValue) { _, newValue in
manager?.cloudKitMode = newValue ? .privateDatabase : .none
}
.labelsHidden()
case .chevron:
Image(systemName: "chevron.right")
.foregroundColor(.gray)
case .none:
EmptyView()
}
}
var body: some View {
HStack {
Text(title)
Spacer()
accessoryView
}
.padding()
}
}
调用组件视图:
HomeSettingRow(color: "#226AD6",
title: "Enable iCloud",
accessory: .toggle($isICloudOn, modelConfigManager))

HomeSettingRow(color: "#FFAA00",
title: "About",
accessory: .chevron)
当显示箭头时,选择Accessory.chevron。

总结
SwiftUI中,使用枚举 + @ViewBuilder结构,可以将视图的部分改动内容,通过枚举封装。
使用一个视图组件,就可以覆盖多个UI变体。主视图负责布局,@ViewBuilder负责具体内容渲染。

@ViewBuilder是一个函数构建器(Function Builder),SwiftUI内部通过它拼装多个视图为一个复合视图。
@ViewBuilder
private var accessoryView: some View {
switch accessory {
// 返回不同 View
}
}
当需要扩展更多的交互视图时,可以修改Accessory枚举进行扩充:
enum Accessory {
case toggle(Binding<Bool>, ModelConfigManager?)
case chevron
case link(URL)
case button(title: String, action: () -> Void)
case none
}
这样可以保持住布局不变,新增case即可。
扩展知识
1、SwiftUI构建多个视图@ViewBuilder:https://fangjunyu.com/2024/12/23/swiftui%e6%9e%84%e5%bb%ba%e5%a4%9a%e4%b8%aa%e8%a7%86%e5%9b%beviewbuilder/
2、Swift科普文《枚举enum》:https://fangjunyu.com/2024/10/20/swift%e7%a7%91%e6%99%ae%e6%96%87%e3%80%8a%e6%9e%9a%e4%b8%beenum%e3%80%8b/
扩展知识
1、Binding枚举
在定义枚举时,定义了Binding参数:
enum Accessory {
case toggle(Binding<Bool>, ModelConfigManager?)
case chevron
case none
}
在调用时,可以直接传入Binding参数:
HomeSettingRow(
color: "#FF0000",
icon: "icloud",
title: "启用 iCloud",
accessory: .toggle($isICloudOn, modelConfigManager)
)
在View视图中,判断Binding参数时:
case .toggle(let isOn, let modelConfigManager):
Toggle("", isOn: isOn)
这里的isOn是一个Binding<Bool>类型,如果这里写成$isOn,就会变成Binding<Binding<Bool>>类型:
case .toggle(let isOn, let modelConfigManager):
Toggle("", isOn: $isOn) // 写成$isOn会报错, Instance method 'onChange(of:initial:_:)' requires that 'Binding<Bool>' conform to 'Equatable'
2、Binding枚举解包
在定义枚举时,定义了Binding参数:
enum Accessory {
case toggle(Binding<Bool>, ModelConfigManager?)
case chevron
case none
}
在视图中,通过枚举将数值解构出来:
case .toggle(let isOn, let modelConfigManager):
这里的isOn为Binding<Bool>类型。
let isOn表示绑定到匹配值的语法,和声明常量或者变量不同,因此这里及时修改为var:
case .toggle(var isOn, let modelConfigManager):
也不会改变外部绑定的状态。
3、wrappedValue的使用
在使用onChange时,因为isOn是一个Binding<Bool>类型。
Binding 是一个「结构体」,它是 SwiftUI 提供的 属性包装器类型。
它的核心定义(简化后)大致是这样的:
@propertyWrapper
public struct Binding<Value> {
public var wrappedValue: Value { get nonmutating set }
}
本身并不能被onChange监听,所以需要使用wrappedValue获取其实际的值:
.onChange(of: isOn.wrappedValue) { _, newValue in
modelConfigManager?.cloudKitMode = newValue ? .privateDatabase : .none
}
4、@ViewBuilder的替代方式
如果@ViewBuilder视图比较多,影响代码的预览,也可以将@ViewBuilder拆分成普通的View视图。
拆分前:
struct HomeSettingRow: View {
let color: String
let title: String
let accessory: Accessory
@ViewBuilder
private var accessoryView: some View {
switch accessory {
case .toggle(let isOn, let manager):
Toggle("", isOn: isOn)
.onChange(of: isOn.wrappedValue) { _, newValue in
manager?.cloudKitMode = newValue ? .privateDatabase : .none
}
.labelsHidden()
case .chevron:
Image(systemName: "chevron.right")
.foregroundColor(.gray)
case .none:
EmptyView()
}
}
var body: some View {
HStack {
Text(title)
Spacer()
accessoryView
}
.padding()
}
}
拆分后:
struct HomeSettingRow: View {
let color: String
let title: String
let accessory: Accessory
var body: some View {
HStack {
Text(title)
Spacer()
accessoryView(accessory: accessory)
}
.padding()
}
}
private struct accessoryView: View {
let accessory: HomeSettingsEnum
var body: some View {
switch accessory {
case .toggle(let isOn, let manager):
Toggle("", isOn: isOn)
.onChange(of: isOn.wrappedValue) { _, newValue in
manager?.cloudKitMode = newValue ? .privateDatabase : .none
}
.labelsHidden()
case .chevron:
Image(systemName: "chevron.right")
.foregroundColor(.gray)
case .none:
EmptyView()
}
}
}
