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
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.
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.
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.
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.
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.
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/Concurrencyis where actors, tasks, and the executor model live; reading the actor and task-group sources demystifies whatawaitcompiles to. -
apple/swift-syntax:
the macro infrastructure.
@Observable,@Model, and#Predicateare 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
coremltoolslowering 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:
AsyncSequenceoperators (debounce, merge, combine). Thedebounceandthrottleimplementations 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
- Apple, The SwiftUI framework documentation. developer.apple.com/documentation/swiftui (accessed 2025). The authoritative reference for views, state, layout, and navigation.
- Apple, The Observation framework documentation and "Discover Observation in SwiftUI", WWDC23. developer.apple.com/documentation/observation.
- Apple, "Meet SwiftUI" and "Data Essentials in SwiftUI", WWDC. The original derivation of views as functions of state and the state ownership model.
- Apple, The SwiftData documentation and "Meet SwiftData", WWDC23. developer.apple.com/documentation/swiftdata.
- Apple, The Core ML documentation, developer.apple.com/documentation/coreml, and the Human Interface Guidelines, developer.apple.com/design/human-interface-guidelines.
- Apple, "Meet Swift Testing" and the Swift Testing documentation, WWDC24. developer.apple.com/documentation/testing.
- Apple, "Explore Swift performance" / "Demystify SwiftUI performance" and the Instruments Animation Hitches documentation, WWDC. The frame-budget and hitch model.
- 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.
- The Swift Programming Language (Swift 6), the Concurrency chapter. docs.swift.org/swift-book. Actors, async/await, Sendable, structured concurrency.
- Gregorczyk, Grynspan, et al. (Swift Evolution), SE-0395 "Observation", 2023. github.com/apple/swift-evolution.
- McCall, et al., SE-0414 "Region-based isolation", 2024. github.com/apple/swift-evolution.
- SE-0302 "Sendable and @Sendable closures", 2021, and SE-0306 "Actors", 2021. github.com/apple/swift-evolution.
- SE-0401 "Remove actor isolation inference caused by property wrappers", 2023, and SE-0304 "Structured concurrency", 2021. github.com/apple/swift-evolution.
- Apple, coremltools documentation, including the optimize.coreml palettization and quantization APIs. apple.github.io/coremltools.
- Apple Machine Learning Research, MLX documentation and the ml-explore/mlx repository, 2023-2025. ml-explore.github.io/mlx.
- Hudson, P., Hacking with Swift and the "100 Days of SwiftUI" reference. hackingwithswift.com. Widely-used practitioner reference for SwiftUI and Swift concurrency.
- Kodeco (formerly Ray Wenderlich), SwiftUI by Tutorials and Modern Concurrency in Swift. kodeco.com.
- 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.
- Apple, URLSession and Codable documentation, developer.apple.com/documentation/foundation. The async networking and decoding APIs.
- Apple, App Store Review Guidelines and TestFlight documentation, developer.apple.com. The provisioning, beta-distribution, and review path.
- 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.