Modern iOS: SwiftUI, concurrency, and on-device machine learning

SwiftUI is not a widget toolkit with a nicer syntax; it is a claim that a user interface is a pure function of state, that the runtime can compute the difference between two renderings of that function cheaply enough to do it sixty or a hundred and twenty times a second, and that most UI bugs are really state-ownership bugs that a value-type model can prevent. This page derives that claim: how the framework decides which views to re-render, why a view body must be cheap and free of side effects, how the Observation framework in Swift 5.9 narrowed invalidation from "the whole object changed" to "this one property that this one view read changed", and how the layout engine negotiates size by proposal rather than by constraint solving. It then crosses into Swift 6: async/await, structured concurrency, and the actor model that makes data races a compile error rather than a Thursday-afternoon crash, with the underlying memory model deferred to the concurrency page. It closes on the hardware: the 8.3-millisecond frame budget a 120Hz display imposes, and the memory-bandwidth arithmetic that explains why a 3-to-4 billion parameter model is the practical ceiling for a language model running entirely on a phone.

Why this subject matters now

Five years ago an iOS engineer wrote imperative view controllers: an object owned a tree of UIViews, mutated their properties in response to events, and was personally responsible for keeping the pixels consistent with the model. The failure mode was the desynchronized view, a label that still shows the old price because one of the fourteen code paths that change the price forgot to call the method that updates the label. SwiftUI, shipped in 2019 and mature by 2023, replaces the imperative tree with a declarative function. The engineer describes what the interface should look like for a given state, and the framework is responsible for making the pixels match. The desynchronized view becomes structurally impossible, because there is no setter to forget; there is only the state, and a re-evaluation of the function whenever the state changes.

The thing a practitioner is expected to understand today, and was not expected to five years ago, is the machinery that makes this affordable. Re-evaluating a function of state a hundred times a second would be ruinous if it meant rebuilding the whole screen, so the framework does not do that; it computes a structural diff and touches only what changed. Understanding when a view re-renders, and why, is now the central performance skill, the way that understanding retain cycles was the central memory skill of the reference-counting era. Layered on top are three shifts that all landed between 2023 and 2025: the Observation framework (SE-0395), which replaced the coarse @ObservableObject invalidation with per-property tracking; Swift 6's strict concurrency, which turns the entire category of data races into compile-time diagnostics; and the arrival of capable on-device machine learning, where the Neural Engine and unified memory make it realistic to run a multi-billion-parameter model on a phone, subject to a memory budget this page works out in full.

Core theory

The view as a value-type function of state

A SwiftUI View is a Swift struct, a value type, that conforms to a protocol with a single required computed property, body. The body returns another view. The type of that returned view is not erased; it is a concrete, often enormous, generic type computed at compile time. A stack containing a text and an image has a static type on the order of VStack<TupleView<(Text, Image)>>, and the some View opaque return type exists precisely so the engineer does not have to write that type out while the compiler still knows it exactly. This matters because the framework uses the static type as the skeleton of its diffing: two renders of the same view produce values of the same type, and the framework compares them field by field.

Because a view is a value, constructing one is cheap and has no identity of its own; it is a description, not the thing described. The rendered interface, the persistent state, the animation timers, all of that lives in a parallel tree the framework maintains, sometimes called the render tree or the attribute graph. The struct you write is the recipe; the framework owns the cake. This separation is the reason a body must be a pure function. SwiftUI may call body zero times if nothing that view depends on changed, or many times in a single frame while it settles a layout, and it may call bodies in any order. A body that mutates external state, kicks off a network request, or reads the wall clock will produce a UI that flickers, loops, or races, because those side effects fire an unpredictable number of times. The rule, then, is not stylistic: a view body must be cheap and idempotent, a fast pure function from the view's inputs to a description of its output, with every effect pushed into an explicit lifecycle hook such as task, onAppear, or an action closure.

The diffing and identity algorithm

When state changes, SwiftUI must decide which views to re-render. It does this by walking the view tree and, at each node, comparing the newly produced view value against the one from the previous render. The comparison hinges on a concept the framework calls identity. Two view values are "the same view across time" if they occupy the same identity; the framework then diffs their contents to see what animated or changed. If they have different identities, the old one is torn down (its state discarded, its disappearance transition run) and the new one is created fresh (its state initialized, its appearance transition run). Getting identity right is therefore not a cosmetic concern; it determines whether state survives and whether transitions play.

SwiftUI derives identity two ways. The default is structural identity: a view's identity is its position in the view tree, encoded by the generic type structure. The first child of a VStack that is a Text has a stable structural identity as long as it stays the first child and stays a Text. This is why a ForEach over an array must be told how to identify its elements, through Identifiable or an explicit id key path: without it, the framework can only use array position, and deleting the first of ten rows would make the framework believe rows one through nine each turned into the row that used to be below them, animating a cascade of content changes instead of one deletion. The second mechanism is explicit identity, assigned with the .id(_:) modifier. Giving a view an explicit id that differs from its previous id forces a teardown and rebuild even if its structural position is unchanged, which is the supported way to reset a subtree's state, for example to clear a form when the edited record changes.

A subtle and frequently-missed consequence concerns branches. An if in a view builder does not select between two configurations of one view; it produces two structurally distinct views, one in the "then" branch and one in the "else" branch. The framework models this with _ConditionalContent, and crossing the branch is an identity change: the then-view is destroyed and the else-view created. If both branches are, say, a TextField, toggling the condition throws away the first field's editing state and focus and builds a new one. When the intent is one view whose configuration depends on a condition, the ternary belongs inside a single modifier (.foregroundStyle(flag ? .red : .primary)), which preserves identity, rather than around two whole views. This is the single most common source of "my text field keeps losing focus" bug reports, and it falls directly out of the identity model.

state change
     │
     ▼
re-invoke body of every view whose *inputs* changed  ──►  produce new view values
     │
     ▼
for each node, match new value to old by IDENTITY
     ├─ same identity  ──►  diff fields; animate/update the changed attributes only
     └─ new identity   ──►  tear down old subtree (state lost, exit transition)
                            build new subtree (state init, enter transition)

The phrase "views whose inputs changed" is doing heavy lifting, and the next section is about what counts as an input.

The state system: ownership, and the Observation framework

A view's inputs are the stored properties it reads, but not all stored properties are equal. A plain let is a constant captured at construction; changing the value passed in from the parent will, on the next parent render, produce a new view value that the diff picks up. The property wrappers exist for the cases a plain value cannot handle: mutable state that must survive re-renders, and shared state that several views read and write. Each wrapper encodes a different answer to the question of who owns this value and how long does it live.

