top of page

Swift Study Notes

What Are the Heap and the Stack?

💡 The simplest explanation

The stack stores short-lived values whose lifetimes and storage requirements can usually be managed predictably as functions are entered and exited.

The heap stores dynamically allocated values whose lifetimes may continue beyond the function that created them.

In Swift, value types and reference types give us useful clues about how values behave, but you should not assume that every struct lives on the stack or that every piece of class-related data must remain on the heap. The compiler is allowed to optimise the final representation.

Swift developers frequently hear that structs live on the stack while classes live on the heap.

That explanation is useful as a first mental model.

However, it is not a complete rule describing every value produced by an optimising compiler.

What matters most is understanding the different jobs performed by stack storage and heap storage, how value and reference semantics relate to them, and why frameworks such as SwiftUI benefit from using lightweight value descriptions alongside shared reference-based model data.

Memory Is Needed While a Program Runs

Every running program needs somewhere to store its values.

A function may create local integers, strings, structures, closures and object references.

Some values exist only while the function is running.

Other values must remain alive after that function has returned because another part of the program still uses them.

The stack and the heap support these different lifetime requirements.

What Is the Stack?

The stack is a region of memory used to manage function calls and many short-lived local values.

When a function begins, the program creates a stack frame containing information needed by that call.

The frame may contain:

  • Function parameters.
  • Local variables.
  • Temporary values.
  • Saved execution information.
  • References to values stored elsewhere.

When the function returns, its stack frame can normally be removed as one operation.

func calculateTotal() -> Int {
    let first = 10
    let second = 20

    return first + second
}

The local values belong to this function call.

Once the function has produced its result, the storage associated with that call is no longer required.

Why Stack Storage Is Efficient

The stack follows a last-in, first-out structure.

The most recently entered function is normally the first function to return.

Conceptually:

main()
    │
    └── loadProfile()
            │
            └── formatName()

formatName() finishes first.

Its stack frame is removed.

Then loadProfile() finishes and its frame is removed.

This orderly lifetime makes stack allocation inexpensive.

The program can usually reserve storage by moving the stack pointer and release it by moving the pointer back.

It does not normally need to search for a suitable free region of memory or independently track every allocation.

What Does “Known at Compile Time” Mean?

It is common to hear that stack memory is calculated at compile time.

This needs careful wording.

The compiler can often determine the size, alignment and layout required by a function’s local values before the program runs.

struct Coordinates {
    let x: Double
    let y: Double
}

func origin() {
    let point = Coordinates(x: 0, y: 0)
    print(point)
}

The compiler understands the stored properties of Coordinates and can reason about the storage needed for the local value.

The function still executes at runtime.

However, the compiler can produce instructions describing how the stack frame should be arranged before that execution begins.

Important

The stack is not a collection of values literally created by the compiler while the app is being built.

The memory is used at runtime, but much of its layout can be planned by the compiler in advance.

What Is the Heap?

The heap is a region of memory used for dynamically allocated values whose size or lifetime cannot be managed solely by entering and leaving one function.

Classes provide the clearest Swift example.

final class User {
    var name: String

    init(name: String) {
        self.name = name
    }
}

func makeUser() -> User {
    let user = User(name: "Milo")
    return user
}

The local variable named user disappears when makeUser() returns.

However, the User instance must remain alive because the function returns a reference to it.

The instance therefore cannot be tied only to the lifetime of the function’s stack frame.

Heap storage supports this independently managed lifetime.

A Reference Is Not the Object

When we work with a class instance, the local variable usually behaves like a reference to an object stored elsewhere.

let firstUser = User(name: "Milo")
let secondUser = firstUser

firstUser and secondUser refer to the same User instance.

Conceptually:

Stack                         Heap

firstUser  ───────┐
                  ├────────▶  User
secondUser ───────┘           name: "Milo"

Assigning the reference does not create a second independent User object.

Both references provide access to the same identity.

secondUser.name = "Luna"

print(firstUser.name) // Luna

Heap Allocation Requires Runtime Work

Heap allocation is more complicated than reserving space in a stack frame.

