Anyway to do better: PhaseAnimator((1...45).map(Double.init)) { phase in

PhaseAnimator((1...45).map(Double.init)) { phase in
...
}

How to avoid the .map() part and tell Swift I want a Sequence of Double for 1 to 45?
?

Better how? What's issue do you have with your approach?

If you're looking to avoid allocations/copies, you can use a StrideThrough<Double> instead:

PhaseAnimator(stride(from: 1.0, through: 45.0, by: +1.0)) { phase in
...
}
2 Likes

I would like to see alternative (better) approach.

I like your answer!

Will it be different compared to this?

PhaseAnimator(1...45) { i in
    let phase = Double(i)
    ...
}

Doesn’t compile:

Initializer 'init(_:trigger:content:animation:)' requires that 'Double.Stride' (aka 'Double') conform to 'SignedInteger'

Don’t know why Swift doesn’t accept Range<Int>. Phase: Equatable

Edit: I'm not able to tell by looking at the PhaseAnimator: Phase is Equatable

But it looks like PhaseAnimator pass in a Double to the content closure even when I pass a [Phase] of [Int].

Because Doubles don't have an obvious "stride" like integers do.

1...45 should pretty obviously be 1, 2, 3, ... , 45.

The same is not true for (1.0)...(45.0). Should it go 1.0, 2.0, 3.0, ... , 45.0?
Or perhaps 1.0, 1.1, 1.2, 1.3, ... , 45.0?
Why not 1.0000001, 1.0000002, 1.0000003, ..., 45.0?

There's no one universally correct answer, so you have to use stride(from:to:by:) or stride(from:through:by:) to explicitly state the stride.

2 Likes

Again, it's not clear what you mean by "better". More readable? Fewer allocations? Fewer instructions? Lower latency? The answer would be different in each case.

Glad I could help.

My question is when I use an Range<Int> as the phases sequence it doesn’t compile with that error talking about Double. Where is this Double from? I’m only using Int.

So the problem code is:

PhaseAnimator(1…45) { Text(“\($0)”) }

This works:

PhaseAnimator([1, 2, 3]) { Text(“\($0)”) }

But why is content closure argument $0 is a Double?

Both work for me:

            PhaseAnimator(1 ... 45) { Text("\($0)") }
            PhaseAnimator([1, 2, 3]) { Text("\($0)") }

But... I'm on Xcode 15 beta. Will check on non beta later on.

I only happen in my code:


import SwiftUI

struct ExampleView: View {
  var body: some View {
    HStack(spacing: 30) {
      
      PhaseAnimator([true, false]) { phase in
        RoundedRectangle(cornerRadius: phase ? 10 : 30)
          .fill(.green.gradient)
          .frame(width: 120, height: 120)
          .overlay{ Text(phase ? "true" : "false") }
      }
      
//      PhaseAnimator(stride(from: 10, through: 60, by: 5), trigger: animate) { phase in
      PhaseAnimator(1...45) { phase in
        RoundedRectangle(cornerRadius: phase)
          .fill(.blue.gradient)
          .frame(width: 120, height: 120)
          .overlay {
            VStack {
              Text(phase, format: .number.precision(.integerAndFractionLength(integer: 3, fraction: 0)))
                .contentTransition(.numericText(value: phase))
            }
          }
      } animation: { phase in
//        let _ = print("phase = \(phase)")
        return .easeInOut(duration: 5.0 / phase)
      }
    }
    .font(.largeTitle).fontWeight(.bold).foregroundColor(.white)
  }
}

#Preview("ExampleView") {
  ExampleView()
}

Now it's mentioning CGFloat, before it was Double. What wrong with my code?

I see. This is ok:

    PhaseAnimator(1...45) { phase in
        Text("\(phase)")
    } animation: { phase in
        return .easeInOut(duration: 5.0 / Double(phase))
    }

Remove the double cast - you'll have the error you are talking about.

But phase is a Double somehow:

struct ExampleView: View {
  var body: some View {
//      PhaseAnimator(stride(from: 10, through: 60, by: 5)) { phase in
//    PhaseAnimator(1...45) { phase in
    PhaseAnimator([1, 2, 3]) { phase in
      RoundedRectangle(cornerRadius: phase)
        .fill(.blue.gradient)
        .frame(width: 120, height: 120)
        .overlay {
          VStack {
            Text(phase, format: .number.precision(.integerAndFractionLength(integer: 3, fraction: 0)))
              .contentTransition(.numericText(value: phase))
          }
        }
    } animation: { phase in
      //        let _ = print("phase = \(phase)")
      return .easeInOut(duration: 5.0 / phase)
    }
    .font(.largeTitle).fontWeight(.bold).foregroundColor(.white)
  }
}