@State declares a piece of value-type state that the view owns. The framework, not the struct, allocates the storage in the render tree, so it persists across the many short-lived reconstructions of the struct. Writing to a @State property marks the view as needing re-evaluation. Because the struct is thrown away and rebuilt constantly, @State must be initialized to a value the framework can use the first time and thereafter ignore; the property is private by convention because nobody outside should reach into another view's owned state. @Binding is a two-way reference to state owned elsewhere: a Binding<Bool> is essentially a getter/setter pair, so a child can read and mutate a parent's @State without owning it, and the parent's re-evaluation is triggered on write. The $ prefix projects a wrapped value to its binding; $isOn is the binding to the value isOn.

For reference-type models shared across a screen, the modern mechanism is the @Observable macro from the Observation framework, which in Swift 5.9 superseded the older ObservableObject protocol with its @Published properties. The difference is the whole point. Under the old protocol, a model exposed a single objectWillChange publisher; every @Published property fired it, and every view observing that object was invalidated whenever any published property changed. A list screen whose header read only the model's title was re-rendered every time an unrelated scrollOffset updated sixty times a second. The Observation framework replaces the object-level signal with property-level tracking. The @Observable macro rewrites each stored property so that reading it registers an access with the current tracking scope and writing it notifies exactly the scopes that read it. SwiftUI wraps each view's body evaluation in such a tracking scope via withObservationTracking, so a view becomes dependent on precisely the properties its body actually read on the last pass, and is invalidated only when one of those changes.

Concretely, the macro expands a class annotated @Observable into one conforming to the Observable protocol, backing each stored property with an access to an ObservationRegistrar. A read becomes registrar.access(self, keyPath: \.title) wrapped around the stored value; a write becomes a registrar.willSet/didSet pair. The registrar maintains, per property, the set of tracking scopes that touched it, and a write notifies only that set. This is why the guidance flipped: with @Observable you generally do not scatter models into many small objects to limit invalidation, because tracking is already per-property; a single well-factored model is fine, and the framework will still re-render only the views that read the field you changed. The related @Bindable wrapper produces bindings into an @Observable object's properties, the analogue of $ for these models, so a TextField can bind two-way to model.name.

@Environment is the third ownership mode: dependency injection down the tree. A value placed in the environment by an ancestor is readable by any descendant by type or by key, without being threaded through every intermediate initializer. The system populates it with things like the color scheme, the locale, and the dynamic-type size, and an app injects its own services and @Observable models the same way. Reading an environment value that changes invalidates the reader, and, as of the Observation-aware environment, reading an observable object from the environment tracks only the properties actually read, same as a directly-held model. The ownership taxonomy, then, is: @State for value state the view owns, @Binding for value state owned above, @Observable plus a holding @State for reference models the view creates and owns, @Environment for models handed down from an ancestor, and a plain let for constants. Choosing the wrong one is how state either fails to persist or fails to be shared.

Layout: parent proposes, child chooses

SwiftUI's layout is not a constraint solver like Auto Layout, where the engine finds an assignment of frames satisfying a system of inequalities. It is a single top-down/bottom-up negotiation with a simple contract: the parent proposes a size to each child, the child chooses its own size given that proposal, and the parent then positions each child using the size it chose. Nothing is solved globally; each view is asked once (in the common case) and answers for itself. The proposal is a ProposedViewSize, a width and a height that may each be a concrete number, nil (meaning "choose your ideal"), or the sentinels for zero and infinity. A child is free to ignore the proposal: a fixed-size image returns its intrinsic dimensions regardless, a Text chooses a size that fits its string within the proposed width, and a Color or Spacer greedily takes whatever is offered.

This contract composes cleanly. A VStack proposing to its children subtracts spacing, offers each child a share of the height, collects their chosen sizes, and reports its own size as the union. Because each layer only reasons locally, layout is close to linear in the number of views rather than the superlinear cost a global solver can incur, which is part of why SwiftUI can afford to run it every frame during an animation. Since iOS 16 the same contract is available to application code through the Layout protocol: a custom container implements sizeThatFits(proposal:subviews:cache:), in which it inspects each subview's size for various proposals and returns its own chosen size, and placeSubviews(in:proposal:subviews:cache:), in which it calls place(at:anchor:proposal:) on each subview. This is exactly the machinery the built-in stacks use, exposed. A radial menu, a flow layout that wraps chips, or a masonry grid are each a few dozen lines against this protocol.

parent                              child
  │  propose (w=320, h=nil) ───────►  │
  │                                   │  I need h=44 to fit my text at w=320
  │  ◄─────────── choose (320, 44)    │
  │                                   │
  │  place child at (0,0) with size (320,44)
  ▼

GeometryReader is the escape hatch for the rare case that a child genuinely needs to know the concrete size it was proposed before deciding its content, for instance to size a custom drawing to a fraction of the available width. It is a view that proposes its full received size to a closure and reports that same size upward, which makes it greedy: it takes all offered space, so wrapping a small piece of content in one often produces surprising layouts. The idiom is to reach for it only when a concrete measurement is unavoidable, and to prefer the ordinary proposal mechanism, alignment guides, or the newer containerRelativeFrame otherwise. The Grid container, added in iOS 16, is the answer to two-dimensional alignment that stacks cannot express: it lays out GridRows and aligns cells across rows into columns, measuring in two passes so that column widths are shared, which a nesting of HStacks inside a VStack cannot do because each HStack sizes independently.

Navigation: value-based routing and a type-safe path

The original NavigationView pushed destinations imperatively through NavigationLinks that each carried a hard-wired destination view, which made programmatic navigation and deep links awkward. NavigationStack, introduced in iOS 16, reframes a navigation stack as a value: the stack is driven by a path, an ordered collection of values representing the pushed screens, and the mapping from a value to the view that displays it is registered separately with navigationDestination(for:). A NavigationLink(value:) then appends a value to the path rather than naming a view; tapping it pushes the destination the framework looks up by the value's type. Because the path is an ordinary bindable collection, programmatic navigation is a mutation: pushing is path.append(item), popping to the root is path.removeAll(), and restoring a saved navigation state on launch is assigning a decoded array. A deep link becomes: parse the URL into a value, append it. The type-erased NavigationPath variant lets a single stack hold heterogeneous destination types while remaining Codable for state restoration. The design point is that navigation state stops being hidden inside a stack of view controllers and becomes plain data the app owns and can inspect, serialize, and test.

Swift concurrency: async/await, structured tasks, and actors

A user interface has a hard real-time obligation: the main thread must return to the run loop before the next frame deadline, or the frame is dropped. Any work that blocks it, a synchronous network call, a large JSON parse, an image decode, causes a visible hitch. The classic answer was to hop to a background queue with a completion handler, which fragments a linear piece of logic into a pyramid of nested callbacks and makes error handling and cancellation manual and error-prone. Swift's async/await, from Swift 5.5, restores the linear shape: an async function can await a suspension point, at which the thread is released to do other work and the function resumes later, possibly on a different thread, when its awaited result is ready. The code reads top to bottom while never blocking a thread.

