Hi
I'm trying to use the new PhaseAnimator
from iOS 17 and I noticed a bug.
For some reason, the animation is not deallocated when the view is removed from hierarchy, leading to a crash app.
Example
enum OuterBreathState: CaseIterable {
case exhale, inhale
var scale: CGFloat {
switch self {
case .inhale: return 2
case .exhale: return 1
}
}
var color: Color {
switch self {
case .inhale: return .white
case .exhale: return .blue
}
}
}
struct PhaseView: View {
var body: some View {
PhaseAnimator(OuterBreathState.allCases) { state in
Circle()
.background(.clear)
.frame(width: 150, height: 150)
.padding(.vertical)
.scaleEffect(state.scale)
.background(in: Circle())
.foregroundStyle(state.color)
} animation: {state in
switch state {
case .inhale: return .easeInOut(duration: 1.5)
case .exhale: return .easeInOut(duration: 3.5)
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("push") {
PhaseView()
}
}
}
}
Pushing the view works just fine, but after popping, the app becomes unresponsive 😓
It's the content update that fails actually. If I make no updates to the Circle, everything works fine.
I also tried with the phaseAnimator
trailing function, I get the same behaviour.
Interesting fact : if I use a trigger (and even a timer that flips that trigger to mimic the infinite loop), it does not crash the app. Here is the code
enum OuterBreathState: CaseIterable {
case exhale, inhale
var scale: CGFloat {
switch self {
case .inhale: return 2
case .exhale: return 1
}
}
var color: Color {
switch self {
case .inhale: return .white
case .exhale: return .blue
}
}
}
struct PhaseView: View {
@State var animate = false
let timer = Timer.publish(every: 5, tolerance: .zero, on: .main, in: .default).autoconnect()
var body: some View {
PhaseAnimator(OuterBreathState.allCases, trigger: animate) { state in
Circle()
.background(.clear)
.frame(width: 150, height: 150)
.padding(.vertical)
.scaleEffect(state.scale)
.background(in: Circle())
.foregroundStyle(state.color)
} animation: {state in
switch state {
case .inhale: return .easeInOut(duration: 1.5)
case .exhale: return .easeInOut(duration: 3.5)
}
}
.onReceive(timer, perform: { _ in
animate.toggle()
})
.onTapGesture {
animate.toggle()
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("push") {
PhaseView()
}
}
}
}
#Preview {
ContentView()
}
Any idea why it crashes and how I can make it work ? Thanks!