Fix: Custom UIBarButtonItem Flickers on Push Transitions in UIKit

Fix: Custom UIBarButtonItem Flickers on Push Transitions in UIKit

Written by

in

Quick answer: A custom UIBarButtonItem flickers during navigation transitions because UIKit treats distinct button instances on consecutive view controllers as separate elements, destroying and redrawing them mid-animation. To prevent this, assign matching identifiers to identical bar buttons across screens or host a persistent button on the navigation controller.

Why this happens

When you push a new view controller onto a UINavigationController stack, UIKit orchestrates a cross-dissolve and slide transition between the incoming and outgoing UINavigationItem objects. System-provided elements, such as standard back buttons and navigation titles, benefit from internal transition coordinators that morph or persist identical items across the bar.

Custom bar button items created with UIBarButtonItem(customView:) do not receive this treatment by default. If your outgoing view controller and incoming view controller each instantiate their own UIButton wrapped in a UIBarButtonItem, UIKit assumes these are entirely unrelated controls. As a result, the transition fades out the old custom view while simultaneously fading in the new one.

During rapid push animations or when using modern translucent bar styles, this simultaneous fade creates a visible drop in opacity and an awkward redrawing artifact known as bar button flicker. The visual interruption becomes especially pronounced when the two buttons use identical icons, frames, and background materials.

How to fix it

1. Assign matching identifiers across view controllers

If you want identical custom buttons to persist smoothly across consecutive screens, assign matching identifiers to the UIBarButtonItem instances. Supplying an explicit identifier signals to the navigation bar layout engine that the items on both view controllers represent the same functional control, allowing the transition coordinator to keep the item stable rather than destroying and recreating its view hierarchy.

import UIKit

class BaseNavigationViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setupRightBarButtonItem()
    }

    private func setupRightBarButtonItem() {
        let actionButton = UIButton(type: .system)
        actionButton.setImage(UIImage(systemName: "bell"), for: .normal)
        actionButton.addTarget(self, action: #selector(didTapNotifications), for: .touchUpInside)

        let barButton = UIBarButtonItem(customView: actionButton)

        // Assign a consistent identifier so UIKit treats these as the same item across transitions
        if #available(iOS 26.0, *) {
            barButton.identifier = "navigation.notifications.button"
        }

        navigationItem.setRightBarButton(barButton, animated: false)
    }

    @objc private func didTapNotifications() {
        let nextVC = DetailsViewController()
        navigationController?.pushViewController(nextVC, animated: true)
    }
}

class DetailsViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let actionButton = UIButton(type: .system)
        actionButton.setImage(UIImage(systemName: "bell"), for: .normal)

        let barButton = UIBarButtonItem(customView: actionButton)
        if #available(iOS 26.0, *) {
            barButton.identifier = "navigation.notifications.button"
        }

        navigationItem.setRightBarButton(barButton, animated: false)
    }
}

Ensure that the identifier string matches exactly between view controllers and that the button dimensions remain consistent so the bar does not perform an unnecessary resize animation.

2. Use standard image-based UIBarButtonItems instead of customView

Wrapping a standard UIButton inside UIBarButtonItem(customView:) is frequently unnecessary and disables UIKit’s built-in navigation item deduplication. If your custom button only displays an icon or text without specialized layout requirements, instantiate UIBarButtonItem directly using an image or title.

import UIKit

class MasterViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let shareItem = UIBarButtonItem(
            image: UIImage(systemName: "square.and.arrow.up"),
            style: .plain,
            target: self,
            action: #selector(handleShare)
        )

        navigationItem.rightBarButtonItem = shareItem
    }

    @objc private func handleShare() {
        let destination = SecondaryViewController()
        navigationController?.pushViewController(destination, animated: true)
    }
}

class SecondaryViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Direct image-based items are reconciled automatically by UIKit
        let shareItem = UIBarButtonItem(
            image: UIImage(systemName: "square.and.arrow.up"),
            style: .plain,
            target: self,
            action: #selector(handleShare)
        )

        navigationItem.rightBarButtonItem = shareItem
    }

    @objc private func handleShare() {
        // Contextual action for this screen
    }
}

Standard UIBarButtonItem objects allow UIKit to interpolate tint colors and position smoothly between navigation steps without cross-fading independent UIView subtrees.

3. Manage shared controls on the parent navigation controller

If a button must stay pinned in the exact same location across an entire flow (such as a profile avatar or close button), avoid recreating it inside every child view controller’s navigationItem. Instead, maintain the control on a custom UINavigationController subclass or adjust the navigation item actions dynamically based on the visible controller.

import UIKit

final class PersistentFlowNavigationController: UINavigationController {
    private let globalActionItem = UIBarButtonItem()

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self

        globalActionItem.image = UIImage(systemName: "gearshape")
        globalActionItem.target = self
        globalActionItem.action = #selector(performContextualAction)
    }

    @objc private func performContextualAction() {
        if let topActionProvider = topViewController as? NavigationActionProvider {
            topActionProvider.handleGlobalAction()
        }
    }
}

extension PersistentFlowNavigationController: UINavigationControllerDelegate {
    func navigationController(
        _ navigationController: UINavigationController,
        willShow viewController: UIViewController,
        animated: Bool
    ) {
        // Retain the single bar button reference across transitions
        viewController.navigationItem.rightBarButtonItem = globalActionItem
    }
}

protocol NavigationActionProvider: AnyObject {
    func handleGlobalAction()
}

By passing the identical UIBarButtonItem reference into each incoming child’s navigationItem, UIKit detects pointer equality and skips the remove-and-insert transition altogether.

How to prevent it

  • Prefer standard UIBarButtonItem initializers (init(image:style:target:action:) or init(title:style:target:action:)) over init(customView:) whenever custom subview hierarchies are not strictly required.
  • Assign stable identifier values to matching bar button items across all view controllers that participate in navigation transitions.
  • Avoid calling navigationItem.setRightBarButton(_:animated:) with animated: true inside viewDidLoad or viewWillAppear; allow the navigation controller’s push transition to manage bar animation timing.
  • Keep custom view bounds identical between screens; sudden frame mismatches force the navigation bar to recalculate geometry mid-flight, causing visual stutter.
  • Reuse a single UIBarButtonItem instance across screens via a common navigation controller delegate when controls persist through an entire user flow.

FAQ

Why does the system back button never flicker during navigation pushes?

The back button is managed directly by the navigation bar’s layout coordinator. UIKit tracks the navigation stack hierarchy internally and applies specialized vector and alpha morphing to system back indicators, bypassing standard view replacement passes.

Does assigning an accessibilityIdentifier stop transition flickering?

No. The accessibilityIdentifier property is reserved for UI testing and accessibility auditing. UIKit’s navigation bar transition coordinator relies on item identity and the identifier property to reconcile matching controls during push and pop sequences.

Can I fix custom view flickering by setting animated to false on setRightBarButton?

Calling navigationItem.setRightBarButton(item, animated: false) avoids triggering a secondary cross-fade within the view controller itself, but it does not eliminate the cross-fade performed by the navigation controller during the push transition. You must either provide consistent item identifiers or reuse the same bar button reference.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.