Structured concurrency is the discipline that keeps this from degenerating into unmanaged background work. Every concurrent task has a parent scope and cannot outlive it. async let starts a child task whose result is awaited later in the same function; withTaskGroup spawns a dynamic set of children and joins them all before returning. The structural guarantee is that when the scope exits, whether normally or by a thrown error, all its child tasks are awaited or cancelled first, so there is no orphaned work. Cancellation is cooperative and propagates down the tree: cancelling a task marks all its descendants cancelled, and well-behaved async APIs check Task.isCancelled or call try Task.checkCancellation() and unwind. SwiftUI's .task modifier ties a task's lifetime to a view's: the task starts when the view appears and is cancelled automatically when it disappears, which is why network loads attached with .task do not leak when the user navigates away mid-flight.

The data-race problem, shared mutable state touched from two tasks without synchronization, is solved by the actor. An actor is a reference type that protects its mutable state behind an isolation boundary: only one task executes inside a given actor at a time, so its stored properties can never be accessed concurrently. Calling an actor's method from outside is an async call, because the caller may have to suspend until the actor is free. @MainActor is a special global actor whose executor is the main thread; annotating a type or method with it guarantees that code runs on the main thread, which is how SwiftUI ensures view updates never happen off-main. The full memory-model justification for why single-actor execution is sufficient to prevent races, and how it relates to acquire/release ordering and happens-before, is developed on the concurrency page; here it is enough that an actor serializes access and establishes the ordering that makes each task see the writes of the previous one.

Swift 6, released in 2024, makes this checking complete and mandatory rather than advisory. Under the Swift 6 language mode the compiler proves, at compile time, that no value crosses an isolation boundary unless it is safe to do so. The mechanism is the Sendable protocol (SE-0302): a type is Sendable if a value of it can be handed from one isolation domain to another without introducing a race, which is automatic for value types composed of sendable parts, for immutable classes, and for actors, and must be asserted with care (@unchecked Sendable) for a class that manages its own locking. A non-Sendable value, a mutable class instance, simply cannot be passed into an actor or a detached task; the compiler rejects it. The naive version of this rule is too strict, because a great deal of correct code passes a freshly-created object to another domain and never touches it again. Swift 6's region-based isolation (SE-0414) recovers those cases: the compiler tracks disjoint regions of the object graph and permits transferring a value whose region is provably not referenced by the sending domain afterward, so "make it here, send it there, forget it" type code compiles without the value being Sendable at all. The result is that the large class of bugs where two threads scribble on one object becomes a category of code that does not build.

SwiftData: the modern persistence layer

SwiftData, introduced in 2023, is a persistence framework built on the same storage engine as Core Data but with a Swift-native, macro-driven surface. A model is an ordinary class annotated @Model; the macro synthesizes the schema from the stored properties, makes the instances managed and change-tracked, and wires them into an object graph with relationships inferred from property types. There is no .xcdatamodeld visual schema file and no NSManagedObject subclassing; the Swift type is the schema. A ModelContainer owns the on-disk store and the schema, a ModelContext is the unit of work that batches inserts, updates, and deletes and flushes them on save, and the SwiftUI @Query property wrapper runs a live fetch that re-executes and re-renders when the underlying data changes. The relationship to Core Data is inheritance, not replacement at the storage layer: SwiftData sits on Core Data's persistence and can interoperate with an existing Core Data stack, so the migration story is incremental. What it buys is the elimination of the string-keyed, dynamically-typed surface (value(forKey:), fetch requests built from NSPredicate strings) in favor of key paths and the #Predicate macro that type-checks the query against the model at compile time.

Networking, decoding, and typed errors

Networking collapses to a few lines under async/await. URLSession's data(for:) is an async throws method returning the bytes and the response; there is no delegate and no completion handler in the common path. Decoding is Codable: a type that conforms to Decodable is populated from JSON by a JSONDecoder that matches keys to properties, with CodingKeys to remap names and a keyDecodingStrategy to convert snake_case to camelCase in bulk. Errors are values thrown and caught with do/catch, and the discipline that separates a robust client from a fragile one is to model the failure space explicitly, a domain enum distinguishing a transport failure (offline, timeout) from a server error (a 500 with a body) from a decoding failure (the shape changed), so the UI can react differently to each rather than showing one generic alert.

Testing and the frame budget

Swift Testing, introduced in 2024, replaces the XCTest assertion macros with a single #expect macro that captures the expression's structure, so a failed #expect(total == 42) reports the actual value of total and the operands of the comparison without the engineer writing a message. Tests are plain functions annotated @Test, grouped into @Suite types, and can be parameterized to run over a collection of inputs as separate cases. #require is the throwing form that unwraps an optional or aborts the test. The framework runs tests in parallel by default and is built on Swift concurrency, so an async test awaits directly.

Performance on iOS is governed by the frame deadline, and the arithmetic is unforgiving. A 60Hz display gives \( 1/60 \approx 16.67 \) ms per frame for the app to produce the next image; a 120Hz ProMotion display halves that to \( 1/120 \approx 8.33 \) ms. The main thread must run its layout, its view-body evaluations, and hand a committed transaction to the render server within that window, or the previous frame is shown again and the user sees a hitch: a discontinuity in motion. Instruments measures this directly through the Animation Hitches instrument, reporting a hitch time ratio, milliseconds of hitch per second of scrolling. Because ProMotion tightened the budget, work that was invisible at 60Hz, an 8 ms image decode on the main thread, now reliably drops a frame at 120Hz. The engineering response is exactly the concurrency machinery above: get everything off the main thread except the final view update, keep view bodies cheap so their re-evaluation fits in a slice of 8.3 ms, and use Instruments' Time Profiler and the SwiftUI instrument to find the body that is doing too much work or being invalidated too often.

On-device machine learning: Core ML, the Neural Engine, and the memory wall

Running a model on the device rather than a server buys latency (no round trip), privacy (data never leaves the phone), and offline capability, at the cost of a fixed and modest compute and memory budget. Core ML is Apple's inference runtime. A model trained in PyTorch or another framework is converted to the Core ML format with coremltools, which traces the computation graph and lowers it to Core ML operations, producing a .mlpackage. At load time Core ML compiles that to a device-specific representation and, guided by the computeUnits setting, distributes operations across three backends: the CPU, the GPU, and the Apple Neural Engine (ANE), a fixed-function accelerator specialized for the convolution and matrix-multiply patterns of neural networks. The ANE is fast and power-efficient for the operations it supports but is not general; Core ML will place unsupported operations on GPU or CPU, and a model that constantly bounces tensors between ANE and GPU can be slower than one that stays on GPU, so the compute-unit choice (.all, .cpuAndNeuralEngine, .cpuAndGPU, .cpuOnly) is a real tuning knob rather than "always pick all".

