SwiftUI组件化与架构之「枚举+@ViewBuilder」结构
SwiftUI组件化与架构之「枚举+@ViewBuilder」结构

SwiftUI组件化与架构之「枚举+@ViewBuilder」结构

在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()
        }
    }
}
   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注