young
(rtSwift)
1
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?
?
AlexanderM
(Alexander Momchilov)
2
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
young
(rtSwift)
3
I would like to see alternative (better) approach.
I like your answer!
tera
4
Will it be different compared to this?
PhaseAnimator(1...45) { i in
let phase = Double(i)
...
}
young
(rtSwift)
5
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].
AlexanderM
(Alexander Momchilov)
6
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
AlexanderM
(Alexander Momchilov)
7
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.
young
(rtSwift)
8
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?
tera
9
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.
young
(rtSwift)
10
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?
tera
11
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.
young
(rtSwift)
12
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)
}
}
itaiferber
(Itai Ferber)
13
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
young
(rtSwift)
14
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
itaiferber
(Itai Ferber)
15
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
young
(rtSwift)
16
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
itaiferber
(Itai Ferber)
17
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
young
(rtSwift)
18
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?
tera
19
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
young
(rtSwift)
20
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