The dominant constraint on-device is memory, both capacity and bandwidth. A model's weights must fit in RAM alongside the operating system, the app, and everything else, and on a phone with 8 GB total shared between CPU, GPU, and ANE that is a tight ceiling. Two techniques from coremltools shrink the footprint. Quantization stores each weight in fewer bits, typically INT8 or INT4 instead of FP16, dividing the weight memory by two or four at some accuracy cost. Palettization goes further for weights that cluster: it builds a small lookup table (a palette) of, say, 16 representative values and stores each weight as a 4-bit index into it, which for a model whose weights group tightly gives 4x compression with little loss, and generalizes to per-block palettes. These are the on-device analogues of the GPTQ and AWQ post-training quantization schemes derived on the applied generative AI page; the mathematics of minimizing quantization error is the same, and only the deployment target differs.

The reason a large language model is hard to run on a phone is not arithmetic throughput but memory bandwidth. Autoregressive decoding generates one token at a time, and generating each token requires reading essentially every weight of the model once, to multiply it against the single-token activation. This makes decoding memory-bandwidth-bound: the time per token is roughly the model's byte size divided by the achievable memory bandwidth, and the ANE's TOPS rating is nearly irrelevant because the multipliers sit idle waiting for weights to arrive. With phone memory bandwidth in the tens of gigabytes per second, a multi-gigabyte model yields only tens of tokens per second, and a larger model both reads more bytes per token (slower) and may not fit at all (impossible). The problems below work this out with numbers; the conclusion, that roughly 3 to 4 billion parameters at 4-bit is the practical ceiling for a usable on-device chat model on a current phone, is a memory statement, not a compute one.

MLX, an array framework Apple released in 2023 for Apple silicon, is the newer and lower-level alternative to Core ML for exactly this workload. It is a NumPy-like API with lazy evaluation and automatic differentiation, designed around unified memory so that arrays live in one address space visible to CPU and GPU without copies, and it targets the Metal GPU directly rather than routing through Core ML's op set. For running and even fine-tuning transformer models on Apple silicon it has become the common choice in the open-source community, with a Swift binding (mlx-swift) that brings the same capability to iOS and macOS apps. The division of labor as of 2025 is roughly: Core ML for shipping a fixed converted model efficiently across CPU, GPU, and ANE inside a production app, and MLX for research, for models whose operations the ANE does not support well, and for workflows that want direct control of the GPU and unified memory.

Worked problems

Problem 1

A screen holds one @Observable model with three properties: title: String, unreadCount: Int, and scrollOffset: CGFloat. The view tree is a VStack of three subviews. HeaderView's body reads model.title. BadgeView's body reads model.unreadCount. ScrollTracker's body reads model.scrollOffset. During a scroll, scrollOffset is written 90 times in one second while title and unreadCount do not change. How many view-body evaluations does the Observation framework trigger over that second, and how many would the old ObservableObject with three @Published properties have triggered? Assume each write is a distinct frame.

Solution. Under Observation, tracking is per-property. On its last evaluation each view registered a dependency only on the property its body read: HeaderView on title, BadgeView on unreadCount, ScrollTracker on scrollOffset. A write to scrollOffset notifies only the scopes that read scrollOffset, which is ScrollTracker alone. So each of the 90 writes invalidates one view, giving \( 90 \times 1 = 90 \) body evaluations over the second. HeaderView and BadgeView are evaluated zero times, because nothing they read changed.

Under ObservableObject, every @Published write fires the single objectWillChange publisher, and every view observing the object is invalidated regardless of which property changed. All three views observe the object, so each of the 90 writes invalidates three views, giving \( 90 \times 3 = 270 \) body evaluations. Two-thirds of that work, the 180 evaluations of HeaderView and BadgeView, is wasted: it recomputes bodies whose inputs did not change and produces byte-identical view values that the diff then discards. This is the concrete meaning of "Observation re-renders only the views that read the changed property", and on a 120Hz scroll where the budget is 8.33 ms per frame, eliminating two unnecessary body evaluations per frame is often the difference between a smooth scroll and a hitchy one.

Problem 2

A phone has 8 GB of RAM, of which about 5.5 GB is realistically available to a foreground app before the system starts terminating it under memory pressure. You want to run a decoder transformer with 3.2 billion parameters, quantized to 4 bits per weight, with the following architecture: 28 layers, hidden size 3072, 24 attention heads, 8 key/value heads (grouped-query attention), head dimension 128. The KV cache is stored in FP16. (a) How much memory do the weights occupy? (b) How much does the KV cache occupy at a 4096-token context? (c) Does the model fit, and what is the largest context you could support? Show the arithmetic.

Solution. (a) Weights. At 4 bits per weight, each parameter costs \( 4/8 = 0.5 \) bytes. With \( 3.2 \times 10^9 \) parameters,

$$ M_{\text{weights}} = 3.2\times10^{9} \times 0.5\ \text{bytes} = 1.6\times10^{9}\ \text{bytes} \approx 1.49\ \text{GiB}. $$

(Using \( 1\ \text{GiB} = 2^{30} = 1.074\times10^{9} \) bytes.) Call it about 1.5 GB. In practice quantized formats carry a little overhead for per-block scales, so the on-disk and in-memory size is nearer 1.7-1.8 GB; the leading term is 1.5 GB.

(b) KV cache. The cache stores, for every layer and every past token, one key vector and one value vector over the key/value heads (grouped-query attention shares KV across query heads, which is exactly why it is used: it shrinks this cache). Per token, the number of cached scalars is

$$ 2 \times n_{\text{layers}} \times n_{kv} \times d_{\text{head}} = 2 \times 28 \times 8 \times 128 = 57{,}344\ \text{scalars}. $$

At FP16, 2 bytes each, that is \( 57{,}344 \times 2 = 114{,}688 \) bytes \( \approx 112 \) KiB per token. For a 4096-token context,

$$ M_{\text{kv}} = 114{,}688 \times 4096 = 4.70\times10^{8}\ \text{bytes} \approx 448\ \text{MiB}. $$

(c) Feasibility. Weights plus a full 4096-token cache come to about \( 1.5 + 0.45 = 1.95 \) GB, well under the 5.5 GB ceiling, so the model fits comfortably with room for the app, image buffers, and the framework. To find the largest context, give the KV cache the remaining budget after weights and a working reserve. Reserving, say, 2 GB for everything else leaves \( 5.5 - 1.8 - 2.0 = 1.7 \) GB for the cache, so

$$ N_{\max} = \frac{1.7\times10^{9}}{114{,}688} \approx 14{,}800\ \text{tokens}. $$

The binding constraint is memory capacity, and it is generous here; the model would run out of usable speed (next problem) long before it ran out of context capacity. The lesson is that at 3B-4B and 4-bit, capacity is not the wall on a modern phone; the next problem shows what is.

Problem 3

