Created
November 10, 2025 14:11
-
-
Save noppefoxwolf/73df9b747f4eaf365d3b3175fab616a7 to your computer and use it in GitHub Desktop.
[iOS26.1] Possible leak: Replacing tabs with identical identifier retains previous VC.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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