SwiftUI如何绑定含不同类型关联值枚举的关联值?
多关联值类型枚举的SwiftUI绑定优化方案
问题场景
最初定义的同关联值类型枚举:
enum Choice { case one(String) case two(String) }
调整后枚举不同分支的关联值类型出现差异:
enum Choice { case one(String) case two(Bool) }
在视图层通过switch分支判断枚举值时,需要将不同分支的关联值分别绑定到TextField、Toggle、Stepper等对应控件。原先方案仅提供String类型的计算属性,无法满足Bool、Int、Float等多类型的绑定需求。
如果为每一种关联值类型单独复制一份计算属性代码,仅替换类型名,在关联值类型较多的业务场景下冗余度极高,可维护性很差。
初始冗余实现
最初的实现给Value枚举的每一种关联值类型都写了独立的读写计算属性,代码重复度非常高:
import SwiftUI @main struct MyApp: App { @StateObject private var model = Model() var body: some Scene { WindowGroup { ContentView() .environmentObject(model) } } } struct ContentView: View { @EnvironmentObject private var model: Model var body: some View { VStack { ScrollView { Text(model.jsonString) } List($model.data, id: \.name, children: \.children) { $node in HStack { switch node.value { case .none: Spacer() case .bool: Toggle(node.name, isOn: $node.value.bool) case let .int(value): Stepper("\(value)", value: $node.value.int) case let .float(value): Text(value.description) case let .string(value): Text(value) } } } } } } class Model: ObservableObject { @Published var data: [Node] = [ Node(name: "My Bool", value: .bool(false)), Node(name: "My Int", value: .int(25)), Node(name: "My float", value: .float(3.123)), Node(name: "My parent node", value: .none, children: [ Node(name: "Default name", value: .string("Untitled")), Node(name: "Fluid animations", value: .bool(true)), Node(name: "More children??", value: .none, children: [ Node(name: "Hello", value: .string("There")) ]) ]) ] private let encoder = JSONEncoder() init() { encoder.outputFormatting = .prettyPrinted } var jsonString: String { String(data: try! encoder.encode(data), encoding: .utf8)! } } struct Node: Codable { var name: String var value: Value var children: [Node]? = nil } enum Value: Codable { case none case bool(Bool) case int(Int) case float(Float) case string(String) var bool: Bool { get { switch self { case let .bool(value): return value default: fatalError("Unexpected value \(self)") } } set { switch self { case .bool: self = .bool(newValue) default: fatalError("Unexpected value \(self)") } } } var int: Int { get { switch self { case let .int(value): return value default: fatalError("Unexpected value \(self)") } } set { switch self { case .int: self = .int(newValue) default: fatalError("Unexpected value \(self)") } } } var float: Float { get { switch self { case let .float(value): return value default: fatalError("Unexpected value \(self)") } } set { switch self { case .float: self = .float(newValue) default: fatalError("Unexpected value \(self)") } } } var string: String { get { switch self { case let .string(value): return value default: fatalError("Unexpected value \(self)") } } set { switch self { case .string: self = .string(newValue) default: fatalError("Unexpected value \(self)") } } } }
优化后实现
核心优化思路:
- 移除枚举中所有重复的类型专属计算属性,将绑定逻辑下沉到视图层,避免枚举定义和UI绑定逻辑强耦合
- 抽离独立的
ValueView组件处理不同枚举值的控件渲染,统一维护分支逻辑 - 在switch分支匹配到对应关联值时,直接构造临时
Binding:getter返回匹配到的关联值,setter将新值包装为对应枚举case回写,不需要提前写枚举计算属性 - 给视图绑定
.animation(.default, value: node.value)动画修饰符,解决枚举值切换时的UI动画异常问题
最终可运行代码:
import SwiftUI enum Value: Codable, Equatable { case none case bool(Bool) case int(Int) case float(Float) case string(String) } struct Node: Codable { var name: String var value: Value var children: [Node]? = nil } struct ValueView: View { let name: String @Binding var value: Value var body: some View { HStack { switch value { case .none: Spacer() case let .bool(bool): Toggle(name, isOn: Binding(get: { bool }, set: { value = .bool($0) } )) case let .int(int): Stepper("\(int)", value: Binding(get: { int }, set: { value = .int($0) })) case let .float(float): Text(float.description) case let .string(string): Text(string) } } } } class Model: ObservableObject { @Published var data: [Node] = [ Node(name: "My Bool", value: .bool(false)), Node(name: "My Int", value: .int(25)), Node(name: "My float", value: .float(3.123)), Node(name: "My parent node", value: .none, children: [ Node(name: "Default name", value: .string("Untitled")), Node(name: "Fluid animations", value: .bool(true)), Node(name: "More children??", value: .none, children: [ Node(name: "Hello", value: .string("There")) ]) ]) ] private let encoder = JSONEncoder() init() { encoder.outputFormatting = .prettyPrinted } var jsonString: String { String(data: try! encoder.encode(data), encoding: .utf8)! } } struct ContentView: View { @StateObject private var model = Model() var body: some View { VStack { ScrollView { Text(model.jsonString) } List($model.data, id: \.name, children: \.children) { $node in ValueView(name: node.name, value: $node.value) .animation(.default, value: node.value) } } } } @main struct MyApp: App { @StateObject private var model = Model() var body: some Scene { WindowGroup { ContentView() } } }
内容的提问来源于stack exchange,提问作者Frank Tinsley
相关产品推荐
相关产品推荐

