Ternary op is ugly...so I look for better option..and oh boy what I found!

My code is littered with things like this:

color.opacity(isOn ? 1 : 0).animation(isOn ? nil : .linear(duration: 1.5))

(similar, not exactly the same)

It's mapping true/false to another type. I wonder, is there better way, something like Bool.map(...)? The first thing I found, mentioned ternary op slow to compile bug. :flushed::hot_face:.

His solution is:

extension Bool {
    
    /// Convenience method to map the value of a Boolean to a specific Type.
    ///
    /// - Parameters:
    ///   - ifTrue: The result when the Boolean is true
    ///   - ifFalse: The result when the Boolean is false
    /// - Returns: Returns the result of a specific Type
    func mapTo<T>(_  ifTrue:T, _ ifFalse:T ) -> T {
        
        if self {
            return ifTrue
        } else {
            return ifFalse
        }
        
    }
    
}

I added this:

extension Bool {
    var asBit: Double { if self { return 1 } else { return 0 } }
}

my code become:

color.opacity(isOn.asBit).animation(isOn.mapTo(nil, .linear(duration: 1.5)))

Is this a good thing or not?

Well, I don't have suggestion for ternary operators, what you use seem equally confusing (if not more) to me. I doubt you can get any terser than ternary operator, given that SwiftUI is sensitive to statement locations. Esp that ternary operator is literally just all information (condition, true case, false case) with 2-6 extra characters. Though some of them you could turn into ViewModifier, like

color.visible(isOn)...

given that they're probably common and have semantic.

PS

The bug you cited are two year old, it'd be interesting if someone try again given that we have new diagnostic architecture since Swift 5.2.

Personally, I'd advice against tuning animation wrt. view state. It doesn't look good (without tremendous effort) with interruptible animations.

2 Likes
color
    .opacity(isOn ? 1 : 0)
    .animation(isOn ? nil : .linear(duration: 1.5))
6 Likes

Can you explain what you mean? This is a simplified version of what I'm doing:

struct Foo: Animatable, View {
    var animatableData: Double

   init(size: Double) {
        self.animatableData = size
    }

