Followup question to this answer.
What if:
enum Choice {
case one(String)
case two(String)
}
is instead:
enum Choice {
case one(String)
case two(Bool)
}
and in my view I'm switching on the enum and then binding one case's associated value to, say, a text field, and the other, to a toggle? A computed String property won't do the trick for me now in the case where I need to bind to the bool.
All I can think is to copy & paste the property and change all the String
to Bool
but in my real code I have lots of different types so that approach is getting pretty stupid looking.
Any ideas would be appreciated, thanks!
Current code:
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)")
}
}
}
}
Update: Added code below that I ended up with after changes from answer and working around a weird swiftui animation thing.
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()
}
}
}
CodePudding user response:
Indeed, having several very similar properties is a code smell, as well as the fact that those properties were added just because they are needed by the UI layer.
However, since you already have the switch over the value type, you can push the logic for the binding to that (UI) layer. Here's a possible implementation:
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)
}
}
}
}
I also took the liberty of extracting the code to a dedicated view and decoupling that view from the Node
type, this is more idiomatic in SwiftUI and makes your code more readable and easier to maintain.
With the above in mind, ContentView
simply becomes:
Usage:
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)
}
}
}
}
, and you can safely delete the "duplicated" properties from the Value
enum.