Take the same 3.2B model at 4 bits, in-memory size about 1.7 GB. The phone's memory subsystem delivers, generously, 60 GB/s of usable bandwidth. Assuming decoding is memory-bandwidth-bound and each generated token requires streaming the full set of weights once from memory, estimate the ceiling on tokens per second. Then explain why a 13B model at the same 4 bits would be more than four times slower, not merely proportionally slower in arithmetic, and why the Neural Engine's TOPS rating does not rescue it.

Solution. If every token forces one full read of the weights, the time per token is at least the bytes moved divided by the bandwidth:

$$ t_{\text{tok}} \geq \frac{M_{\text{weights}}}{B} = \frac{1.7\times10^{9}\ \text{bytes}}{60\times10^{9}\ \text{bytes/s}} \approx 0.0283\ \text{s} = 28.3\ \text{ms}. $$

That is an upper bound on throughput of \( 1 / 0.0283 \approx 35 \) tokens per second, before any overhead for the KV-cache reads (which add to the bytes moved and pull the real number lower), attention compute, sampling, and the fact that achievable bandwidth is below peak. A real measurement would land somewhere in the 15-30 tokens/second range, which is around reading speed, usable but not fast.

The 13B comparison. A 13B model at 4 bits is about \( 13/3.2 \approx 4.06 \times \) more weight bytes, so at the same bandwidth it is at least \( 4\times \) slower on the weight-streaming term alone: roughly \( 4.06 \times 28.3 \approx 115 \) ms/token, under 9 tokens/s. But it is worse than proportional for two compounding reasons. First, at \( 13\times10^{9} \times 0.5 = 6.5 \) GB the weights plus cache plus OS overrun a phone's usable RAM, so the system pages or refuses to load, and if any weights spill to slower storage the effective bandwidth for those reads collapses by an order of magnitude. Second, a larger working set evicts more of the caches between token steps, lowering the fraction of peak bandwidth actually achieved. The Neural Engine does not help because the bottleneck is not multiply-accumulate throughput: at batch size one, each weight is used for exactly one multiply after being fetched, so the arithmetic intensity is about one FLOP per byte, far below the ratio at which a 35-TOPS accelerator becomes the limit. The multipliers are starved. This is why the practical ceiling for an interactive on-device chat model is 3-4B: it is the largest that both fits in RAM and streams fast enough to feel responsive.

Problem 4

A list cell's body, during a fast scroll on a 120Hz ProMotion display, does the following on the main thread each time it is built: formats a date with a freshly-constructed DateFormatter (measured at 3.1 ms because allocating the formatter dominates), computes a layout-dependent value in 0.4 ms, and produces its view value in 0.2 ms. Twelve cells are built per frame during the scroll. (a) What is the per-frame main thread cost from these cells, and does it fit the budget? (b) If the DateFormatter is hoisted to a cached static instance so formatting drops to 0.05 ms, what is the new per-frame cost and the resulting headroom? Show the numbers.

Solution. (a) Per cell the main thread spends \( 3.1 + 0.4 + 0.2 = 3.7 \) ms. Twelve cells per frame is

$$ 12 \times 3.7\ \text{ms} = 44.4\ \text{ms}. $$

The 120Hz budget is 8.33 ms per frame. At 44.4 ms the main thread misses the deadline by more than a factor of five; the frame arrives \( 44.4 / 8.33 \approx 5.3 \) frame-times late, which the Animation Hitches instrument records as a large hitch and the user sees as a stutter. Even the 60Hz budget of 16.67 ms is blown by \( 2.7\times \). The formatter allocation alone, \( 12 \times 3.1 = 37.2 \) ms, is the whole problem.

(b) With the cached formatter, per cell cost falls to \( 0.05 + 0.4 + 0.2 = 0.65 \) ms, and per frame

$$ 12 \times 0.65\ \text{ms} = 7.8\ \text{ms}. $$

That is under the 8.33 ms ProMotion budget, leaving \( 8.33 - 7.8 = 0.53 \) ms of headroom, thin but passing at 120Hz and comfortable at 60Hz (\( 16.67 - 7.8 = 8.87 \) ms spare). The general lesson is that the expensive thing was an allocation hidden inside an innocuous formatting call, repeated per cell per frame, and that the fix is not concurrency but not doing the work: a view body must be cheap, and "cheap" against an 8.3 ms budget shared across a screenful of views means sub-millisecond. Anything heavier, a real image decode or a network parse, belongs off the main thread entirely via .task and an actor, with only the finished value handed back to the body.

Problem 5

Consider this actor and two concurrent callers. Does Swift's actor model prevent a data race on balance? Does it prevent the logical bug in which a withdrawal succeeds against a balance that was already spent, given the await between the check and the mutation? Explain what "actor reentrancy" means here and how Swift 6 does or does not flag it.

actor Account {
  var balance: Int = 100
  func withdraw(_ amount: Int) async -> Bool {
    guard balance >= amount else { return false }   // check
    await auditLog(amount)                            // suspension point
    balance -= amount                                 // mutate
    return true
  }
}
// Task A: await account.withdraw(80)
// Task B: await account.withdraw(80)

Solution. There is no data race. A data race is unsynchronized concurrent access to the same memory, and the actor forbids it: only one task runs inside Account at a time, so the reads and writes of balance are serialized and each is fully ordered with respect to the others. The compiler guarantees this, and Swift 6 would additionally reject any attempt to touch balance from outside the actor without await. So memory safety holds.

There is nonetheless a logical bug, and it comes from reentrancy. When withdraw hits the await auditLog(amount) suspension point, the actor is released: it does not stay locked across the suspension. While task A is suspended awaiting the audit log, the actor is free, so task B can enter withdraw, pass its own guard (balance is still 100, because A has not yet subtracted), and suspend at its own audit call. Now both tasks have passed the check. They resume in some order and each executes balance -= 80, driving the balance to \( 100 - 80 - 80 = -60 \): an overdraft that the guard was supposed to prevent. This is the classic reentrancy hazard, an invariant checked before an await and relied upon after it, when the state can change during the suspension.

Swift 6 does not flag this, and it is important to know why. The concurrency checker proves the absence of data races, not the preservation of application invariants; a released-across-await actor is behaving exactly as specified. The bug is a correctness error the type system does not see. The fix is to not straddle a suspension with an invariant: perform the check and the mutation with no await between them (subtract first, then log after), or re-check the balance after resuming, or restructure so the audit does not sit on the critical path. The takeaway for interviews is precise: actors give you race freedom for free, but reentrancy means they do not give you atomicity across suspension points for free, and Swift 6's compile-time guarantees stop at the former.

Problem 6

A form view contains a TextField for a note. The parent shows it inside a branch: if isEditing { EditRow() } else { EditRow() }, where both branches are the same EditRow containing the field, and isEditing toggles when the user taps a button. Users report that toggling the button clears whatever they had typed and dismisses the keyboard. Explain the cause in terms of the identity algorithm, and give the one-line fix.

