Course outline

iOS / Swift / Accessible systems and target evidence / Lesson 14

Instruments, ARC, and performance evidence

Locate actual rendering or memory costs and reason about retained closures before proposing optimizations.

About 5 min readContent reviewed
On this page

Lesson goals

  • Define a measurable user interaction
  • Distinguish CPU, layout, rendering, and retention
  • Remove repeated transformation at the correct boundary

Define a measurable user interaction

A performance investigation begins with a repeatable task: scrolling a particular list, opening a detail view, filtering a representative dataset, or returning from an editor. Record build configuration, target hardware, OS, dataset size, and the metric that reflects the complaint. Debug instrumentation and a desktop preview can distort the experience of a release app on a phone. Use the appropriate Instruments or Xcode tools in an authorized target host and compare repeated samples. A single fast run or a small arithmetic example is not evidence of a universal optimization.

Distinguish CPU, layout, rendering, and retention

An expensive body calculation, repeated image decoding, complicated layout, and retained view models can produce different symptoms. Measure the phase rather than assuming every hitch means SwiftUI updates too often. Reducing body calls does not necessarily reduce a costly visual effect, and weak references do not make an inefficient filter fast. Memory growth across repeated open-and-close cycles deserves separate inspection from one-time cache allocation. Identify which owner keeps an object reachable and whether that lifetime is intentional. A cache plateau and continuing growth call for different conclusions.

Remove repeated transformation at the correct boundary

The first fragment builds display summaries from a fixed input snapshot. It keeps presentation text creation outside each row’s body and makes the input-output rule testable. In an actual model, recompute when relevant inputs change, not on every unrelated render. Caching requires a correct key: the same item count does not imply the same titles or seat values. Do not retain large projections indefinitely without a lifetime policy. This small mapping is a correctness example only; whether precomputation improves an actual app must come from measurement on representative data.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
struct SummaryInput {
    let title: String
    let seats: Int
}
func summaryLines(_ items: [SummaryInput]) -> [String] {
    items.map { "\($0.title): \($0.seats)" }
}
print(summaryLines([SummaryInput(title: "Maps", seats: 3)]))
Expected output
["Maps: 3"]

The output is predicted from a pure mapping. It is not a benchmark and does not establish device rendering cost.

Understand strong cycles in stored closures

ARC releases an instance when strong ownership no longer keeps it alive. If an object stores a closure and that closure strongly captures the same object, a cycle can retain both. The second fragment uses a weak capture for a callback that should not own its parent. A weak reference can become nil, so the callback deliberately does nothing when the owner is gone. Do not replace every capture with weak self automatically: some operations require a strong scoped lifetime, and unowned references require a stronger proof that the referenced object outlives every access.

swift Illustrative Swift or SwiftUI fragment; requires the declared toolchain host.
final class RefreshOwner {
    private(set) var updates = 0
    var onRefresh: (() -> Void)?
    func configure() {
        onRefresh = { [weak self] in self?.updates += 1 }
    }
}
let owner = RefreshOwner()
owner.configure()
owner.onRefresh?()
print(owner.updates)
Expected output
1

The stored callback does not strongly own RefreshOwner. This example does not measure deallocation timing or prove the absence of every retained reference.

Treat asynchronous lifetime and images separately

A retained Task can keep captured values alive until its work completes, even when a screen has disappeared. Cancellation and task ownership therefore belong in a memory investigation, but cancellation remains cooperative. Images also have their own budget: a compressed file can expand into a large decoded buffer, and using full-resolution content for tiny thumbnails may waste memory. Review downsampling and cache policy under documented APIs for the selected target. Do not prescribe arbitrary cache sizes or strip useful state merely to make one memory graph smaller. Preserve the task and correctness requirements.

Compare evidence after one focused change

Change one measured cause, repeat the same workload, and compare relevant latency, frame, and memory observations. Confirm that the improvement did not break selection identity, cancellation, localization, or accessibility. For a retention defect, repeat opening and closing the feature and inspect whether the intended owners disappear rather than relying only on a console print. Keep tool traces tied to the artifact and target actually measured. This course did not launch Instruments, profile an Apple process, or execute ARC examples; it teaches how to make those claims precise when authorized evidence exists.

Common errors and fixes

Weak captures are added as a universal optimization

Why it happens
Lifetime design was replaced with a mechanical rule.
How to fix it
Inspect the ownership graph and required callback lifetime.
Prevent it next time
Test behavior when the owner disappears.

A projection cache returns wrong same-length data

Why it happens
The key ignored content or filter dependencies.
How to fix it
Invalidate from all relevant inputs under one owner.
Prevent it next time
Include replacement fixtures with equal counts.

Debug timing is reported as release performance

Why it happens
Build and instrumentation overhead were ignored.
How to fix it
Measure the relevant artifact configuration on representative targets.
Prevent it next time
Record workload, host, and build mode with metrics.

Check your understanding

1. Does weak self prove an object has no memory leak?

Show worked solution

No. It removes one possible strong edge. Other closures, tasks, subscriptions, caches, or owners may still retain the instance. Inspect the actual ownership graph and repeated lifetime behavior. Preserve strong ownership where the operation legitimately requires the object to remain alive.

2. What should be done before replacing a standard List for performance reasons?

Show worked solution

Capture the problematic workload and identify whether row computation, images, layout, rendering, or retention dominates. Fix that measured cause and compare. A custom container can add accessibility and lifecycle complexity without improving the real bottleneck.

Key takeaways

  • Define a measurable user interaction
  • Compare evidence after one focused change

Official references