To expand on @tera's comment, the issue is that phase doesn't have an explicit type, which leaves the compiler to try to guess from context. You first use phase as such:

PhaseAnimator(1...45) { phase in
    RoundedRectangle(cornerRadius: phase)

RoundedRectangle.init(cornerRadius:style:) takes cornerRadius as a CGFloat, so Swift is left to infer that phase must be a CGFloat (since integers are not implicitly convertible to floating-point numbers). This, then causes 1...45 to fail, since floating-point numbers are not Stridable.

Edit: .easeInOut(duration:) takes a TimeInterval, which is equivalent to Double. It appears that Double is taking precedence here over inferring CGFloat, but either way, the result is the same: 1...45 is constructed using literals, whose type is unknown until Swift typechecks how the literals are used — and your use-case indicates to Swift that the values should be Doubles.

If you give phase an explicit type (e.g., phase: Int in ...), then you'll get errors on the constructor for RoundedRectangle and .easeInOut(duration:), but the range will be correctly typed.

3 Likes

I see...so it seems type inference is hitting error condition by trying to infer the type of phase by looking at the closure body. I'm guessing combine with ViewBuilder, it's giving out puzzling error message that require better understanding of Swift.

I try doing this and it's still result in the same error:

    let array: [Int] = [1, 2, 3, 4]
    PhaseAnimator(array) { phase in

So the type of array is known, there is no guessing. It should fail at:

RoundedRectangle(cornerRadius: phase)   // Should fail here:  expect a Double, got an Int!

Why is the compiler still try to do type inference for phase, it can only be an Int here? Is this just how ViewBuilder work and can't do better?

1 Like

Unfortunately, due to how ViewBuilders work (and the complexity involved), when there's a typechecking error inside of a ViewBuilder, you'll very often get a completely unrelated or seemingly random error out, even one that doesn't seem possible. Sometimes it helps to break code out of a ViewBuilder into smaller parts to try to hunt down where the error really is.

I would try to replicate a similar circumstance outside of a ViewBuilder to see what you get, but it definitely looks like the compiler is type-checking the bodies of the closures first, before the outer context (quite possibly in order to figure out how to compile the ViewBuilder); because phase is still untyped, it appears to first infer that phase must be a Double, and then fails when it has to type-check phase against the actual argument you pass in.

Presumably, if you use phase: Int in ..., the error changes? (And obviously, if you fix the actual error, the messages go away, correct?)

2 Likes
PhaseAnimator(1...45) { (phase: Int) in

this works...so presumably this short circuit the type inferencer and fixed the type at Int and not go inferring for phase?

I learn my lesson today about ViewBuilder and type inference. I usually try to keep my View code small. But the problem is cause by one view modifier.

Now I learned!

Thanks!

1 Like

Correct: when you give the variable an explicit type, the type checker doesn't need to infer anything — both saving effort on compilation, and leading to more correct error messages.

(This is, by the way, why some suggestions were made in your Emoji thread to give some expressions explicit types: when the compiler sees a literal (e.g., 1) or unqualified dot syntax (e.g. .foo vs T.foo), it has to go and figure out the types from context. Here and there, it's not an issue performance-wise, but when you have extremely long expressions, Swift needs to try out all possible combinations in case one of them is corect, which can be extraordinarily time-consuming. This is especially problematic for expressions containing both literals and overloaded functions, like operators, because the number of possibilities to check can be overwhelming.)

2 Likes

So now, it easy to tell why these errors.

struct BlahView: View {
  var body: some View {
    PhaseAnimator(1...45) { // Initializer 'init(_:content:animation:)' requires that 'Double.Stride' (aka 'Double') conform to 'SignedInteger'
      RoundedRectangle(cornerRadius: $0, style: .continuous)  // Cannot convert value of type 'Int' to expected argument type 'CGFloat'
//        .contentTransition(.numericText(value: $0)) // uncomment this, the error upward at PhaseAnimator(1...45)
    }
  }
}

It seems with just RoundedRectangle(...), inferencer prefer $0 to be an Int

But add another type conflict, it seems now prefer $0 to be a Double. So it seems it cannot handle more than one inference conflict?

IMHO swift inference is over the top. I'd rather those would be straight errors:

Just remove the possibility of inference to not get it a chance of inferring anything (wrongly).

    PhaseAnimator(1...45) { (phase: Int) in

or

    PhaseAnimator(Int(1)...Int(45)) { phase in
1 Like

I agree with your sentiment: but I think the complication here is ViewBuilder: type inference is kind of in limbo and very likely hit on error unrelated and spit out whatever error.

For my Emoji thread, it's very surprising that the way I was doing with just .init, it's actually the best case slow but can finish. When I change all the .init to the types, Swift consume all app memory popup and sometime after MacBook restarted.

1 Like