Solution. An if/else in a view builder does not configure one view; it constructs two structurally distinct views wrapped in _ConditionalContent, the then-branch and the else-branch, which occupy different structural identities even though both happen to be an EditRow. When isEditing flips, the framework sees the active child change identity from "then-branch EditRow" to "else-branch EditRow". By the diffing rule, a change of identity is a teardown and rebuild: the old EditRow's subtree is destroyed, discarding the TextField's editing state and its first-responder (keyboard focus) status, and a brand-new EditRow is created with empty state. Hence the cleared text and dismissed keyboard, on every toggle.

The fix is to keep a single view with one identity and move the condition inside, so nothing crosses a branch boundary. Since both branches were identical, delete the branch entirely and render EditRow() unconditionally, pushing whatever isEditing actually controls into a modifier or a property on EditRow, for example EditRow().disabled(!isEditing). One view, one stable structural identity, no teardown; the field keeps its text and its focus across the toggle. The general rule this illustrates: use if to add or remove a view, and a ternary inside a modifier to reconfigure a view that must persist.

Implementation

The first block is a complete, idiomatic Swift 6 feature slice: an @Observable view model that loads data off the main actor, an actor that owns a cache, a SwiftData model, and the SwiftUI view that binds to all of it. It compiles under the Swift 6 language mode with strict concurrency. The comments mark the isolation of each piece, since isolation is the thing a reader most needs to see.

import SwiftUI
import SwiftData

// A SwiftData model: the @Model macro synthesizes the schema and change
// tracking. The Swift type IS the persisted schema; no .xcdatamodeld file.
@Model
final class Note {
    var title: String
    var body: String
    var updatedAt: Date

    init(title: String, body: String, updatedAt: Date = .now) {
        self.title = title
        self.body = body
        self.updatedAt = updatedAt
    }
}

// An actor: it owns a mutable cache and serializes all access, so concurrent
// callers can never race on `store`. Calls from outside are `await`ed.
actor SummaryCache {
    private var store: [PersistentIdentifier: String] = [:]

    func summary(for id: PersistentIdentifier,
                 compute: @Sendable () async -> String) async -> String {
        if let hit = store[id] { return hit }        // fast path, no await between
        let value = await compute()                  // suspension: actor is released
        store[id] = value                            // re-enters; last writer wins (fine here)
        return value
    }
}

// An @Observable view model, isolated to the main actor because it drives UI.
// Only the properties a given view's body READS become that view's dependencies,
// so writing `isLoading` invalidates only views that read `isLoading`.
@MainActor
@Observable
final class NotesViewModel {
    var notes: [Note] = []
    var isLoading = false
    var errorText: String?

    private let cache = SummaryCache()

    // async, structured: the network work suspends without blocking the main
    // thread; the final assignments run back on the main actor.
    func refresh(using service: NotesService) async {
        isLoading = true
        defer { isLoading = false }
        do {
            let fetched = try await service.fetchNotes()   // off-main, awaits I/O
            notes = fetched                                // back on main actor
            errorText = nil
        } catch {
            errorText = Self.describe(error)
        }
    }

    nonisolated static func describe(_ error: Error) -> String {
        switch error {
        case is CancellationError: return "Cancelled."
        case let urlError as URLError where urlError.code == .notConnectedToInternet:
            return "You appear to be offline."
        default: return "Something went wrong."
        }
    }
}

struct NotesScreen: View {
    // The view owns the view model; @State keeps it alive across rebuilds.
    @State private var model = NotesViewModel()
    @Environment(\.notesService) private var service   // injected dependency

    var body: some View {
        // NavigationStack: the path is a value the view owns; destinations
        // are registered by type, so navigation is type-safe data.
        NavigationStack {
            List(model.notes) { note in
                NavigationLink(value: note.persistentModelID) {
                    // This row's body reads note.title and note.updatedAt only.
                    NoteRow(title: note.title, date: note.updatedAt)
                }
            }
            .overlay { if model.isLoading { ProgressView() } }
            .navigationDestination(for: PersistentIdentifier.self) { id in
                NoteDetail(id: id)
            }
            .navigationTitle("Notes")
            // .task ties the load's lifetime to the view: auto-cancelled on disappear.
            .task { await model.refresh(using: service) }
        }
    }
}

The second block is the Core ML inference path: loading a compiled model, choosing compute units, and running a prediction, followed by the coremltools conversion and palettization step in Python that produces the model in the first place. The Swift side treats the model as an async, cancellable unit of work behind an actor so that inference never blocks the UI.

import CoreML

// Wrap the model in an actor so predictions are serialized and off-main.
actor Classifier {
    private let model: MLModel

    init() throws {
        let config = MLModelConfiguration()
        // Let Core ML place ops across CPU, GPU, and the Neural Engine.
        // For an LLM decode you might force .cpuAndGPU instead, since the ANE
        // helps little on a bandwidth-bound, batch-1 matmul and op-hopping hurts.
        config.computeUnits = .all
        // `compiledModelURL` points at a .mlmodelc produced at build or first run.
        self.model = try MLModel(contentsOf: Self.compiledModelURL, configuration: config)
    }

    func predict(features: MLFeatureProvider) async throws -> MLFeatureProvider {
        try Task.checkCancellation()                 // cooperative cancellation
        // MLModel.prediction is synchronous work; running it inside the actor
        // keeps it off the main thread and serialized against other calls.
        return try model.prediction(from: features)
    }

    static let compiledModelURL: URL = {
        Bundle.main.url(forResource: "TextClassifier", withExtension: "mlmodelc")!
    }()
}

// Calling it from a view model, isolated to the main actor:
@MainActor
@Observable
final class InferenceViewModel {
    var label: String = ""
    private let classifier = try? Classifier()

    func classify(_ input: MLFeatureProvider) async {
        guard let classifier else { return }
        do {
            let out = try await classifier.predict(features: input)  // awaits off-main
            label = out.featureValue(for: "label")?.stringValue ?? "" // resumes on main
        } catch is CancellationError {
            // view disappeared; nothing to do
        } catch {
            label = "error"
        }
    }
}
# coremltools: convert a traced PyTorch model to Core ML and palettize the
# weights to 4-bit so it fits and streams faster on device.
import torch
import coremltools as ct
from coremltools.optimize.coreml import (
    OpPalettizerConfig, OptimizationConfig, palettize_weights,
)

# 1. Trace the PyTorch model to a graph coremltools can lower.
example = torch.rand(1, 3, 224, 224)                 # (N, C, H, W)
traced = torch.jit.trace(model.eval(), example)

# 2. Convert to an ML program (.mlpackage), the modern Core ML container.
mlmodel = ct.convert(
    traced,
    inputs=[ct.TensorType(name="image", shape=example.shape)],
    convert_to="mlprogram",
    compute_precision=ct.precision.FLOAT16,          # FP16 activations
    minimum_deployment_target=ct.target.iOS18,
)

