When you apply an animation to a custom Transition in Swift 6, it is likely that that the app will crash with a SwiftUI.AsyncRenderer dispatch_assert_queue_fail error. Non-animated Transitions do not crash nor do animated system transitions. If you use ViewModifiers to create an AnyTransition with the .modifier(active:, identity:) static method, there is no problem.
I used the example Transition in the docs for Transition to illustrate this problem.
I'm using Xcode 16.2 RC and Swift 6, running iOS 18.1.1 on an iPhone 16 Pro.
I've created two separate Previews that illustrate what specifically crashes the app and what doesn't as well as a workaround for this bug.
func generateRandomString() -> String {
let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<10).compactMap { _ in characters.randomElement() })
}
// MARK: Works
extension AnyTransition {
struct RotatingFadeTransitionModifier: ViewModifier {
let opacity: CGFloat
let rotation: Angle
func body(content: Content) -> some View {
content
.opacity(opacity)
.rotationEffect(rotation)
}
}
static var rotatingFade: AnyTransition {
.asymmetric(
insertion: .modifier(
active: RotatingFadeTransitionModifier(opacity: 0, rotation: .degrees(30)),
identity: RotatingFadeTransitionModifier(opacity: 1, rotation: .zero)
),
removal: .modifier(
active: RotatingFadeTransitionModifier(opacity: 0, rotation: .degrees(-30)),
identity: RotatingFadeTransitionModifier(opacity: 1, rotation: .zero)
)
)
}
}
struct WorkingTransitionView: View {
@State private var text: String = "some string"
var body: some View {
VStack(spacing: 32) {
Text("system transition: \(text)")
.id(text)
.transition(.slide)
// Gets the explicit Button animation applied instead of
// the transition animation
Text("animated system transition: \(text)")
.id(text)
.transition(.slide.animation(.bouncy(duration: 0.5)))
Text("custom transition: \(text)")
.id(text)
.transition(.rotatingFade)
Text("animated custom transition: \(text)")
.id(text)
.transition(.rotatingFade.animation(.bouncy( extraBounce: 0.5)))
Button("animated randomize - safe") {
withAnimation(.smooth(duration: 5.45, extraBounce: 0.15)) {
text = generateRandomString()
}
}
}
}
}
// MARK: Crashes
struct RotatingFadeTransition: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
content
.opacity(phase.isIdentity ? 1.0 : 0.0)
.rotationEffect(phase.rotation)
}
}
extension TransitionPhase {
fileprivate var rotation: Angle {
switch self {
case .willAppear: .degrees(30)
case .identity: .zero
case .didDisappear: .degrees(-30)
}
}
}
struct CrashingTransitionView: View {
@State private var text: String = "some string"
@State private var presentCustomTransitionText: Bool = false
@State private var presentAnimatedCustomTransitionText: Bool = false
var body: some View {
VStack(spacing: 32) {
Text("on 1-5 attempts generally, animated custom Transitions will crash with a SwiftUI.AsyncRenderer dispatch_assert_queue_fail")
Divider()
textWithSafeSystemTransition
if presentCustomTransitionText {
textWithCustomTransition
}
if presentAnimatedCustomTransitionText {
textWithAnimatedCustomTransition
}
Divider()
Text("Randomization")
Button("randomize - won't crash non-animated custom transition text") {
text = generateRandomString()
}
Button("animated randomize - will crash any custom transition text") {
withAnimation(.smooth(duration: 0.45, extraBounce: 0.15)) {
text = generateRandomString()
}
}
Divider()
Text("Text Presentation")
Button("present non-animated custom transition text") {
presentCustomTransitionText = true
}
Button("present animated custom transition text") {
presentAnimatedCustomTransitionText = true
}
}
}
private var textWithSafeSystemTransition: some View {
Text("safe, system transition: \(text)")
.id(text)
.transition(.slide)
}
private var textWithCustomTransition: some View {
Text("safe text, custom transition: \(text)")
.id(text)
.transition(RotatingFadeTransition())
}
private var textWithAnimatedCustomTransition: some View {
Text("crashing text: \(text)")
.id(text)
.transition(RotatingFadeTransition().animation(.smooth))
}
}
#Preview("Working Code") {
WorkingTransitionView()
}
#Preview("Crashing Code") {
CrashingTransitionView()
}