Can't use `if case` with Ternary Operator?

Yesterday a coworker of mine and I were trying get a red border to appear around an input field if the input text is not valid. I had this working with a simple Ternary Operator that was setting the border opacity to 1 if the state was invalid and 0 if not, but we refactored the enum that has the different cases to manage state so that they have associated values to allow passing in a message which would make the call site cleaner. In doing so we discovered that using something like case .invalid = validationState ? 1.0 : 0.0 was not allowed and I'd like to know why that is. Is the syntax wrong or does Swift just not allow this? And if so, was this intentional, and why?

Here's the enum:

public enum ValidationState: Equatable {
    case none
    case valid(message: String, textColor: Color?)
    case invalid(message: String, textColor: Color?)
    case checking(message: String, textColor: Color?)
    case notChecked(message: String, textColor: Color?)
    
    public var message: String {
        switch self {
        case .none:
            return ""
        case .valid(message: let message, _):
            return message
        case .invalid(message: let message, _):
            return message
        case .checking(message: let message, _):
            return message
        case .notChecked(message: let message, _):
            return message
        }
    }
    
    public var textColor: Color? {
        switch self {
        case .none:
            return nil
        case .valid(_, textColor: let textColor):
            return textColor ?? .green
        case .invalid(_, textColor: let textColor):
            return textColor ?? .red
        case .checking(_, textColor: let textColor):
            return textColor ?? Color(.standardTextColor)
        case .notChecked(_, textColor: let textColor):
            return textColor ?? Color(.standardTextColor)
        }
    }
    
    public var isInvalid: Bool {
        switch self {
        case .invalid:
            return true
        default:
            return false
        }
    }
}

Here's where I was trying to use a Ternary Operator, but had to make a computed property instead (opacityForInvalidBorder):

// This is what it looks like now:
.opacity(opacityForInvalidBorder)

// This is the property that computes the opacity
private var opacityForInvalidBorder: Double {
        if case .invalid = validationState {
            return 1.0
        } else {
            return 0.0
        }
    }

My coworker and I were saying it would've been great to be able to say something like:

.opactity(case .invalid = validationState ? 1.0 : 0.0)

Any clarification on this is appreciated. Thanks for taking the time to read this. I hope I was clear with my question and the problem we were trying to solve. If not, please let me know so I can explain further.

Cheers!

1 Like

The question is very clear. Unfortunately, there is no such feature in Swift. You simply have to do the pattern matching with if case, guard case, or in a switch.

There has been plenty of discussion here about how to design such a feature to match cases without also comparing the associated values, but such a feature does not exist today. (If there were such a feature, it could not be spelled as-is, because the precedence of the ternary operator is higher than =. One suggested syntax is something like: validationState is case .invalid; there are others, I'm sure.)

For now, one commonly used workaround is to create a subtype within your enum; that subtype would be an enum with the same cases, but without associated values. You would then add a property to the parent enum that returns a value of the subtype with the relevant case, which could then be compared for equality in other code. In other words:

public enum A {
  case foo(Int), bar(Int), baz(Int)

  public enum B { case foo, bar, baz }
  public var b: B {
    switch self {
    case .foo(_): return B.foo
    case .bar(_): return B.bar
    case .baz(_): return B.baz
    }
  } 
}
let a = A.foo(42)
if a.b == .foo { print("Hello") }
3 Likes

There's no contracted form of case pattern. Rather, pattern matching are different from Bool expression. One thing I can think of is to make a property if it comes up often:

extension ValidationState {
  var isValid: Bool {
    if case .valid = self {
      return true
    }
    return false
  }
}

PS

IMO, your ValidationState looks more like a struct than enum.

1 Like

This provides you with the option to do

.opacity(.invalid ~= validationState ? 1 : 0)
3 Likes

Thanks for replying! So those functions would just live locally in the project I'm assuming?

Thanks for your reply. Why do you say it looks more like a struct rather than an enum?

Nah, SPM is too easy to use. I think you should always use it for extensions that aren't only friendly with internal types.

Very interesting! Thanks so much for your reply! I really appreciate ya and the solution you provided!

Sorry, I'm new to SPM as this is my first package and I'm trying to wrap my head around it. Just a little confused by how you mean what you said. SPM being too easy to use as a good thing? Like, that's where these functions should live? If so, what would it be an extension of? Maybe the enum type itself? Not very experienced with Pattern Matching either and custom operators.

You have about the same associated type on most of the cases. It'd feel more natural to do:

struct ValidationState: Equatable {
  enum State: Equatable {
    case valid, invalid, checking, notChecked
  }

  var message: String, textColor: Color?, state: State
}

The case none is quite curious. Usually you'd use Optional for that.