At runtime, the allocator must find or obtain a suitable region of memory.

The program must preserve the allocation while references still require it.

The allocation must eventually be reclaimed.

Accessing an object also involves following a reference to its storage.

This may introduce costs involving:

  • Dynamic allocation.
  • Deallocation.
  • Reference counting.
  • Pointer indirection.
  • Reduced cache locality.

These costs are real, but they should not be exaggerated.

Using a Class Is Not Automatically Slow

A class allocation is more involved than reserving a small local value on the stack.

That does not mean classes are too slow for ordinary application code.

Modern devices can perform enormous amounts of work every second.

A user is unlikely to notice whether one small model was implemented as a class or a struct.

Even an application containing many class-based types may still perform perfectly well.

Do not turn “heap allocation costs more” into “classes are bad.”

Reference types provide identity, shared mutable state and independently managed lifetimes.

Those are important capabilities, not design mistakes.

The difference becomes more important in performance-sensitive systems that create, copy, compare and discard very large numbers of values.

A framework repeatedly processing an entire user-interface hierarchy has different performance requirements from one function creating a single model object.

Value Types

Structures, enumerations and tuples are value types.

When a value type is assigned to another variable, the language gives the destination its own value.

struct UserSettings {
    var usesDarkMode: Bool
}

var firstSettings = UserSettings(usesDarkMode: false)
var secondSettings = firstSettings

secondSettings.usesDarkMode = true

print(firstSettings.usesDarkMode)  // false
print(secondSettings.usesDarkMode) // true

The two variables have independent values.

This is value semantics.

Value semantics make local reasoning easier because changing one value does not normally mutate another value merely because it was assigned from the first.

Reference Types

Classes are reference types.

Assigning a class reference creates another reference to the same instance.

final class UserSettings {
    var usesDarkMode: Bool

    init(usesDarkMode: Bool) {
        self.usesDarkMode = usesDarkMode
    }
}

let firstSettings = UserSettings(usesDarkMode: false)
let secondSettings = firstSettings

secondSettings.usesDarkMode = true

print(firstSettings.usesDarkMode)  // true
print(secondSettings.usesDarkMode) // true

There is one shared object.

The two constants refer to that same identity.

Value Semantics Do Not Guarantee Stack Storage

It is useful to associate small value types with stack-friendly storage.

However, value semantics and storage location are not the same concept.

A struct may contain storage that is managed indirectly.

A value may escape the function that created it.

A value may be captured by an escaping closure.

A collection may use dynamically allocated backing storage.

struct MessageList {
    var messages: [String]
}

MessageList is a value type.

However, its Array can manage dynamically allocated storage internally.

The struct’s value semantics do not require every byte belonging to its implementation to exist directly inside one stack frame.

Reference Semantics Do Not Prevent Optimisation

Similarly, class instances are normally associated with heap allocation, but the compiler can sometimes prove that an allocation never escapes a limited scope.

An optimising compiler may eliminate allocations, shorten lifetimes or transform storage when doing so preserves the program’s observable behaviour.

Therefore, the most accurate rule is:

Choose structs and classes for their semantics first.

Allow measurement and profiling to tell you whether their storage behaviour has become a real performance problem.

Compile-Time Knowledge and Runtime Knowledge

The compiler can reason especially well about values whose concrete type, size, ownership and lifetime are visible.

That information may allow it to:

  • Plan stack-frame layouts.
  • Inline functions.
  • Remove unnecessary copies.
  • Eliminate temporary values.
  • Specialise generic code.
  • Remove allocations that do not escape.

Heap-allocated objects have lifetimes determined by runtime behaviour.

The compiler may not be able to know which references will remain alive across every execution path.

Runtime management is therefore required.

This does not mean that heap-backed code has no compile-time safety.

Swift still checks types, access rules, actor isolation, exclusivity and many ownership relationships at compile time.

The limitation is more specific:

The exact lifetime and final release point of a shared reference may depend on events that occur only while the program is running.

Automatic Reference Counting

Swift uses Automatic Reference Counting, usually called ARC, to manage the lifetime of class instances.

