[iOS 18 Only] Intermittent Crash at completeTransition in Custom Navigation Animation (Firebase Crashlytics)

Hi everyone,

I'm encountering an intermittent crash on iOS 18 only (not reproducible locally, reported in Firebase Crashlytics) at transitionContext.completeTransition(!transitionContext.transitionWasCancelled) within my custom UIViewControllerAnimatedTransitioning. The same code runs fine on iOS 16 and 17 (no Crashlytics report for those iOS version) Here's the crash log:

Crashed: com.apple.main-thread
0  libswiftCore.dylib         0x4391f0 swift_getObjectType + 40
1  ROOM                       0x490c48 ItemDetailAnimator.navigationController(_:animationControllerFor:from:to:) + 47 (ItemDetailAnimator.swift:47)
2  ROOM                       0x490f3c @objc ItemDetailAnimator.navigationController(_:animationControllerFor:from:to:) + 92 (<compiler-generated>:92)
3  UIKitCore                  0xa2d7a4 -[UINavigationController _customTransitionController:] + 516
4  UIKitCore                  0x2e51dc -[UINavigationController _immediatelyApplyViewControllers:transition:animated:operation:] + 2620
5  UIKitCore                  0x1541d4 __94-[UINavigationController _applyViewControllers:transition:animated:operation:rescheduleBlock:]_block_invoke + 100
6  UIKitCore                  0x150768 -[UINavigationController _applyViewControllers:transition:animated:operation:rescheduleBlock:] + 776
7  UIKitCore                  0x2e7e44 -[UINavigationController pushViewController:transition:forceImmediate:] + 544
8  UIKitCore                  0x2e4230 -[UINavigationController pushViewController:animated:] + 444
9  ROOM                       0x66cb04 UINavigationController.pushViewController(_:animated:completion:) + 185 (UINavigationController+Room.swift:185)
10 ROOM                       0x8cef4c ItemDetailCoordinator.start(animated:completion:) + 99 (ItemDetailCoordinator.swift:99)
11 ROOM                       0xc6c95c protocol witness for Coordinator.start(animated:completion:) in conformance BaseCoordinator + 24 (<compiler-generated>:24)
12 ROOM                       0x8ca520 AppCoordinator.startCoordinator(_:url:reference:animated:completion:) + 729 (AppCoordinator.swift:729)
13 ROOM                       0x8cb248 protocol witness for URLSupportCoordinatorOpener.startCoordinator(_:url:reference:animated:completion:) in conformance AppCoordinator + 48 (<compiler-generated>:48)
14 ROOM                       0xd6166c URLSupportCoordinatorOpener<>.open(url:openingController:reference:animated:completion:) + 118 (URLSupportedCoordinator.swift:118)
15 ROOM                       0xc56038 RRAppDelegate.handleURL(url:completion:) + 588 (RRAppDelegate.swift:588)
16 ROOM                       0xc502d0 RRAppDelegate.applicationDidBecomeActive(_:) + 330 (RRAppDelegate.swift:330)
17 ROOM                       0xc5041c @objc RRAppDelegate.applicationDidBecomeActive(_:) + 52 (<compiler-generated>:52)
18 UIKitCore                  0x1fb048 -[UIApplication _stopDeactivatingForReason:] + 1368







My animateTransition code is:

