Nested enums with associated values misbehave as @State variables in SwiftUI

When using an enum with associated values as an associated value of another enum, something weird happens in SwiftUI - the outer enum inexplicably changes to a different case:

enum Value {
    case whole(Number)
    case fraction(Number, Number)
}

enum Number {
    case int(Int)
    case double(Double)
}

struct ContentView: View {
   @State var value: Value = .fraction(.int(1), .int(1))
    
   var body: some View {
      VStack() {
         Text("\(value)" as String)
            
         Button("randomize denominator") {
            self.value = .fraction(.int(1), .int(Int.random(in: 1...100)))
         }
      }
   }
}

Changing the enum value where the first associated value (the numerator) is kept the same, while changing the second associated value (the denominator), toggles the case of the value between whole and fraction.

Here's what the screen shows (for brevity I'm omitting the CustomStringConvertable of each enum) after each button click:

.fraction(.int(1),.int(64))
.whole(.int(1))
.fraction(.int(1),.int(23))
.whole(.int(1))
.fraction(.int(1),.int(87))

The state variable value inexplicably toggles its case.

This erroneous behavior goes away if:

  1. nested enum has only one case
  2. nested enum doesn't have associated values
  3. first associated value (numerator) also changes
  4. the order of cases whole and fraction is reversed (fraction comes first)

This feels like a bug, but wanted to confirm, because it's not clear what SwiftUI does behind the scenes here. And if it is a bug, is there a way to address it, either by explaining why the reasons above caused it or in some other way?

1 Like

I am running into this same issue. Have you found a solution?

I tested this sample app now - I can see this bug on iOS 14 but not on iOS 15 (tested on simulator). Plus, if to move the state variable into a separate model class the bug is no longer reproduced on iOS 14.

class Model: ObservableObject {
    @Published var value: Value = .fraction(.int(1), .int(1))
}
1 Like

I ran into something really similar using recursive enums. I had this set up originally as a @State property. It turns out setting making that enum conform to Equatable did the trick! I did not need to use Observable Object