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?
?
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
...
}
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
.
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 Double
s.
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.
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?
Unfortunately, due to how ViewBuilder
s 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?)
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!
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.)
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
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.