Conceptually, ARC tracks the strong references to an object.

final class Rocket {
    let name: String

    init(name: String) {
        self.name = name
        print("\(name) created")
    }

    deinit {
        print("\(name) destroyed")
    }
}

When a strong reference begins requiring the object, ARC keeps that object alive.

When a strong reference is removed, the object may have one fewer owner.

When no strong references remain, Swift can destroy the object and reclaim its storage.

func launch() {
    let rocket = Rocket(name: "Explorer")
    print(rocket.name)
}

At the end of the function, the local strong reference disappears.

If no other strong references exist, the Rocket can be destroyed.

ARC Is Automatic but Not Free

ARC removes the need for most manual memory management.

However, maintaining ownership still requires work.

Strong references may need to be retained when ownership begins and released when ownership ends.

The compiler can remove some unnecessary reference-counting operations, but it cannot always prove that every operation is unnecessary.

Code that repeatedly moves large graphs of references through performance-critical paths may therefore spend measurable time managing ownership.

Again, this normally matters at scale rather than for a handful of model objects.

Reference Cycles

ARC can reclaim an object only when no strong references remain.

Two objects can accidentally keep each other alive.

final class Person {
    let name: String
    var apartment: Apartment?

    init(name: String) {
        self.name = name
    }
}

final class Apartment {
    let number: Int
    var tenant: Person?

    init(number: Int) {
        self.number = number
    }
}

If the Person strongly retains the Apartment and the Apartment strongly retains the Person, the cycle may prevent both reference counts from reaching zero.

Swift provides weak and unowned references to express relationships that should not keep an object alive.

final class Apartment {
    let number: Int
    weak var tenant: Person?

    init(number: Int) {
        self.number = number
    }
}

This is an ownership problem associated with shared reference identity.

Independent value types do not create the same kind of reference cycle merely by being assigned.

Comparing the Stack and the Heap

Stack Heap
Closely follows function-call lifetimes. Supports independently managed lifetimes.
Often used for local values and references. Commonly used for class instances and dynamic backing storage.
Storage can often be planned by the compiler. Allocation commonly occurs dynamically at runtime.
Allocation and release are usually inexpensive. Allocation and release require more runtime management.
Typically has strong locality of reference. Objects may be distributed throughout memory.
Frames are normally removed in order. Allocations can be released in a different order from creation.
Does not require ARC for ordinary local values. Class instances are managed through ARC.
Best suited to scoped, predictable lifetimes. Best suited to shared identity and flexible lifetimes.

Stack Overflow and Heap Exhaustion

The names also appear in two familiar failure conditions.

A stack overflow can occur when the program consumes too much stack space, often through uncontrolled recursion.

func recurseForever() {
    recurseForever()
}

Each call needs another stack frame until the available stack space is exhausted.

Heap exhaustion occurs when a program continues allocating dynamic memory and cannot obtain enough additional storage.

These are different failures caused by exhausting different memory regions.

Why Value Types Are Useful to SwiftUI

SwiftUI views are normally structures conforming to the View protocol.

struct ProfileView: View {
    let name: String

    var body: some View {
        Text(name)
    }
}

The ProfileView value is not a traditional mutable screen object comparable to a UIViewController.

It is better understood as a lightweight description of the interface that should result from the current data.

The view declaration describes rules.

struct StatusView: View {
    let isOnline: Bool

    var body: some View {
        if isOnline {
            Text("Online")
        } else {
            Text("Offline")
        }
    }
}

The declaration says:

When isOnline is true, the interface should contain “Online.”

Otherwise, the interface should contain “Offline.”

This is declarative code.

We describe the interface associated with a state instead of manually issuing every command required to mutate an existing screen into that state.

A SwiftUI View Is a Description, Not the Persistent Model

SwiftUI may create and evaluate view values frequently.

That is practical because view structures are intended to be lightweight descriptions.

The structure describes what the interface should be.

SwiftUI manages the persistent state, identity and rendered platform resources needed to present and update that interface.

A SwiftUI View value is not the rendered pixels.

