The weird Animation and List crash

A weird crash when navigating between pages, one of which had animation, the other which had a list. Spoiler: it got solved fairly easily.

Navigating between pages

Let's take a simple NavigationView with two NavigationLinks—one to a page laying out a list of words using ForEach, the other laying out a list of words using List. Okay, so probably not the most useful app at this stage.

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Spacer()
                
                NavigationLink(destination: PageOneView()) {
                    Text("Page One")
                }
                
                Spacer()
                
                NavigationLink(destination: PageTwoView()) {
                    Text("Page Two")
                }
                
                Spacer()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct PageOneView: View {
    let words = ["One", "Two", "Three", "Four"]
    
    var body: some View {
        ForEach(words, id: \.self) { word in
            Text("\(word)")
        }
    }
}

struct PageTwoView: View {
    let words = ["Five", "Six", "Seven", "Eight"]
    
    var body: some View {
        List(words, id: \.self) { word in
            Text("\(word)")
        }
    }
}

The app runs perfectly on the device but is lacking that certain something—well, apart from proper content and a purpose. It needs a shape on the first page!

Adding the capsule

Right at the top of our VStack we'll add a Capsule:

                 Capsule()
                    .fill(Color.purple)
                    .frame(width: 20.0, height: 120.0)

The app runs, but there's still something missing—animation!

Adding the animation

We'll add a @State var to hold the rotationAngle, tell the capsule that it has a rotationEffect, and that it should animate forever. And then throw in an onAppear so we can set the rotation angle to animate to and from.

struct ContentView: View {
    @State var rotationAngle: Double = 0.0

    var body: some View {
        NavigationView {
            VStack {
                Capsule()
                    .fill(Color.purple)
                    .frame(width: 20.0, height: 120.0)
                    .rotationEffect(.degrees(rotationAngle))
                    .animation(
			Animation.easeInOut(duration: 2.0)
                        		.repeatForever(autoreverses: true)
                    )
                    .onAppear() {
                        self.rotationAngle = 180.0
                    }

                Spacer()
                
                NavigationLink(destination: PageOneView()) {
                    Text("Page One")
                }
                
                Spacer()
                
                NavigationLink(destination: PageTwoView()) {
                    Text("Page Two")
                }
                
                Spacer()
            }
        }
    }
}

The app runs, the capsule spins, we can go to Page One and back, and we can go to Page Two and, umm, crash on the way back. But only on this particular device (an iPhone XS running iOS 13.4)—it works okay on the Simulator.

If we comment out the animation it works fine; if we avoid using a List on the second page it works fine. It appears that something to do with the combination of .animation and List is causing a conflict somewhere.

The crash

The backtrace is lengthy—removed a mere twenty-one thousand repeated lines between frame #10 and #21159.

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x16ce5ffe0)
    frame #0: 0x0000000102f46498 libMainThreadChecker.dylib`checker_c + 8
    frame #1: 0x0000000102f45680 libMainThreadChecker.dylib`trampoline_c + 76
    frame #2: 0x0000000102f05424 libMainThreadChecker.dylib`handler_start + 52
    frame #3: 0x0000000102f092d4 libMainThreadChecker.dylib`__trampolines + 16000
    frame #4: 0x00000001ac4ba03c UIKitCore`-[UIViewController _ancestorViewControllerOfClass:allowModalParent:] + 48
    frame #5: 0x00000001ac40d288 UIKitCore`-[UINavigationController _outermostNavigationController] + 52
    frame #6: 0x00000001ac422c70 UIKitCore`-[UINavigationController _navigationBar:itemEnabledAutoScrollTransition:] + 92
    frame #7: 0x00000001ac422d18 UIKitCore`-[UINavigationController _navigationBar:itemEnabledAutoScrollTransition:] + 260
    frame #8: 0x00000001ac422d18 UIKitCore`-[UINavigationController _navigationBar:itemEnabledAutoScrollTransition:] + 260
    frame #9: 0x00000001ac422d18 UIKitCore`-[UINavigationController _navigationBar:itemEnabledAutoScrollTransition:] + 260
    frame #10: 0x00000001ac422d18 UIKitCore`-[UINavigationController _navigationBar:itemEnabledAutoScrollTransition:] + 260

   ...
   
    frame #21159: 0x00000001ac422d18 UIKitCore`-[UINavigationController _navigationBar:itemEnabledAutoScrollTransition:] + 260
    frame #21160: 0x00000001e1238a58 SwiftUI`SwiftUI.(DestinationHostingController in _68D0EA466201B2BCFA061800A250D32F).viewDidLayoutSubviews() -> () + 280
    frame #21161: 0x00000001e1238ab0 SwiftUI`@objc SwiftUI.(DestinationHostingController in _68D0EA466201B2BCFA061800A250D32F).viewDidLayoutSubviews() -> () + 28
    frame #21162: 0x00000001acfb04fc UIKitCore`-[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2648
    frame #21163: 0x00000001af69b6a8 QuartzCore`-[CALayer layoutSublayers] + 292
    frame #21164: 0x00000001af69bae8 QuartzCore`CA::Layer::layout_if_needed(CA::Transaction*) + 472
    frame #21165: 0x00000001af6ae034 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 144
    frame #21166: 0x00000001af5f3cf4 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double) + 304
    frame #21167: 0x00000001af61e840 QuartzCore`CA::Transaction::commit() + 656
    frame #21168: 0x00000001af61f438 QuartzCore`CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 96
    frame #21169: 0x00000001a895e094 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 36
    frame #21170: 0x00000001a8958d24 CoreFoundation`__CFRunLoopDoObservers + 420
    frame #21171: 0x00000001a89591c4 CoreFoundation`__CFRunLoopRun + 1020
    frame #21172: 0x00000001a8958aa0 CoreFoundation`CFRunLoopRunSpecific + 480
    frame #21173: 0x00000001b318f604 GraphicsServices`GSEventRunModal + 164
    frame #21174: 0x00000001acb004d4 UIKitCore`UIApplicationMain + 1944
  * frame #21175: 0x0000000102eab090 TestAnimationCrash`main at AppDelegate.swift:12:7
    frame #21176: 0x00000001a87d41ec libdyld.dylib`start + 4

Obviously the repeated frames are related to the animation of the navigation pages sliding in and out of view. This is exciting—a chance to pore through the backtrace and find out why it is crashing like this!

Testing on another device

But first, let's try it on an iPad Pro running iOS 13.4. First run shows that we've completely forgotten about the Master/Detail stack view appearance on iPads resultling in the big white screen that makes one think the application has forgotten to do anything until you remember to slide in the navigation panel. So let us add:

 .navigationViewStyle(StackNavigationViewStyle()) 

to the NavigtionView. Strange—the app runs perfectly on the iPad—no crashes when going back from Page Two.

Back to the iPhone

Puzzled that the code works fine on the iPad but not on the iPhone, let's run the app again and have another look at the backtrace. Except it works now. Whatever magic StackNavigationViewStyle() does it appears to have fixed the problem. Which on one hand is rather nice, but on the other hand it means diving into the backtrace to determine what was going wrong is no longer required. Maybe later.