    var body: some View {
        let isOn = animatableData > 0.5
        return Color.green
            .opacity(isOn ? 1 : 0)
            .animation(isOn ? nil : .linear(duration: 1.5)
    }
}

What animation problem do you see?

Okay, you change my mind, ternary op is not ugly, it's beautiful now :star_struck:. It's self explanatory so it's better than the extra layer of obfuscation.

Anyway, even if the slow compile is still true, it doesn't affect me much.

I like the extension idea. I wouldn't name a property "asBit" when it actually is a Double. If we reasonably assume anything starting with "is" to be Bool, "asDouble" is a pretty obvious modifier. Or one could go with doubleValue, in keeping with Apples tendency in Obj-C.

I don't want this because it's not mapping to any double. I tried asOneOrZero because that's what it's doing. Didn't like...

Anyway, I changed my mind, I'm keeping with ternary.

Ah, that was a brain fart, apologies. I meant that you mix continuous animations (.linear) with instantaneous ones (nil). It can look quite jarring when .linear one gets interrupted nil. If, say, within the 1.5 second that you're turning it on, you turn it back off. The linear animation continues, but with the added jumping animation, it can undershoot the animation outside 0...1 boundaries (though the endpoint is always correct). It's not a big problem here since opacity is already clamped should that happens. It's more apparent for things like position:

Circle()
  .frame(width: 20, height: 20)
  .offset(x: isOn ? -100 : 100, y: 0)
  .animation(isOn ? nil : .linear(duration: 5.0))

If you repeatedly toggle isOne, the circle can undershoot way to the left (duration of 5 for extra exaggeration).

1 Like
Yet another digression

For things that really shows/hides the view, I'd try to use transition whenever possible:

ZStack {
  if isOn {
    Color.green
  }
}
.transition(.opacity)
.animation(.linear)

IMO it express the fact that I'm showing/hiding much better, though it's just a different style, no need to fret about it. Won't work with non-filling views though.

Although you want to use the ternary operator now, I still want to add that if someone wanted to extend Bool with a mapTo() function, they should use auto closures like this:

extension Bool {
    /// Convenience method to map the value of a Boolean to a specific Type.
    ///
    /// - Parameters:
    ///   - ifTrue: The result when the Boolean is true
    ///   - ifFalse: The result when the Boolean is false
    /// - Returns: Returns the result of a specific Type
    func mapTo<T>(_ ifTrue: @autoclosure () -> T, _ ifFalse: @autoclosure () -> T) -> T {
        if self {
            return ifTrue()
        } else {
            return ifFalse()
        }
    }
}

Only then the behavior of calling mapTo() would be exactly the same as when using the ternary operator.

3 Likes

If the true/false values are static/constant as mine are, is the compiler smart to not create unnecessary function call?

Maybe numericValue instead of asBit?

And make it generic?

1 Like

I don't know how...so write it out in long form:

extension Bool {
    var numericValue: Double {
        get {
            if self { return 1 } else { return 0 }
        }
    }
}

My guess is change Double somehow to <T: Numeric> but...I can't figure this out....help...........please?

:pray:

Pretty sure properties can't do generic, you'll need functions

extension Bool {
  func numericValue<T>(of type: T.Type = T.self) -> T where T: ExpressibleByIntegerLiteral {
    self ? 1 : 0
  }
}

Though IIRC generic specialisation isn't exactly zero-cost, so unless you use them throughout, you can make do with normal function/property.

1 Like

Oh, ExpressibleByIntegerLiteral is even higher up than Numeric...now I see.

:raised_hands:

Does it even make sense to want generic store property getter?

On another thing:

So I thought I could do:

if self { 1 } else { 0 }

and not have to do:

if self { return 1 } else { return 0 }

Try to avoid ternary due to that slow compile bug...

Theoretically, you can use use type inference to infer the type or as to provide the type information.

let a: Int = false.numericValue
let b = true.numericValue as Double

Then again, from design perspective, I don't really like that something is inferred solely from the return type. And I don't think you should really need generic (as mentioned above).

In Swift if-else is a control flow statement, not expression. It was a point of contention some time ago, but no, if-else does not return anything.

Did you experience this on you compiler directly? If so, you should file a bug report. A statement this simple with a well-defined return type is basically nothing.

Do you mean:

let a: Int = false.numericValue()
let b = true.numericValue() as Double

it's a func, now a var. Just to be sure I'm not mistaken...

I have no idea if it still slow to compile or not. I care more about faster code...

I mean, theoretically, you could use var. It's just not allowed by the language atm.

It should compile to the same code. There's no reason to write more convoluted code than necessary unless the compiler hits you with some blunt weapon.

1 Like

Scala has a nice way of doing similar things:

val r = (1 match {
  case 0 => "0"
  case 1 => "1"
}) match {
  case s => s"the number is $s"
}
println(r)

In Swift it's possible possible if you're prepared to get creative:

precedencegroup Map {
    associativity: left
}

infix operator <*> : Map
    func <*> <T, R>(_ lhs: T, _ f: (T) -> R) -> R {
        return f(lhs)
}

let result = 1 <*> {
    $0 * 2
} <*> { "the number is \($0)" }

So, for the initial case with a boolean value:

true <*> { $0 ? 1 : 0 }

take a look at if/else expression, I like that better. Ternary look ugly in many cases.

The usual way of spelling the more general operation in SIMD and shader programming is:

func select<T>(_ ifFalse: T, _ ifTrue: T) -> T

For the specific function mapping to 0 or 1, I would personally probably call it "indicator", but "numericValue" seems acceptable as well (an indicator is explicitly a function returning 0 or 1, while some languages assign true a numeric value of -1, so there's a smidgen more ambiguity).