Skip to content

Instantly share code, notes, and snippets.

@noppefoxwolf
Created November 10, 2025 14:11
Show Gist options
  • Select an option

  • Save noppefoxwolf/73df9b747f4eaf365d3b3175fab616a7 to your computer and use it in GitHub Desktop.

Select an option

Save noppefoxwolf/73df9b747f4eaf365d3b3175fab616a7 to your computer and use it in GitHub Desktop.
[iOS26.1] Possible leak: Replacing tabs with identical identifier retains previous VC.
import UIKit
import SwiftUI
/// Minimal reproducible example for a potential memory leak when replacing `tabs` with
/// a new array that contains a tab with the same title/image/identifier.
///
/// Repro steps:
/// 1. Launch the app and open the Memory Graph (Product > Profile or Debug Memory Graph).
/// 2. This controller first sets `tabs` to contain ViewControllerA.
/// 3. It then immediately replaces `tabs` with a new array that contains ViewControllerB,
/// but with the SAME `title`, `image`, and `identifier` as the first tab.
/// 4. Observe in the memory graph: `TabItemViewControllerA` appears to be leaked (retained).
///
/// Expected:
/// - When replacing `tabs`, previously created view controllers should be released if they
/// are no longer referenced.
///
/// Actual:
/// - `TabItemViewControllerA` appears to remain in memory after `tabs` is replaced by a tab
/// with the same title/image/identifier.
final class ViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// MARK: 1) Set initial tab (A)
// Intentionally using the same title and identifier for A and B to demonstrate the issue.
let tabA = UITab(title: "TabA", image: nil, identifier: "a") { _ in
let vc = TabItemViewControllerA()
print("[Lifecycle] Creating TabItemViewControllerA: \(Unmanaged.passUnretained(vc).toOpaque())")
return vc
}
tabs = [tabA]
print("[Tabs] Set tabs = [A]")
// MARK: 2) Replace with another tab (B) that has the SAME title/image/identifier
// This mimics a real scenario where tabs might be rebuilt with identical identifiers.
let tabB = UITab(title: "TabA", image: nil, identifier: "a") { _ in
let vc = TabItemViewControllerB()
print("[Lifecycle] Creating TabItemViewControllerB: \(Unmanaged.passUnretained(vc).toOpaque())")
return vc
}
tabs = [tabB]
print("[Tabs] Replaced tabs = [B] (same title/image/identifier as A)")
// MARK: 3) Inspect memory graph
// Open the Memory Graph now. `TabItemViewControllerA` may still be retained.
print("[Repro] Now open Memory Graph to check if A is leaked/retained.")
}
}
/// Simple VC types to distinguish instances in Memory Graph
final class TabItemViewControllerA: UIViewController {
deinit {
print("[Lifecycle] Deinit TabItemViewControllerA")
}
}
final class TabItemViewControllerB: UIViewController {
deinit {
print("[Lifecycle] Deinit TabItemViewControllerB")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment