[Pitch] Enum Quality of Life Improvements

Redux-style systems are perfect for SwiftUI, but the ability to store both the data for a given screen and its view-route as associated enum cases causes view refreshes on every update, even if the outermost view observing the case is only interested in its route.

E.g. with a view route that looks like this:

enum Navigation: Hashable {
    case firstScreen(FirstScreenData)
    case secondScreen(SecondScreenData)
}

And a navigation router that looks like this:

struct NavigationRouter: View {
    @StateBinding var navigation = GlobalState.shared.lensing(\.navigation)

    var body: some View {
        switch navigation {
        case .firstScreen:
            FirstScreen()
        case .secondScreen:
            SecondView()
        }
    }
}

NavigationRouter will refresh even if the case hasn't changed.

It is of course possible to store the navigation route as an enum without associated cases and to store the data for each screen in a separate location, but that leaves us with a system that needs to be kept manually in sync, which can be a security concern if any of your screens contain sensitive data that is required to be forgotten once the screen has been navigated away from.

What I'm proposing is very similar to CaseIterable in nature. For the sake of reference, let's give it the preliminary name of UnassociatedCases.

An example of what applying UnassociatedCases would do:

extension Navigation: UnassociatedCases {
    // Compiler generated
    enum Unassociated: Hashable, CaseIterable {
        case firstScreen
        case secondScreen
    }
    
    // Compiler generated
    var unassociatedCase: Unassociated {
        switch self {
        case .firstScreen:
            return .firstScreen
        case .secondScreen:
            return .secondScreen
        }
    }

    // Compiler generated
    func isCase(_ unassociatedCase: Unassociated) -> Bool {
        switch (self, unassociatedCase) {
        case (.firstScreen, .firstScreen):
            return true
        case (.secondScreen, .secondScreen):
            return true
        default:
            return false
        }
    }
}

With these improvements to enums, we could then use GlobalState.shared.lensing(\.navigation. unassociatedCase) and views would no longer refresh unless the route itself changed.

I'm sure there's many other places such improvements would be useful, I'm just speaking to my personal headaches here, which required me to write a workaround to achieve this functionality using Sourcery.

Problems considered:

In the event that two cases share the same name, a differentiating element would need to be added. The most favourable option would be to include the names of the associated values.
E.g.
case firstScreen_data_counter

1 Like

In my own code I do a similar thing, but would love it if Swift supported this as a sort of special identity inquiry. I refer to it in my own stuff as “celf” (case-self) and mock variables to handle it much how you do with an inner enum stripped down without values, but would love if something like this was feasible without boilerplate

enum CelfRepresenting {
  case a(Int)
  case b(Double)
  case c(String)

  enum CaseSelf {
     case a
     case b
     case c
  }
  
  var celf: CaseSelf {
     switch self {
       case .a: return .a
       case .b: return .b
       case .c: return .c
     }
  }
}

let x: CelfRepresenting = .a(42)
Print(x.celf == .a) // true

Aside from convenience, it cuts down on unwrapping enums in cases where I don’t care about the value and (personally) find the brevity a huge help as I’m usually doing that in conditionals where it clutters legibility, IMO.

Also allows use of it in ternary operators, which I favor, and would be great for things like hardcoding fast paths in more involved enums

let x = Set<MyEnum.celf>(.a, .b, .c, —- .z)
…
let y = MyEnum.f(ComplexObject)
x.contains(y.celf) ? easyCalc(y) : slowCalc(y)
3 Likes

This would definitely be nice to have. Cf. Pitch: Auto-synthesize cases for enums which I think is the last time this was raised.

(Also maybe of interest, a suggestion for the companion property for the associated value Extract Payload for enum cases having associated value)

1 Like

There's a very clever library by Point-Free which introduces the concept of CasePaths. This can certainly help with case matching.

Nevertheless, I'm not against the principle of this pitch. There's still plenty to explore in this particular area and I'm very strongly in favor of these mechanisms being baked into the language itself.

4 Likes

I raised something very similar a little while ago too:
https://forums.swift.org/t/idea-enum-intcase-stringcase

I think the conversation got sidetracked on the Codable stuff, but I was essentially just proposing auto-synthesised cases for enums with associated types.

I think IntCase and StringCase provide much more flexibility. It's nice to know the index/name of the enum case when handling custom Codable implementations.

We wrote the library because Swift doesn't come with case paths / enum key paths, but we would be more than happy to retire it if someone gets an implementation working! We'd even be happy to help write the pitch and work out some of the design details.

This is exactly what case paths was born out of. We use them heavily in swift-composable-architecture. They're useful for far more, though. Even "vanilla" SwiftUI benefits, since it gives you the ability to derive bindings of enum cases from bindings of enums.

8 Likes

Hi. Your task can be solved without language changes, if I understand it correctly.

// Firstly, create this struct: 
@propertyWrapper
public struct EquatableExcluded<T>: Equatable {
  public var wrappedValue: T
  
  public init(wrappedValue: T) {
    self.wrappedValue = wrappedValue
  }
  
  public init(_ wrappedValue: T) {
    self.init(wrappedValue: wrappedValue)
  }
  
  public static func == (lhs: Self, rhs: Self) -> Bool { true }
}

extension EquatableExcluded: Hashable {
  /// Empty Implementation
  public func hash(into hasher: inout Hasher) {}
}

// Then wrap your associated values

enum Navigation: Hashable {
    case firstScreen(EquatableExcluded<FirstScreenData>)
    case secondScreen(EquatableExcluded<SecondScreenData>)
}

There are also some other similar pithes: Comparing enum cases while ignoring associated values - #8 by Dmitriy_Ignatyev

The goal is to have it be equatable at the case level but not at the associated values level, so that solution doesn't work.

If there's other pitches then I'd say it's got traction as something the developers want in the language, or at least those of us willing to pitch it, so perhaps we should see if we can move it along into a real pitch?

You can implement caseName property generically in several lines of code, and then conform Equatable protocol where caseName is compared.

caseName is the least important part of this proposal, I've implemented it myself many times.

Terms of Service

Privacy Policy

Cookie Policy