It is an input into the system that determines what interface should be presented for the current state.

When data changes, SwiftUI evaluates the affected view declarations and updates the necessary parts of the interface.

It does not necessarily destroy and recreate every visible platform object or redraw the entire screen from nothing.

Why Small Value Descriptions Help at Framework Scale

Creating one structure instead of one class rarely produces a difference that a person can perceive.

SwiftUI, however, processes large trees of view descriptions.

It may construct, compare, transform and discard many values while determining the necessary interface updates.

At this scale, predictable value semantics and opportunities for compiler optimisation become valuable.

Small value descriptions can help the compiler reason about:

  • The concrete types forming a view hierarchy.
  • The values passed into each view.
  • The lifetime of temporary descriptions.
  • Which operations can be inlined or specialised.
  • Which temporary storage can be removed.

This is one reason SwiftUI’s design makes extensive use of structures, generics and opaque result types.

But the Interface Still Needs Persistent Data

A temporary view description cannot be the only home for mutable application data.

View values may be recreated.

The model data therefore needs an appropriate source of truth whose lifetime is managed independently from one temporary view value.

Sometimes the source of truth is a local value managed by SwiftUI.

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        Button("Count: \(count)") {
            count += 1
        }
    }
}

The CounterView structure describes the interface.

SwiftUI manages the persistent state associated with that view’s identity.

Recreating the description does not mean that the count must be reset each time.

Observable Reference Models

Applications also frequently need model data that is shared between several views.

A reference type is useful when multiple parts of the interface should observe and mutate one shared model instance.

import Observation
import SwiftUI

@Observable
final class ProfileModel {
    var name = "Milo"
    var isOnline = false
}

The model is a class.

It has identity.

Several views can hold references to the same instance.

struct ProfileView: View {
    let model: ProfileModel

    var body: some View {
        VStack {
            Text(model.name)

            Text(model.isOnline ? "Online" : "Offline")
        }
    }
}

The view itself is a value type.

The model is a shared reference type.

This combination is deliberate.

SwiftUI View Value Observable Model Object
Describes the interface. Stores shared application data.
Usually implemented as a struct. Often implemented as a class.
Can be created and evaluated frequently. Has an independently managed lifetime.
Uses value semantics. Uses reference semantics.
Represents how the current state should appear. Provides the state being observed.

The View Is a Representation of State

A SwiftUI view can be understood as a function of its inputs.

Current model data
        │
        ▼
SwiftUI view declaration
        │
        ▼
Description of the desired interface

When an observable property read by the view changes, SwiftUI knows that the affected view declaration may now produce a different result.

model.isOnline = true

The model object remains the shared source of truth.

SwiftUI evaluates the view’s rules again using the new data.

The resulting value describes what the relevant interface should now look like.

Why Not Make Every View a Class?

It would be possible to design a user-interface framework around reference objects.

UIKit does exactly that for many of its major components.

final class ProfileViewController: UIViewController {
    // Persistent reference identity
}

UIKit view and view-controller hierarchies are heavily object-oriented.

Those objects possess identity, mutable state and independently managed lifetimes.

This model is capable and remains entirely appropriate for many applications.

SwiftUI introduces a different authoring model.

The developer supplies value-based descriptions while the framework manages the persistent rendering machinery.

This reduces the need for each small piece of authored interface code to become another independently managed reference object.

SwiftUI and UIKit

SwiftUI interoperates closely with UIKit on Apple platforms.

Applications can embed SwiftUI inside UIKit and UIKit inside SwiftUI.

At runtime, SwiftUI ultimately relies on platform rendering and event-handling systems that include substantial reference-based infrastructure.

However, it is too simplistic to describe every SwiftUI View structure as directly becoming a corresponding UIKit object.

The SwiftUI framework owns the translation, reconciliation and rendering process.

The important architectural point is that a lightweight value-oriented API is operating alongside mature object-heavy platform systems.

Why This Matters at Scale

Imagine an application containing thousands of interface elements and a framework repeatedly evaluating which parts need updating.

Making every temporary description a separately allocated, reference-counted object could add unnecessary allocation and ownership work.