```func animateTransition(
            using transitionContext: UIViewControllerContextTransitioning) {
            guard let (fromView, toView, fromVC, toVC)
                = filterTargets(context: transitionContext) else {
                    transitionContext.cancelInteractiveTransition()
                    transitionContext.completeTransition(false)
                    return
            }
            let containerView = transitionContext.containerView
            toView.frame = transitionContext.finalFrame(for: toVC)
            guard let targetView = fromVC.animationTargetView,
                let fromFrame = fromVC.animationTargetFrame,
                let toFrame = toVC.animationTargetFrame
                else {
                    containerView.insertSubview(toView, aboveSubview: fromView)
                    toView.frame = transitionContext.finalFrame(for: toVC)
                    transitionContext.completeTransition(true)
                    return
            }
            let newFromFrame = fromView.convert(fromFrame, to: containerView)
            let tempImageView: UIImageView
            if let target = targetView as? UIImageView,
                let image = targetImage ?? target.image,
                image.size.height != 0,
                target.frame.height != 0,
                image.size.width / image.size.height != target.frame.width / target.frame.height {
                targetImage = image
                tempImageView = UIImageView(image: image)
                tempImageView.frame = newFromFrame
                tempImageView.contentMode = .scaleAspectFit
            } else {
                tempImageView = targetView.room.asImageView()
                tempImageView.frame = newFromFrame
            }
            targetView.isHidden = true
            let tempFromView = containerView.room.asImageView()
            targetView.isHidden = false
            let tempHideView = UIView()
            containerView.addSubview(tempFromView)
            containerView.insertSubview(toView, aboveSubview: tempFromView)
            tempHideView.backgroundColor = .white
            toView.addSubview(tempHideView)
            containerView.addSubview(tempImageView)
            //Minus with item detail view y position
            //Need to minus navigation bar height of item detail view
            var tempHideViewFrame = toFrame
            tempHideViewFrame.origin.y -= toView.frame.origin.y
            tempHideView.frame = tempHideViewFrame
            let duration = transitionDuration(using: transitionContext)
            toView.alpha = 0
            UIView.animate(withDuration: duration * 0.5, delay: duration * 0.5, options: .curveLinear, animations: {
                toView.alpha = 1
            })
            let scale: CGFloat = toFrame.width / newFromFrame.width
            let newFrame = CGRect(
                x: toFrame.minX - newFromFrame.minX * scale,
                y: toFrame.minY - newFromFrame.minY * scale,
                width: tempFromView.frame.size.width * scale,
                height: tempFromView.frame.size.height * scale)
            UIView.animate(withDuration: duration, delay: 0.0, options: [.curveEaseInOut], animations: {
                tempFromView.frame = newFrame
                tempImageView.frame = toFrame
            }, completion: { _ in
                tempHideView.removeFromSuperview()
                tempFromView.removeFromSuperview()
                tempImageView.removeFromSuperview()
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })
        }

My question is: What could be causing this intermittent crash specifically on iOS 18? Any insights or debugging suggestions for an unreproducible crash would be appreciated.

Take a look at the documentation for debugging techniques that has guidance on debugging common reasons for a crash. If you get stuck, let us know what you tried, and attach the fully symbolicated Apple crash report in your reply. Posting a Crash Report explains how to do so.

We have reviewed the documentation and the common reasons for EXC_BAD_ACCESS crashes. As highlighted in our initial report, we are encountering a EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0 crash, with the top of the stack trace indicating an issue within swift_getObjectType during the execution of our custom navigation controller delegate method.

protocol PushPopAnimatable: AnyObject {
    var animationTargetView: UIView? { get }
    /// Set targetview's frame that is relative to viewController's view
    /// e.g. targetView.superview?.convert(targetView.frame, to: viewController.view)
    var animationTargetFrame: CGRect? { get }
}

typealias PushPopAnimatableViewController
    = PushPopAnimatable & UIViewController

func navigationController(
    _ navigationController: UINavigationController,
    animationControllerFor operation: UINavigationController.Operation,
    from fromVC: UIViewController, to toVC: UIViewController)
    -> UIViewControllerAnimatedTransitioning? {
        switch operation {
        case .push:
            if fromVC is PushPopAnimatableViewController, // Crash occurs here
               toVC is ItemDetailViewController {
                return PushAnimator() as UIViewControllerAnimatedTransitioning
            }
        // ... other cases
        }
        return nil
}

The crash consistently occurs on the line checking if fromVC conforms to our PushPopAnimatableViewController type alias (which is a combination of UIViewController and the PushPopAnimatable protocol).

As the stack trace suggests, the crash happens when the system attempts to determine the type of fromVC using swift_getObjectType. This strongly indicates that at the moment this delegate method is called, the fromVC object is in an invalid state (likely deallocated or a dangling pointer).

Based on our analysis and the fact that this crash is predominantly, if not exclusively, occurring on iOS 18 (we have no similar reports from iOS 16 or 17 in Firebase Crashlytics), we suspect a potential change in the way UINavigationController manages view controller lifecycles or calls its delegate methods in iOS 18 might be exposing this issue.

We have already:

  • Examined our custom animation code (PushAnimator, PopAnimator) for potential memory management issues and retain cycles involving the fromVC.
  • Reviewed the lifecycle of the fromVC (which is the parent view controller in the navigation stack) to understand why it might be in an invalid state during the transition.

We are still unable to reliably reproduce this crash locally on our development devices.

Attached to this reply is a fully symbolicated Apple crash report for your review. We hope this will provide further context and insight into the specific conditions leading to this crash on iOS 18.

Apple crash report:

Firebase crash report:

We would appreciate any specific guidance you can offer regarding potential changes in UINavigationController behavior in iOS 18 that might be relevant to this scenario, or any further debugging steps you recommend given our findings.

Thank you for your assistance.

I have another question related to above issue:

  1. Lifecycle Change: Are there any known or undocumented changes in iOS 18 Beta that could affect the lifecycle of UIViewController instances during UINavigationController push transitions, specifically related to the animationControllerFor delegate method? We suspect fromVC might be in an invalid state when the delegate is called.

  2. Delegate Timing Guarantee: Does UINavigationController guarantee the validity of the fromVC object for the entire duration of the animationControllerFor delegate call on iOS 18? If not, what is the recommended approach to ensure fromVC is still valid?

  3. Known Issue: Is this a known issue within the iOS 18 Beta UINavigationController implementation? If so, is there a recommended workaround or fix in progress?

[iOS 18 Only] Intermittent Crash at completeTransition in Custom Navigation Animation (Firebase Crashlytics)
 
 
Q