Home > Enterprise >  Binding to an associated value of an enum that has different types of associated values
Binding to an associated value of an enum that has different types of associated values

Time:06-04

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.

  • Related