Using values does not make every operation free.

Large values can be expensive to copy, and many value types use shared backing storage internally.

However, value semantics give the compiler and framework more opportunities to optimise temporary descriptions and reason locally about their behaviour.

Swift Keeps Evolving Its Ownership Model

Swift has continued to develop features involving ownership, borrowing, consumption and noncopyable types.

These features allow programmers and the compiler to express more information about how values move through a program.

The goals include:

  • Avoiding unnecessary copies.
  • Avoiding unnecessary retains and releases.
  • Representing resources with unique ownership.
  • Moving values without implicitly duplicating them.
  • Providing stronger compile-time guarantees.
  • Making high-level Swift suitable for performance-sensitive systems.

SwiftUI is not the sole reason for these language changes.

The same capabilities matter to collections, concurrency, systems programming, graphics, servers and many other workloads.

However, SwiftUI is a highly visible example of why Swift benefits from expressive value semantics and increasingly precise ownership information.

Why SwiftUI Places Pressure on the Language

SwiftUI asks the language and compiler to process deeply nested generic view types, closures, opaque return types, state dependencies and large numbers of temporary value descriptions.

Features such as result builders, opaque result types, property wrappers, Observation and stronger ownership tools make this programming model practical and expressive.

It would be inaccurate to say that all recent Swift Evolution exists only to support SwiftUI.

It is fair to say that SwiftUI exposes important demands:

  • High-level declarative code must compile into efficient executable code.
  • Temporary values should not cause unnecessary copying.
  • Shared model objects must have clear lifetimes.
  • Data dependencies should be tracked precisely.
  • The compiler needs enough information to optimise large abstractions.

Reference Models Are Not a Failure of Value-Oriented Design

It may initially seem contradictory that SwiftUI views are value types while their models are frequently reference types.

The two sides have different jobs.

The view description benefits from value semantics because it represents a snapshot of rules derived from current inputs.

The model may benefit from reference semantics because several views need access to one shared, mutable source of truth.

Value types answer:

“What does this description contain?”

Reference types answer:

“Which shared object are we talking about?”

Do Observable Models Have to Be Classes?

Not every piece of SwiftUI state must be a class.

SwiftUI can manage value-type state, bindings and environment values.

Reference models are particularly useful when the application needs shared identity and mutation visible from several locations.

The correct choice depends on the semantics required by the data.

Copying Value Types Can Also Cost Time

Value types should not be treated as universally faster than reference types.

A very large structure may be expensive to copy.

struct ImageData {
    var pixels: [UInt8]
    var metadata: [String: String]
}

Swift collections use optimisation techniques such as copy-on-write so that assigning a value does not necessarily duplicate all of its backing storage immediately.

The logical semantics still say that each variable owns an independent value.

The implementation avoids performing an expensive physical copy until mutation requires separation.

Copy-on-Write Combines Both Worlds

Copy-on-write is an example of why “struct equals stack” is too simple.

The public type has value semantics.

Its internal implementation may share heap-allocated storage until one copy is mutated.

var first = [1, 2, 3]
var second = first

second.append(4)

print(first)  // [1, 2, 3]
print(second) // [1, 2, 3, 4]

From the programmer’s perspective, the arrays are independent values.

Internally, Swift can avoid unnecessary copying while they remain unchanged.

This demonstrates an important principle:

Semantics describe how code behaves.

Storage and optimisation describe how Swift implements that behaviour efficiently.

Choosing Between a Struct and a Class

Choose a struct when:

  • The data represents a value rather than an identity.
  • Copies should behave independently.
  • Equality should normally depend on contents.
  • Local reasoning is important.
  • Shared mutable identity is unnecessary.

Choose a class when:

  • The instance has a meaningful identity.
  • Several parts of the program must share the same mutable state.
  • The instance needs an independently managed lifetime.
  • Reference inheritance is required.
  • Deinitialisation behaviour is important.

Do not choose solely from the sentence “structs are faster.”

A semantically incorrect structure can create confusing copying behaviour.

A semantically appropriate class may be both clearer and fast enough.