# 3. Palettize: build a 16-entry lookup table per weight tensor and store each
#    weight as a 4-bit index (log2(16) = 4 bits). ~4x smaller than FP16 weights.
config = OptimizationConfig(
    global_config=OpPalettizerConfig(mode="kmeans", nbits=4)
)
compressed = palettize_weights(mlmodel, config)

compressed.save("TextClassifier.mlpackage")
# Xcode compiles the .mlpackage to a .mlmodelc; on device Core ML JIT-compiles
# that to the specific chip's ANE/GPU/CPU representation at load time.

The last block shows Swift Testing: a parameterized async test with #expect reporting operand values on failure, and a #require that unwraps or aborts. Note the absence of a failure message; the macro captures the expression.

import Testing
@testable import NotesApp

@Suite struct ErrorDescriptionTests {
    // Parameterized: runs as three separate cases, one per input.
    @Test(arguments: [
        (URLError(.notConnectedToInternet), "You appear to be offline."),
        (CancellationError(), "Cancelled."),
        (URLError(.badServerResponse), "Something went wrong."),
    ])
    func describesError(_ error: Error, _ expected: String) {
        // On failure this reports the actual string and `expected`, no message.
        #expect(NotesViewModel.describe(error) == expected)
    }

    @Test func decodesNote() throws {
        let json = Data(#"{"title":"A","body":"B"}"#.utf8)
        // #require unwraps or aborts the test with a captured diagnostic.
        let note = try #require(try? JSONDecoder().decode(NotePayload.self, from: json))
        #expect(note.title == "A")
    }

    @Test func cacheReturnsStoredValueWithoutRecomputing() async {
        let cache = SummaryCache()
        var calls = 0
        let id = PersistentIdentifier.mock
        _ = await cache.summary(for: id) { calls += 1; return "s" }
        _ = await cache.summary(for: id) { calls += 1; return "s" }
        #expect(calls == 1)          // second call hit the cache
    }
}

How it is done in practice

A shipped iOS app is not just the code above; it is a signed, provisioned artifact that passes a human and automated review before it reaches users, and the release path has its own engineering surface. Every build is code-signed against a certificate tied to an Apple Developer account, and every install is authorized by a provisioning profile that binds the app's bundle identifier, the signing certificate, the entitlements it requests (push notifications, associated domains, App Groups, and so on), and, for development builds, the specific device UDIDs allowed to run it. Xcode's automatic signing manages most of this, but understanding the pieces matters when a build fails to install with a provisioning error, which is almost always a mismatch between the entitlements in the code and the capabilities enabled on the profile.

Distribution goes through App Store Connect. A build uploaded there is first available through TestFlight, Apple's beta channel: internal testers (members of the team) get builds immediately, while external testers (up to 10,000) receive builds only after a lighter-weight beta review. TestFlight is where crash reports and real-device feedback arrive before public release, and builds expire after 90 days, which enforces a cadence. Promotion to the public App Store requires passing App Review against the App Store Review Guidelines, an evaluation that is part automated and part human and that checks for crashes, privacy-policy compliance, correct use of entitlements, the presence of required privacy manifests declaring data collection and any use of certain system APIs, and adherence to the Human Interface Guidelines for things like navigation patterns and accessibility. Rejections are common and specific; the practical skill is reading the resolution center note, mapping it to a guideline number, and resubmitting.

On the performance side, the production discipline is measurement in Instruments against real devices, because the simulator runs on Mac silicon with desktop memory bandwidth and cannot reproduce the frame budget or the thermal and memory ceilings of a phone. The Time Profiler attributes main-thread time to symbols; the SwiftUI instrument shows which views were re-evaluated and how often, which is how an over-invalidating @Observable read or a body doing real work is caught; the Animation Hitches instrument reports the hitch time ratio directly; and Core Animation's commit timing shows whether the render server, not the app, is the bottleneck. For on-device ML, the Core ML Instruments template reports per-op timing and which compute unit each op ran on, which is the only reliable way to discover that a model is thrashing between ANE and GPU rather than staying on one.

The current research frontier

The most active area as of 2024-2025 is on-device language models, where Apple's own work and the open-source community are pushing in parallel. Apple described a family of on-device and server foundation models in 2024, using low-bit palettization and a mixed 2-to-4-bit weight scheme with per-layer bit allocation to fit a capable model in a phone's memory while holding quality, together with task-specific LoRA adapters swapped in at runtime so one base model serves many features. The community line of work centers on MLX from Apple's machine-learning research group, which has become the default framework for running and fine-tuning transformers on Apple silicon, and on llama.cpp's Metal backend, which brought aggressive GGUF quantization (2-to-8-bit, k-quants) to Mac and iOS. The competing tension is Core ML and the Neural Engine, which are the most power-efficient path but constrain the operation set, versus MLX and Metal, which are more flexible and give direct GPU control but forgo the ANE's efficiency; which wins depends on whether a model's operations map cleanly onto the ANE.

On the framework side, the trajectory since SwiftUI's introduction has been to move guarantees from runtime to compile time. The Observation framework moved invalidation precision from a runtime publisher to macro-generated per-property tracking; Swift 6's strict concurrency moved data-race detection from ThreadSanitizer, a runtime tool that only catches races it happens to observe, to the type checker, which proves their absence for all executions. Region-based isolation (SE-0414) is the notable research result there: it makes the sender/receiver transfer of non-Sendable values sound without whole-program analysis, by a local region inference that is both decidable and permissive enough for idiomatic code. The open question the community is still working through in 2025 is ergonomics: strict concurrency surfaces a large number of diagnostics when migrating existing codebases, and the evolution process has been adding affordances (sending parameters, isolated conformances, default actor isolation for a module) to reduce the annotation burden without weakening the guarantee.

Open source to read

These repositories are the primary sources for the material above. Each note says what it is good for and which file rewards a first read.

