Memory Leak Diagnosis Skill · diff
v1 to git:20260814.a9c26d7
102 added, 565 removed. Audit A to A.
---
name: Memory Leak Diagnosis Skill
- description: Detecting and fixing memory leaks and retain cycles in Swift apps using Instruments and best practices.
- version: 1.0
- activation: Activate for queries on memory leaks, retain cycles, Instruments leaks detection, ARC issues, or memory management problems.
+ description: Diagnose, explain, and fix Swift and Apple-platform memory leaks, unexpected object retention, deinit failures, heap growth, retain cycles, runaway caches, task and AsyncSequence lifetimes, timer/delegate/closure ownership, jetsam, and ARC issues using Xcode Memory Graph and Instruments Allocations, Leaks, and VM tools. Use when objects do not deallocate, memory rises across repeated workflows, or weak/unowned and capture-list choices are unclear. Do not enable Zombies for leak measurement, call every temporary retention a leak, or use unowned without a proven lifetime invariant.
---
- # Memory Leak Diagnosis Skill
-
- This skill provides expertise in identifying, diagnosing, and fixing memory leaks and retain cycles in Swift applications. It covers Instruments usage, ARC concepts, and memory management best practices for iOS development.
-
- ## Best Practices
-
- 1. **Understand ARC**: Automatic Reference Counting manages memory automatically, but cycles can still occur.
-
- 2. **Use Weak References**: Break retain cycles by using `weak` or `unowned` references in closures and delegates.
-
- 3. **Profile Regularly**: Use Instruments to detect leaks early in development.
-
- 4. **Avoid Strong Reference Cycles**: Be aware of parent-child relationships and delegate patterns.
+ # Swift Memory Diagnosis
- 5. **Clean Up Resources**: Properly invalidate timers, cancel network requests, and remove observers.
+ Prove the lifetime bug before changing capture lists. “Memory grew,” “deinit has not run yet,” “an object is retained,” and “an allocation is leaked” describe different evidence.
- 6. **Test Memory Usage**: Monitor memory growth during app usage.
+ ## Compatibility baseline
- ## Memory Management Guidelines
+ ARC, weak references, and Xcode’s core memory tools apply across currently supported Swift/Apple-platform projects. State version requirements for the specific APIs in the inspected code:
- - Classes create strong references by default.
- - Use `weak` for optional relationships that can become nil.
- - Use `unowned` for non-optional relationships that will always exist.
- - Capture `self` weakly in closures to prevent retain cycles.
- - Use Instruments Leaks and Allocations instruments for diagnosis.
- - Monitor memory usage with Xcode's Memory Graph Debugger.
+ - Structured concurrency and `Task` require Swift 5.5 and the corresponding platform concurrency runtime.
+ - The task example in this skill uses duration-based sleep, available with Swift 5.7-era toolchains and iOS 16 / macOS 13.
+ - Observation, SwiftData, Combine, and framework-specific lifetime rules depend on the app’s declared deployment targets.
+ - Xcode and Instruments UI labels change. Describe goals and inspected relationships, not assumed arrow colors or displayed reference-count numbers.
- ## Examples
+ ## Classify the symptom
- ### Example 1: Fixing Retain Cycles in Closures
+ | Observation | Likely category | Best first tool |
+ | --- | --- | --- |
+ | A dismissed screen remains reachable | Unwanted retention / ownership bug | Memory Graph |
+ | Heap grows after repeating one workflow | Persistent allocations, cache, or leak | Allocations generations |
+ | Leaks instrument reports unreachable blocks | Heap leak | Leaks + allocation backtrace |
+ | Memory falls after pressure or cache purge | Cache / transient high-water mark | Allocations + signposts |
+ | Process is killed under memory pressure | Excess footprint / jetsam | Organizer report, VM Tracker, Allocations |
+ | Crash after replacing `weak` with `unowned` | Lifetime invariant violation | Crash backtrace + ownership graph |
- **User Prompt:** "Fix the retain cycle in this network manager that uses a completion handler."
+ The Leaks instrument does not find every strongly retained object. A retain cycle can remain reachable from framework or application roots and appear only in Memory Graph or repeated Allocations generations.
- **Expected Output:**
- ```swift
- import Foundation
+ ## Evidence-first workflow
- // PROBLEMATIC CODE - Retain Cycle
- class NetworkManagerBad {
- var completionHandler: (() -> Void)?
-
- func fetchData() {
- // This creates a retain cycle: self -> completionHandler -> self
- self.completionHandler = {
- print("Data fetched")
- self.processData() // 'self' is strongly captured
- }
-
- // Simulate async operation
- DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
- self.completionHandler?()
- }
- }
-
- func processData() {
- print("Processing data")
- }
-
- deinit {
- print("NetworkManagerBad deallocated")
- }
- }
+ 1. Reproduce one bounded lifecycle: present/use/dismiss, start/stop, subscribe/unsubscribe, or load/purge.
+ 2. Add temporary `deinit` probes to expected owners and dependencies.
+ 3. Repeat the lifecycle several times. Record settled footprint after idle, not only peak memory.
+ 4. Capture Memory Graph after the object should be gone. Inspect incoming strong paths back to roots.
+ 5. Use Allocations generation marks before and after each repetition. Look for instance counts that monotonically persist.
+ 6. Run Leaks for unreachable allocations and inspect their allocation backtraces.
+ 7. Inspect tasks, streams, timers, observers, sessions, display links, delegates, and caches that cross the lifecycle boundary.
+ 8. Fix the ownership or cancellation contract, not merely the nearest closure.
+ 9. Repeat the identical workflow and verify deinitialization, stable generation counts, and acceptable footprint.
- // FIXED CODE - Using weak self
- class NetworkManagerGood {
- var completionHandler: (() -> Void)?
-
- func fetchData() {
- // Use [weak self] to break the retain cycle
- self.completionHandler = { [weak self] in
- print("Data fetched")
- self?.processData() // 'self' is now weakly captured
- }
-
- // Simulate async operation
- DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
- self.completionHandler?()
- }
- }
-
- func processData() {
- print("Processing data")
- }
-
- deinit {
- print("NetworkManagerGood deallocated") // This will now print!
- }
- }
+ Use Zombies only to diagnose messaging/use-after-free. Zombies intentionally preserve deallocated objects and invalidate leak and footprint measurements.
- // Alternative: Using unowned for guaranteed non-nil self
- class NetworkManagerUnowned {
- var completionHandler: (() -> Void)?
-
- func fetchData() {
- // Use [unowned self] if you're certain self won't be nil
- self.completionHandler = { [unowned self] in
- print("Data fetched")
- self.processData() // No optional chaining needed
- }
-
- DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
- self.completionHandler?()
- }
- }
-
- func processData() {
- print("Processing data")
- }
-
- deinit {
- print("NetworkManagerUnowned deallocated")
- }
- }
+ Read [examples/example_instruments_leaks.md](examples/example_instruments_leaks.md) for a repeatable capture and verification procedure.
- // Usage example
- func testMemoryLeak() {
- print("Creating NetworkManagerBad...")
- var managerBad: NetworkManagerBad? = NetworkManagerBad()
- managerBad?.fetchData()
- managerBad = nil // Won't deallocate due to retain cycle
-
- print("Creating NetworkManagerGood...")
- var managerGood: NetworkManagerGood? = NetworkManagerGood()
- managerGood?.fetchData()
- managerGood = nil // Will deallocate properly
-
- // Give time for async operations
- RunLoop.current.run(until: Date(timeIntervalSinceNow: 2))
- }
- ```
+ ## ARC rules
- ### Example 2: Delegate Pattern Retain Cycles
+ - Strong is the default for ownership.
+ - Use `weak` when the referenced object may deallocate first. A weak property must be optional and becomes `nil` automatically.
+ - Use `unowned` only when the referenced object is guaranteed to outlive every access. A violated invariant traps; “avoids optional handling” is not a valid reason.
+ - Delegates are commonly weak because the delegate often owns the delegating object, but verify the actual graph.
+ - A closure forms a cycle only when an owner stores the closure and that closure strongly captures the owner (directly or through another path).
+ - A queue or task may retain a captured object only until work finishes. That can be harmful delayed release, but it is not automatically a permanent cycle.
- **User Prompt:** "Fix the retain cycle between a view controller and its delegate."
+ ### Correct delegate graphs
- **Expected Output:**
```swift
- import UIKit
-
- // PROBLEMATIC CODE
- protocol DataManagerDelegateBad: AnyObject {
- func dataDidUpdate(_ data: [String])
- }
-
- class DataManagerBad {
- weak var delegate: DataManagerDelegateBad? // This is correct
-
- func fetchData() {
- // Simulate network call
- DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
- let data = ["Item 1", "Item 2", "Item 3"]
- self.delegate?.dataDidUpdate(data)
- }
- }
- }
-
- class ViewControllerBad: UIViewController, DataManagerDelegateBad {
- var dataManager: DataManagerBad?
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- dataManager = DataManagerBad()
- dataManager?.delegate = self // This creates: VC -> dataManager -> delegate -> VC
-
- dataManager?.fetchData()
- }
-
- func dataDidUpdate(_ data: [String]) {
- print("Received data: \(data)")
- }
-
- deinit {
- print("ViewControllerBad deallocated")
- }
- }
-
- // The issue is that ViewController strongly holds DataManager,
- // DataManager weakly holds the delegate (ViewController),
- // but the closure in fetchData strongly captures self (DataManager).
- // When ViewController is dismissed, DataManager's closure keeps it alive.
-
- // FIXED CODE
- protocol DataManagerDelegate: AnyObject {
- func dataDidUpdate(_ data: [String])
- }
-
- class DataManager {
- weak var delegate: DataManagerDelegate?
-
- func fetchData() {
- // Use [weak self] in the closure to break any potential cycle
- DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
- let data = ["Item 1", "Item 2", "Item 3"]
- self?.delegate?.dataDidUpdate(data)
- }
- }
-
- deinit {
- print("DataManager deallocated")
- }
- }
-
- class ViewController: UIViewController, DataManagerDelegate {
- var dataManager: DataManager?
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- dataManager = DataManager()
- dataManager?.delegate = self
-
- dataManager?.fetchData()
- }
-
- func dataDidUpdate(_ data: [String]) {
- print("Received data: \(data)")
- }
-
- deinit {
- print("ViewController deallocated") // This will now print!
- }
- }
+ protocol DataDelegate: AnyObject {}
- // Alternative pattern: Using a closure instead of delegate
- class DataManagerClosure {
- var onDataUpdate: (([String]) -> Void)?
-
- func fetchData() {
- DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
- let data = ["Item 1", "Item 2", "Item 3"]
- self?.onDataUpdate?(data)
- }
- }
-
- deinit {
- print("DataManagerClosure deallocated")
- }
+ final class StrongDelegateManager {
+ var delegate: (any DataDelegate)?
}
- class ViewControllerClosure: UIViewController {
- var dataManager: DataManagerClosure?
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- dataManager = DataManagerClosure()
- // Use [weak self] to avoid retain cycle
- dataManager?.onDataUpdate = { [weak self] data in
- print("Received data: \(data)")
- }
-
- dataManager?.fetchData()
- }
-
- deinit {
- print("ViewControllerClosure deallocated")
- }
+ final class ScreenOwner: DataDelegate {
+ let manager = StrongDelegateManager()
+ init() { manager.delegate = self }
}
```
- ### Example 3: Instruments Leaks Detection
-
- **User Prompt:** "How do I use Instruments to detect memory leaks in my iOS app?"
-
- **Expected Output:**
- To detect memory leaks using Instruments:
-
- 1. **Open Instruments**: In Xcode, go to Product > Profile (⌘I)
-
- 2. **Choose Leaks Instrument**: Select the "Leaks" template
-
- 3. **Configure Recording**:
- - Target your app
- - Make sure "Record reference counts" is enabled
- - Start recording
-
- 4. **Use Your App**: Navigate through the screens where you suspect leaks
-
- 5. **Analyze Results**:
- - Look for red bars in the Leaks track - these indicate memory leaks
- - The Detail pane shows leaked objects and their allocation backtraces
- - Use the "Cycles & Roots" view to see retain cycles
-
- 6. **Identify the Problem**:
- - Click on a leak to see the allocation history
- - Look for objects that should have been deallocated but weren't
- - Check for strong reference cycles
-
- **Example Leaks Output Interpretation:**
-
- ```
- Leaks detected: 5
- - Leaked Object: ViewController (0x7f9b8c0a5e00)
- - Responsible Library: UIKit
- - Allocation: -[UIViewController initWithNibName:bundle:]
- - Backtrace shows it was created in viewDidLoad of parent VC
- - Never deallocated because of retain cycle with timer
-
- - Leaked Object: Timer (0x7f9b8c0a6120)
- - Responsible Library: Foundation
- - Allocation: +[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]
- - Strong reference to target (ViewController) prevents deallocation
- ```
-
- **Common Leak Patterns to Look For:**
-
- 1. **Timer Retain Cycles**:
- ```swift
- // BAD
- class MyViewController: UIViewController {
- var timer: Timer?
-
- func startTimer() {
- timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(update), userInfo: nil, repeats: true)
- }
-
- @objc func update() {
- // Update UI
- }
- }
-
- // GOOD
- class MyViewController: UIViewController {
- var timer: Timer?
-
- func startTimer() {
- timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
- self?.update()
- }
- }
-
- func update() {
- // Update UI
- }
-
- deinit {
- timer?.invalidate()
- }
- }
- ```
+ This graph cycles: owner → manager → delegate → owner. Making `delegate` weak breaks it. If `delegate` is already weak, the delegate relationship is not the cycle; inspect stored callbacks, tasks, and external owners instead.
- 2. **Closure Capture Issues**:
- ```swift
- // BAD
- var completion: (() -> Void)?
- completion = {
- self.doSomething() // Creates retain cycle
- }
+ ### Correct bidirectional graph
- // GOOD
- completion = { [weak self] in
- self?.doSomething() // Breaks the cycle
- }
- ```
+ Both links below are strong, so the graph cycles:
- 3. **Delegate Strong References**:
```swift
- // BAD
- class MyObject {
- var delegate: MyDelegate? // Strong reference
- }
-
- // GOOD
- class MyObject {
- weak var delegate: MyDelegate? // Weak reference
- }
+ final class Person { var car: Car? }
+ final class Car { var owner: Person? }
```
- ### Example 4: Memory Graph Debugger
-
- **User Prompt:** "Use Xcode's Memory Graph Debugger to identify and fix a retain cycle."
-
- **Expected Output:**
- To use the Memory Graph Debugger:
-
- 1. **Enable Memory Graph**: In Xcode, go to Debug > Debug Workflow > View Memory Graph (⌘⌃M)
-
- 2. **Trigger the Debugger**: Run your app and navigate to create the suspected leak
-
- 3. **Analyze the Graph**:
- - Look for objects that should be deallocated but remain in memory
- - Purple arrows show strong references
- - Gray arrows show weak references
- - Numbers show reference counts
-
- 4. **Inspect Objects**: Click on objects to see their properties and references
-
- 5. **Find Cycles**: Look for circular references where objects keep each other alive
+ If the car does not own its person, make `Car.owner` weak. Do not then describe that weak link as incrementing a retain count.
- **Example Memory Graph Analysis:**
+ Read [examples/example_retain_cycle.swift](examples/example_retain_cycle.swift) for compiling stored-closure, delegate, bidirectional, value-container, and task-lifetime examples.
- ```
- Root View Controller (ref count: 1)
- ├── Strong reference to Data Manager (ref count: 2)
- │ ├── Strong reference to Network Client (ref count: 1)
- │ └── Strong reference to Delegate (View Controller) (ref count: 1)
- │ └── Strong reference back to Data Manager
- └── Strong reference to Timer (ref count: 1)
- └── Strong reference to Timer Target (View Controller)
- ```
+ ## Closure and task lifetimes
- **Fixing the Cycle:**
+ Do not add `[weak self]` mechanically. Decide whether work should:
- ```swift
- class DataManager {
- weak var delegate: DataManagerDelegate? // Change to weak
-
- private var timer: Timer?
-
- func startPolling() {
- timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
- self?.fetchData()
- }
- }
-
- func stopPolling() {
- timer?.invalidate()
- timer = nil
- }
-
- private func fetchData() {
- // Network request with [weak self] in completion
- networkClient.fetch { [weak self] result in
- switch result {
- case .success(let data):
- self?.delegate?.dataDidUpdate(data)
- case .failure:
- break
- }
- }
- }
- }
+ - keep the owner alive until a finite operation completes;
+ - stop when the owner disappears; or
+ - be owned by a longer-lived service independent of the UI.
- class ViewController: UIViewController, DataManagerDelegate {
- private let dataManager = DataManager()
-
- override func viewDidLoad() {
- super.viewDidLoad()
- dataManager.delegate = self
- dataManager.startPolling()
- }
-
- override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- dataManager.stopPolling() // Clean up timer
- }
-
- func dataDidUpdate(_ data: [String]) {
- // Update UI
- }
- }
- ```
+ For indefinite work owned by a screen or model, store a task/token, cancel it in an explicit `stop`, and make the task avoid a strong capture across suspension points. A weak capture followed by `guard let self` outside an infinite loop still promotes `self` for the task’s entire remaining lifetime.
- ### Example 5: ARC and Reference Types
+ For `AsyncStream`, define who finishes the continuation and set `onTermination` when producer cleanup is required. For Combine, retain cancellables only for the intended owner lifetime. For `NotificationCenter` block observers, retain and remove the returned token. Invalidate `Timer` and `CADisplayLink`; invalidate URL sessions whose delegate lifetime should end.
- **User Prompt:** "Explain and fix ARC-related memory issues in a complex object graph."
+ Deliver UI-facing delegate callbacks and state mutations on `MainActor` rather than calling UIKit/AppKit from a background queue.
- **Expected Output:**
- ```swift
- import Foundation
+ ## Value types are not a blanket escape hatch
- // Understanding Reference Types
- class Person {
- let name: String
- var car: Car?
-
- init(name: String) {
- self.name = name
- print("\(name) initialized")
- }
-
- deinit {
- print("\(name) deinitialized")
- }
- }
+ Structs and enums do not have identity under ARC, but they can contain reference-typed storage or escaping closures. A class can strongly own a struct that owns a closure that strongly captures the class. Standard-library collections also use internal reference storage. Analyze the complete graph rather than declaring value types incapable of participating in cycles.
- class Car {
- let model: String
- weak var owner: Person? // Use weak to prevent cycle
-
- init(model: String) {
- self.model = model
- print("\(model) initialized")
- }
-
- deinit {
- print("\(model) deinitialized")
- }
- }
+ ## Memory Graph
- // BAD EXAMPLE - Retain Cycle
- func createRetainCycle() {
- print("=== Creating Retain Cycle ===")
- var person: Person? = Person(name: "John") // ref count: 1
- var car: Car? = Car(model: "Tesla") // ref count: 1
-
- person?.car = car // car ref count: 2 (person + car variable)
- car?.owner = person // person ref count: 2 (car + person variable)
-
- person = nil // person ref count: 1 (still held by car.owner)
- car = nil // car ref count: 1 (still held by person.car)
-
- // Neither object is deallocated!
- print("=== Memory leak occurred ===")
- }
+ 1. Reproduce and pause after the owner should be released.
+ 2. Open Debug Memory Graph.
+ 3. Search for the concrete type and compare instance count with the expected count.
+ 4. Select a stale instance and follow incoming strong references toward a root.
+ 5. Separate framework roots that are expected to persist from application ownership that should have ended.
+ 6. Capture the path and lifecycle that created it before editing code.
- // GOOD EXAMPLE - No Retain Cycle
- func createNoRetainCycle() {
- print("=== No Retain Cycle ===")
- var person: Person? = Person(name: "Jane") // ref count: 1
- var car: Car? = Car(model: "Honda") // ref count: 1
-
- person?.car = car // car ref count: 2
- car?.owner = person // person ref count: 1 (weak reference!)
-
- person = nil // person ref count: 0 -> deallocated
- car = nil // car ref count: 0 -> deallocated
-
- print("=== Both objects properly deallocated ===")
- }
+ Do not depend on a particular arrow color, UI icon, or exact reference count; those presentations vary by Xcode release and compiler optimization.
- // Complex Object Graph Example
- class Company {
- let name: String
- var employees: [Employee] = []
-
- init(name: String) {
- self.name = name
- print("Company \(name) initialized")
- }
-
- deinit {
- print("Company \(name) deinitialized")
- }
- }
+ ## Instruments
- class Employee {
- let name: String
- unowned let company: Company // unowned because company owns employee
-
- init(name: String, company: Company) {
- self.name = name
- self.company = company
- print("Employee \(name) initialized")
- }
-
- deinit {
- print("Employee \(name) deinitialized")
- }
- }
+ - **Allocations:** mark generations around repeated workflows; compare persistent instance counts, allocation sites, and backtraces.
+ - **Leaks:** inspect reported unreachable allocations; absence of a report does not prove a dismissed controller was released.
+ - **VM Tracker:** investigate image buffers, mapped files, graphics surfaces, and other virtual-memory categories outside ordinary object counts.
+ - **Points of Interest / signposts:** align allocation changes with user actions.
- func testComplexGraph() {
- print("=== Complex Object Graph ===")
- var company: Company? = Company(name: "Apple")
-
- // Create employees - company owns them strongly
- let employee1 = Employee(name: "John", company: company!)
- let employee2 = Employee(name: "Jane", company: company!)
-
- company?.employees = [employee1, employee2]
-
- company = nil // This will deallocate company AND all employees
-
- print("=== Complex graph deallocated ===")
- }
+ Distinguish bounded caches from leaks by documenting their limit, eviction trigger, response to memory pressure, and observed steady state. An unbounded cache is still a memory defect even if every object remains intentionally reachable.
- // Value Types vs Reference Types
- struct Address {
- var street: String
- var city: String
- }
+ ## Common ownership audit
- class PersonWithAddress {
- let name: String
- var address: Address // Value type - copied, not referenced
-
- init(name: String, address: Address) {
- self.name = name
- self.address = address
- }
- }
+ - Stored closures and completion handlers
+ - `Task`, task groups, actors, and continuations
+ - `AsyncStream` producers and consumers
+ - Combine subscriptions and notification tokens
+ - Timers, display links, animation callbacks, and run-loop sources
+ - Delegates and data sources
+ - URLSession delegates and outstanding requests
+ - KVO/context observations
+ - Image/data caches and autorelease-heavy loops
+ - SwiftUI state models, environment values, hosting controllers, and presentation closures
+ - Core Data / SwiftData contexts and fetched object graphs
- func testValueVsReference() {
- let address = Address(street: "123 Main St", city: "Springfield")
- var person1: PersonWithAddress? = PersonWithAddress(name: "John", address: address)
- var person2: PersonWithAddress? = PersonWithAddress(name: "Jane", address: address)
-
- person1?.address.city = "Changed City" // Only affects person1's copy
-
- print("Person1 city: \(person1?.address.city ?? "")")
- print("Person2 city: \(person2?.address.city ?? "")")
-
- person1 = nil // Only person1's struct is deallocated
- person2 = nil // Only person2's struct is deallocated
- // address was copied, so no reference counting involved
- }
- ```
+ ## Review checklist
- **Key ARC Concepts:**
+ - The reported stale type has a documented expected lifetime.
+ - There is a reproducible workflow and a baseline instance count.
+ - Incoming strong paths identify a root; no conclusion relies only on memory rising once.
+ - Weak and unowned choices state the actual lifetime invariant.
+ - Finite asynchronous work is distinguished from indefinite retained work.
+ - Cleanup has an explicit owner and can run on cancellation and error paths.
+ - Caches have bounds and memory-pressure behavior.
+ - The same workflow was repeated after the fix and reached a stable steady state.
- 1. **Strong References** (default): Increase reference count
- 2. **Weak References**: Don't increase reference count, automatically nil when object deallocated
- 3. **Unowned References**: Don't increase reference count, assume object won't be deallocated
- 4. **Value Types** (struct, enum): Copied, not referenced - no retain cycles possible
- 5. **Reference Types** (class): Shared instances - retain cycles possible
+ ## Supporting material
- **When to use each:**
+ - [README.md](README.md) summarizes the diagnostic model.
+ - [examples/example_retain_cycle.swift](examples/example_retain_cycle.swift) contains strict-concurrency-safe ownership examples.
+ - [examples/example_instruments_leaks.md](examples/example_instruments_leaks.md) provides an Instruments and Memory Graph workflow.
+ - [examples/prompts.md](examples/prompts.md) contains representative activation prompts.
- - `strong`: Default, use for owned relationships
- - `weak`: When reference can become nil, like delegates, parent references
- - `unowned`: When reference will never be nil during its lifetime, like self in closures where object owns the closure