Measure Before Optimising

Performance decisions should be based on evidence.

If an application is slow, use profiling tools to discover whether the cost comes from:

  • Heap allocation.
  • Reference counting.
  • Value copying.
  • Rendering.
  • Networking.
  • Disk access.
  • Image decoding.
  • Unexpected SwiftUI updates.
  • Algorithmic complexity.

Changing every class into a struct without measuring may solve nothing and can damage the design.

A More Senior Mental Model

A beginner may say:

“Structs go on the stack and classes go on the heap.”

A more experienced Swift developer says:

“Structs provide value semantics and classes provide reference identity. Small scoped values are often stack-friendly, while class instances generally require dynamically managed storage. However, the compiler and standard library can transform, share or eliminate storage when doing so preserves the required semantics.”

The second explanation is more accurate because it separates three ideas:

  • Language semantics.
  • Memory representation.
  • Compiler optimisation.

Interview Questions

What is the stack?

The stack is a region of memory commonly used for function-call frames and scoped local storage.

Its orderly lifetime allows allocation and release to be very inexpensive.

What is the heap?

The heap is a region of memory used for dynamically allocated values whose lifetimes are managed independently from one function call.

Do all structs live on the stack?

No.

Structures have value semantics, but their storage depends on context and compiler decisions.

They may contain dynamically allocated backing storage or escape the scope in which they were created.

Do class instances normally use the heap?

Yes.

Class instances normally require dynamically managed storage because references to the same instance can survive independently across the program.

The compiler may still optimise an allocation when it can prove that doing so preserves behaviour.

Why is stack allocation usually cheaper?

Stack storage follows predictable function-call lifetimes and can often be reserved or released by adjusting a stack pointer.

Why is heap allocation more expensive?

The runtime must obtain suitable storage, manage the allocation’s lifetime and eventually reclaim it.

Class instances also involve reference ownership managed by ARC.

What is ARC?

Automatic Reference Counting is Swift’s mechanism for managing the lifetime of class instances according to their strong references.

Is ARC garbage collection?

No.

ARC inserts and optimises ownership operations based on Swift’s reference-counting model rather than periodically tracing every reachable object through a separate garbage collector.

Why can reference cycles leak memory?

If objects strongly retain each other, each object may continue to have a strong reference even when the rest of the program no longer needs the cycle.

Are classes slow?

Not inherently.

They involve costs that value-oriented code may sometimes avoid, but classes remain appropriate and sufficiently fast for enormous amounts of application code.

Why are SwiftUI views structures?

SwiftUI views act as lightweight, declarative descriptions of the interface associated with the current inputs and state.

Value semantics make these descriptions easier to create, transform and reason about at framework scale.

Does SwiftUI redraw the entire screen whenever a view value changes?

Not necessarily.

SwiftUI reevaluates affected declarations and determines the updates required for the rendered interface.

Why does SwiftUI still use observable classes?

A shared reference model gives several view values access to one persistent source of truth.

The view describes how that state should appear, while the model stores the independently managed shared data.

Is SwiftUI the only reason Swift’s ownership features are evolving?

No.

Ownership features also support systems programming, concurrency, collections, resource safety and high-performance code.

SwiftUI is one important example of a workload that benefits from these improvements.

Final Revision Note

The stack and the heap solve different memory-lifetime problems.

The stack is well suited to scoped storage whose layout and lifetime can often be planned around function execution.

The heap supports values such as shared class instances that need dynamic allocation and independently managed lifetimes.

Value types and reference types relate closely to these memory models, but they should primarily be understood through their semantics.

Structures describe independent values.

Classes provide shared identity.

ARC manages the lifetime of shared class instances.

SwiftUI combines these ideas by using lightweight value-type views to describe an interface while allowing persistent value state or shared observable reference models to provide the data driving that interface.

Remember

The stack is about predictable scoped lifetime.

The heap is about dynamic independently managed lifetime.

A struct tells you that something behaves like a value.

A class tells you that something has shared reference identity.

Those semantics matter more than trying to predict the exact final address of every value in memory.

bottom of page