  • apple/swift: the compiler and standard library. The concurrency runtime under stdlib/public/Concurrency is where actors, tasks, and the executor model live; reading the actor and task-group sources demystifies what await compiles to.
  • apple/swift-syntax: the macro infrastructure. @Observable, @Model, and #Predicate are all macros; this is the library they are implemented against, and the macro examples show how the expansions in this page are produced.
  • ml-explore/mlx: the array framework for Apple silicon. Start with the unified-memory array and the lazy-evaluation graph; it is the clearest small codebase for understanding on-device tensor computation without Core ML's abstraction.
  • ml-explore/mlx-swift-examples: runnable Swift examples including LLM and vision-model inference. The LLM generation loop is the concrete answer to "how do I run a language model in a SwiftUI app", token streaming included.
  • apple/ml-stable-diffusion: Stable Diffusion converted to Core ML. The conversion scripts are a real, non-toy example of coremltools lowering a large model and splitting it across compute units.
  • huggingface/swift-transformers: tokenizers and model utilities in Swift. Read the tokenizer to see the unglamorous but essential preprocessing an on-device LLM needs before the first matmul.
  • pointfreeco/swift-composable-architecture: a widely-used state-management library that formalizes the reducer pattern on top of SwiftUI. Worth reading as the most rigorous community answer to structuring state, effects, and navigation as testable data.
  • apple/swift-async-algorithms: AsyncSequence operators (debounce, merge, combine). The debounce and throttle implementations are the canonical examples of structured concurrency applied to streams, directly useful for search-as-you-type UIs.

Common misconceptions

"A SwiftUI view is expensive to create, so I should avoid rebuilding it." A view is a value-type description, and constructing one is nearly free; what is expensive is the body evaluation if that body does real work, and the diff if identity is unstable. The optimization target is cheap idempotent bodies and stable identity, not fewer struct allocations.

"@Observable re-renders every view that holds the model." It re-renders only the views whose last body evaluation actually read the property that changed. A view that holds the model but never reads the changed property is not invalidated; this per-property precision is the entire reason Observation replaced ObservableObject.

"Actors make my code thread-safe, so concurrency bugs are gone." Actors eliminate data races on the actor's own state, but they release across every await, so invariants that must hold across a suspension are not protected. Reentrancy can violate a logical invariant with no data race and no compiler complaint, as Problem 5 shows.

"Swift 6 strict concurrency guarantees my program is correct." It guarantees the absence of data races, a memory-safety property. It says nothing about deadlocks (two actors awaiting each other), livelocks, ordering bugs, or the reentrancy hazard; those remain the programmer's responsibility.

"The Neural Engine's high TOPS makes on-device LLMs fast." Batch-1 autoregressive decoding is memory-bandwidth-bound, not compute-bound: each weight is fetched to be used once, so the accelerator's multipliers sit idle waiting on memory. Tokens per second track model-bytes over memory-bandwidth, and TOPS is nearly irrelevant, which is why model size, not the ANF's arithmetic rating, sets the ceiling.

"GeometryReader is the normal way to size a view." It is a greedy escape hatch that takes all offered space and reports it back, which distorts layouts when wrapped around small content. The normal mechanism is the parent-proposes/child-chooses negotiation; reach for GeometryReader only when a concrete measurement is genuinely unavoidable.

"An if in a view builder just toggles a view's visibility." It creates two structurally distinct views with different identities, and crossing the branch tears down one subtree and builds the other, discarding state and focus. To reconfigure a persistent view, put the condition inside a modifier, not around two views.

"SwiftData is just a rename of Core Data." It is a new Swift-native API (the @Model macro, type-checked #Predicate, the @Query wrapper) built on Core Data's storage engine. The persistence layer is shared and interoperable, but the programming surface, and its compile-time type safety, are genuinely different.

Self-check

References

  1. Apple, The SwiftUI framework documentation. developer.apple.com/documentation/swiftui (accessed 2025). The authoritative reference for views, state, layout, and navigation.
  2. Apple, The Observation framework documentation and "Discover Observation in SwiftUI", WWDC23. developer.apple.com/documentation/observation.
  3. Apple, "Meet SwiftUI" and "Data Essentials in SwiftUI", WWDC. The original derivation of views as functions of state and the state ownership model.
  4. Apple, The SwiftData documentation and "Meet SwiftData", WWDC23. developer.apple.com/documentation/swiftdata.
  5. Apple, The Core ML documentation, developer.apple.com/documentation/coreml, and the Human Interface Guidelines, developer.apple.com/design/human-interface-guidelines.
  6. Apple, "Meet Swift Testing" and the Swift Testing documentation, WWDC24. developer.apple.com/documentation/testing.
  7. Apple, "Explore Swift performance" / "Demystify SwiftUI performance" and the Instruments Animation Hitches documentation, WWDC. The frame-budget and hitch model.
  8. Apple Machine Learning Research, "Introducing Apple's On-Device and Server Foundation Models", 2024. machinelearning.apple.com. Low-bit palettization and runtime LoRA adapters on device.
  9. The Swift Programming Language (Swift 6), the Concurrency chapter. docs.swift.org/swift-book. Actors, async/await, Sendable, structured concurrency.
  10. Gregorczyk, Grynspan, et al. (Swift Evolution), SE-0395 "Observation", 2023. github.com/apple/swift-evolution.
  11. McCall, et al., SE-0414 "Region-based isolation", 2024. github.com/apple/swift-evolution.
  12. SE-0302 "Sendable and @Sendable closures", 2021, and SE-0306 "Actors", 2021. github.com/apple/swift-evolution.
  13. SE-0401 "Remove actor isolation inference caused by property wrappers", 2023, and SE-0304 "Structured concurrency", 2021. github.com/apple/swift-evolution.
  14. Apple, coremltools documentation, including the optimize.coreml palettization and quantization APIs. apple.github.io/coremltools.
  15. Apple Machine Learning Research, MLX documentation and the ml-explore/mlx repository, 2023-2025. ml-explore.github.io/mlx.
  16. Hudson, P., Hacking with Swift and the "100 Days of SwiftUI" reference. hackingwithswift.com. Widely-used practitioner reference for SwiftUI and Swift concurrency.
  17. Kodeco (formerly Ray Wenderlich), SwiftUI by Tutorials and Modern Concurrency in Swift. kodeco.com.
  18. Point-Free (Brandon Williams, Stephen Celis), the Composable Architecture and the accompanying essays on SwiftUI state and navigation. pointfree.co and github.com/pointfreeco/swift-composable-architecture.
  19. Apple, URLSession and Codable documentation, developer.apple.com/documentation/foundation. The async networking and decoding APIs.
  20. Apple, App Store Review Guidelines and TestFlight documentation, developer.apple.com. The provisioning, beta-distribution, and review path.
  21. Gerganov, G., et al., llama.cpp and the GGUF k-quant scheme, github.com/ggerganov/llama.cpp, 2023-2025. The community reference for aggressive on-device quantization, including the Metal backend.
SwiftUI is the claim that a UI is a pure value-type function of state, made affordable by a diff that touches only what changed; the two questions a practitioner must always be able to answer are "what is this view's identity" and "which properties did its body read", because those determine what survives and what re-renders. The Observation framework narrowed invalidation to the exact property a view reads, and Swift 6 turned data races into compile errors through Sendable and region-based isolation, but neither gives correctness for free: identity mistakes still lose state, and actor reentrancy still breaks invariants across an await with no compiler complaint. The hardware sets hard budgets that arithmetic, not intuition, must respect: 8.3 ms per frame at 120Hz means view bodies must be sub-millisecond, and the memory-bandwidth wall means a phone LLM is capped near 3-4B parameters because decoding streams every weight once per token and the Neural Engine's TOPS cannot outrun memory. Master the identity algorithm, the isolation model, and the memory arithmetic, and the rest of the platform is detail.