Getting Started Last updated: Feb. 25, 2026, 7:29 p.m.

test

Introduction to Swift

Swift is a high-performance, general-purpose compiled programming language developed by Apple Inc. for its ecosystem, including iOS, iPadOS, macOS, watchOS, and tvOS. Designed as a modern replacement for Objective-C, Swift incorporates decades of experience in language design to offer a syntax that is both concise and expressive. At its core, the language is built upon three primary pillars: Safe, Fast, and Expressive. It utilizes the LLVM compiler infrastructure to transform source code into optimized machine code, allowing it to compete with C++ in terms of execution speed while maintaining the memory safety of modern managed languages.

The language architecture relies heavily on a strong, static type system and advanced type inference, which reduces the verbosity typically associated with statically typed languages. Unlike its predecessors, Swift eliminates entire classes of common programming errors by enforcing strict initialization, checking for overflow in arithmetic operations, and defining a clear approach to memory management through Automatic Reference Counting (ARC).


Core Philosophy and Performance

Swift's design philosophy centers on the idea that the most obvious way to write code should also be the safest and most performant. By adopting a "protocol-oriented" programming paradigm, Swift encourages developers to build modular, reusable components. It bridges the gap between low-level systems programming and high-level application development by providing powerful abstractions without the overhead of a heavy runtime environment.

Language Feature Comparison

Feature Objective-C Comparison Technical Impact
Memory Management Manual or ARC Strict Automatic Reference Counting (ARC)
Type Safety Dynamic / Loose Static / Strong with Type Inference
Nullability Nil pointers allowed Explicit Optionals (Optional<T>)
Namespacing Prefixing (e.g., NS, UI) Implicit Module-based Namespacing
Syntax Verbose (Smalltalk-style) Clean, modern (Inferred types/semicolons optional)

Basic Syntax and Execution

Every Swift program begins with an entry point. In modern Swift applications, this is often handled by an attribute like @main, but for scripts and simple programs, execution starts at the top of the file. The following example demonstrates the declaration of constants, variables, and a basic function to illustrate the clarity of the syntax.

import Foundation

// Defining a constant with 'let' and a variable with 'var'
let languageName: String = "Swift"
var version: Double = 6.0

// A function demonstrating string interpolation and basic logic
func greet(language: String, ver: Double) {
    print("Welcome to \(language) version \(ver)!")
}

greet(language: languageName, ver: version)

Note

Swift is a "whitespace-sensitive" language in specific contexts, such as operators. For example, a+b is valid, and a + b is valid, but a+ b or a +b will trigger a compiler error because the compiler uses the spacing to determine if an operator is prefix, postfix, or infix.


Safety and Optionals

One of the most significant technical shifts in Swift is the introduction of Optionals. In many languages, a null pointer can lead to runtime crashes. Swift addresses this by requiring developers to explicitly declare if a value can be "nothing." An optional type is essentially a wrapper that either contains a value or contains nil. This forces the developer to handle the absence of a value at compile-time, significantly increasing the stability of the final application.

var optionalMessage: String? = "Hello, Swift"

// Forced unwrapping (dangerous)
print(optionalMessage!) 

// Optional Binding (safe and recommended)
if let constantMessage = optionalMessage {
    print("The message is: \(constantMessage)")
} else {
    print("The message was nil.")
}

Warning

Using the exclamation mark (!) to force-unwrap an optional tells the compiler you are 100% certain a value exists. If the value is nil at runtime, the application will trigger a fatal error and crash immediately. This should be avoided in production code unless the logic guarantees the presence of a value.


Integrated Tooling and Ecosystem

Swift is not just a language but a suite of tools. It includes the Swift Package Manager (SPM) for dependency management, which is integrated directly into the build system. Additionally, the language supports Swift Playgrounds, an interactive environment that allows for rapid prototyping where code results are rendered in real-time.

Tool Purpose Primary Use Case
swiftc Swift Compiler Compiling source files into executables or libraries.
SPM Package Manager Managing third-party libraries and modularizing code.
LLDB Debugger Inspecting memory and state during execution.
SwiftUI UI Framework Declarative framework for building user interfaces.

Installation (Xcode & Linux)

The installation process for Swift varies significantly depending on the target operating system. On macOS, Swift is deeply integrated into the Apple ecosystem through Xcode, which provides the compiler, debugger, and SDKs required for development. On Linux distributions (such as Ubuntu, CentOS, or Amazon Linux), Swift is installed as a standalone toolchain. Regardless of the platform, the Swift compiler (swiftc) and the Swift Package Manager (SwiftPM) remain the primary interfaces for building and managing projects.

Swift's cross-platform nature is supported by the Swift Toolchain, a collection of binaries and libraries including the Clang importer, the LLDB debugger, and the Standard Library. While Xcode manages these components automatically on macOS, Linux users must manually configure their environment variables to ensure the shell can locate the Swift executable.


Installation on macOS via Xcode

For macOS developers, the official and most stable way to install Swift is by downloading Xcode from the Mac App Store or the Apple Developer website. Xcode includes everything necessary to compile Swift code for all Apple platforms. Once installed, the Swift command-line tools are usually available globally, but they may need to be explicitly selected if multiple versions of Xcode are present on the system.

# Verify the installed Swift version on macOS
swift --version

# If the command is not found, select the active Xcode path
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

Note

Even if you do not intend to use the Xcode IDE for coding, you must install the Command Line Tools package. This provides the essential headers and system libraries required by the Swift compiler.


Installation on Linux

Installing Swift on Linux requires downloading the official tarball for your specific distribution. Unlike macOS, Linux installations require the manual installation of several dependencies, such as libicu, libcurl, and libedit, which Swift uses for Unicode support and networking.

The process generally involves three stages: installing system dependencies, extracting the Swift toolchain, and exporting the binary path to your system's PATH variable.

Requirement Description Purpose
Glibc GNU C Library Core system calls and basic functionality.
Binutils Binary Utilities Tools for creating and managing binary files.
LibICU International Components for Unicode Advanced Unicode and localization support.
Path Configuration ~/.bashrc or ~/.zshrc Making swift executable from any directory.

Step-by-Step Linux Setup (Ubuntu Example)

To install Swift on an Ubuntu system, you first update the package manager and install the necessary libraries. After downloading the toolchain from Swift.org, move it to a permanent location like /usr/share/swift.

# 1. Install dependencies
sudo apt-get install \
          binutils \
          git \
          unzip \
          libcurl4-openssl-dev \
          libedit-dev \
          libpython3-dev \
          libsqlite3-dev \
          libxml2-dev \
          libz3-dev \
          pkg-config \
          tzdata \
          zlib1g-dev

# 2. Extract the downloaded archive
tar xzf swift-6.0-RELEASE-ubuntu22.04.tar.gz

# 3. Add Swift to your PATH (Adjust path to your actual location)
export PATH=/path/to/swift-6.0-RELEASE-ubuntu22.04/usr/bin:"${PATH}"

Verifying the Installation

Once the installation is complete, you can verify the integrity of the toolchain by entering the Swift REPL (Read-Eval-Print Loop). The REPL allows you to write and execute Swift code line-by-line without creating a full project file. This is an excellent way to test if the compiler and standard library are correctly linked.

// Type 'swift' in your terminal to enter the REPL
$ swift
Welcome to Swift version 6.0.
  1> let setupStatus = "Success"
  2> print("Installation: \(setupStatus)")
Installation: Success
  3> :quit

Warning

On Linux, if you encounter an error stating error: termination sigill, it usually indicates a mismatch between the Swift binary version and the underlying Linux distribution version. Always ensure the download matches your OS version (e.g., Ubuntu 22.04 vs. 24.04).


Comparison of Environments

While the core language remains the same, the developer experience differs between platforms based on the available tooling.

Feature Xcode (macOS) Linux Toolchain
IDE Support Full-featured (Xcode) VS Code (via Swift Extension)
Debuggers Graphical LLDB Command-line LLDB
Package Manager Integrated GUI Command-line swift package
Platform APIs UIKit, AppKit, SwiftUI Swift Foundation (Corelibs)

The REPL & Playgrounds

Swift provides two primary environments for rapid prototyping and interactive coding without the overhead of creating a full application project: the REPL (Read-Eval-Print Loop) and Swift Playgrounds. These environments are designed to tighten the feedback loop, allowing developers to test logic, explore APIs, and visualize data transformations in real-time. While both serve the purpose of interactive execution, they cater to different workflows—the REPL is a terminal-based tool favored by systems programmers, while Playgrounds offer a rich, graphical interface ideal for UI prototyping and educational exploration.


The Swift REPL

The REPL is a command-line interface that allows you to interact with the Swift compiler dynamically. When you type a line of code into the REPL, the compiler immediately parses and executes it, printing the result of the expression. This is particularly useful for testing small snippets of code, checking the behavior of standard library functions, or debugging complex algorithms without needing to recompile an entire module.

To launch the REPL, simply type swift into your terminal. Once inside, every variable you declare remains in the session's memory, allowing for stateful, multi-line logic.

$ swift
Welcome to Apple Swift version 6.0.
  1> let numbers = [10, 20, 30, 40]
numbers: [Int] = 4 items {
  [0] = 10
  [1] = 20
  [2] = 30
  [3] = 40
}
  2> let sum = numbers.reduce(0, +)
sum: Int = 100
  3> :quit

Note

On macOS, the REPL requires the Xcode Command Line Tools to be active. If you receive an error about "xcrun," ensure you have run sudo xcode-select -s /Applications/Xcode.app to point the system to the correct developer directory.


Swift Playgrounds

Swift Playgrounds (available as both a feature within Xcode and a standalone app for iPad and macOS) represent a "literate programming" environment. Unlike the REPL, Playgrounds support rich-text documentation, embedded resources, and a "Live View" for rendering SwiftUI views or SpriteKit scenes. The primary technical advantage of a Playground is its ability to show the execution history of a variable in the "Sidebar" or "Timeline," which tracks how a value changes over multiple iterations of a loop.

Feature Swift REPL Swift Playgrounds
Interface Terminal / Command Line Graphical IDE
Persistence Session-based (Lost on exit) File-based (.swiftpm or .playground)
Platform macOS, Linux, Windows macOS, iPadOS
Visualization Text output only Graphs, Images, UI Previews
Use Case Quick logic checks / Scripting UI Design / Learning / Documentation

Working with Resources and Modules

Both environments allow for the importation of frameworks. In the REPL, you can import system modules like Foundation or Darwin (on macOS) and Glibc (on Linux). In Playgrounds, you can go a step further by adding custom Swift files to the Sources folder or assets to the Resources folder, which the Playground will pre-compile to maintain high performance.

The following code demonstrates a common Playground pattern: using import PlaygroundSupport to allow an asynchronous or infinite execution loop, which is necessary for testing network requests or persistent animations.

 import Foundation
import PlaygroundSupport

// Tells the playground to keep executing even after the last line is reached
PlaygroundPage.current.needsIndefiniteExecution = true

let url = URL(string: "https://api.github.com")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let data = data {
        print("Received \(data.count) bytes")
    }
    // Stop execution once the task is complete
    PlaygroundPage.current.finishExecution()
}

task.resume()

Technical Constraints and Limitations

While powerful, these interactive environments have architectural differences compared to a compiled application. For instance, code in a Playground is executed within a "host" process, which can sometimes lead to different performance profiles or permissions (like sandbox restrictions) than a standalone binary.

Constraint REPL Playgrounds
Compilation Mode JIT (Just-In-Time) JIT with incremental updates
Access Control Operates in a single global scope Requires public access for code in Sources
Optimizations Usually -Onone (Debug) Usually -Onone (Debug)
Third-Party Code Limited to pre-installed libs Supports Swift Packages (in .swiftpm)

Warning

Avoid putting heavy computational loops (like infinite while loops without a break) in a Playground. Because Playgrounds attempt to capture the state of every execution, an accidental infinite loop can cause the com.apple.dt.Xcode.Playground process to consume all available system memory, leading to an IDE crash.

A Swift Tour (Hello World)

The "Hello World" program in Swift serves as a demonstration of the language’s streamlined syntax and modern design. Unlike many C-based languages, Swift does not require a boilerplate main function or a class wrapper to execute top-level code. In a single-file Swift program or a script, the first line of executable code acts as the entry point for the entire application. This section explores the fundamental structure of a Swift program and the various methods for executing it across different environments.


The Minimal Program

A complete, functional Swift program can consist of a single line of code. Swift uses the print(_:separator:terminator:) function from the Standard Library to output text to the console. By default, this function appends a line break at the end of the output.

print("Hello, world!")

While simple, this line invokes the Swift runtime to handle string encoding and standard output streams. In more complex scenarios, you may wish to suppress the newline character or add custom separators when printing multiple items.

// Example of a customized print statement
print("Hello", "Swift", separator: " ", terminator: "!\n")

Execution Methods

Depending on your development environment, there are three primary ways to run a "Hello World" program. Each method targets a different stage of the development lifecycle, from rapid prototyping to building redistributable binaries.

Method Command / Tool Technical Context
Swift Interpreter swift hello.swift Executes the script immediately without creating a binary file.
Swift Compiler swiftc hello.swift Compiles source code into a standalone executable binary.
SwiftPM swift run Manages dependencies and builds a structured project.

Using the Compiler (System Programming)

To create a high-performance executable, use the Swift compiler (swiftc). This generates a machine-code binary optimized for the host architecture.

# 1. Create the file
echo 'print("Hello, Compiled Swift!")' > hello.swift

# 2. Compile into an executable named 'hello'
swiftc hello.swift -o hello

# 3. Run the binary
./hello

Understanding the Entry Point

In a multi-file application or a Swift Package, the entry point is handled more formally. For command-line tools created via the Swift Package Manager (SwiftPM), the code resides in a file named main.swift. The compiler treats main.swift specially, allowing it to contain top-level expressions that are executed in order.

In modern application frameworks like SwiftUI or AppKit, the entry point is often obscured by the @main attribute, which designates a specific structure as the starting point of the program's lifecycle.

File Type Entry Point Logic
Single Script Top-to-bottom execution of the .swift file.
SwiftPM Executable Requires a main.swift file or an @main struct.
Library No entry point; contains definitions for other modules to use.
// Inside a typical SwiftPM 'main.swift'
import Foundation

let greeting = "Hello, World!"
let timestamp = Date()

print("\(greeting) Current time is: \(timestamp)")

Warning

You cannot have top-level code (code outside of a function or class) in any file other than main.swift or a script file. Attempting to do so in a standard library file will result in a compiler error: "Expressions are not allowed at the top level."


Best Practices for Initial Projects

When starting a new Swift project, it is highly recommended to use the Swift Package Manager rather than manual compiler commands. This ensures that your project structure remains consistent and compatible with version control systems like Git.

# Initialize a new executable project
swift package init --type executable

# Build and run the project in one command
swift run

Note

Swift is case-sensitive. While print is the standard function, Print would result in a "Use of unresolved identifier" error. Similarly, all keywords like import, let, and var must remain lowercase.

Package Manager (SwiftPM)

The Swift Package Manager (SwiftPM or SPM) is the official tool for managing the distribution of Swift code. It is integrated directly into the Swift build system and the Xcode IDE, providing a unified way to automate the process of downloading, compiling, and linking dependencies. Unlike third-party managers used in the past, SPM is cross-platform, working identically on macOS, Linux, and Windows. It uses a declarative configuration file written in Swift itself, allowing the build logic to be type-safe and easily maintainable.

The architecture of a Swift Package is defined by the Package.swift manifest file. This file describes the package's name, its products (libraries or executables), its targets (the actual source code modules), and its external dependencies.


Anatomy of a Swift Package

A standard Swift package follows a strict directory hierarchy. This structure allows the compiler to map source files to specific targets automatically. The Sources directory contains the logic for your modules, while the Tests directory houses the unit tests.

Component Description Requirement
Package.swift The manifest file defining the package configuration. Mandatory
Sources/ Contains subdirectories for each target's source code. Mandatory
Tests/ Contains unit tests, typically using the SwiftTesting or XCTest framework. Recommended
.swiftpm/ Hidden directory containing local configuration and state. Auto-generated

Creating and Building a Package

To initialize a new project, you use the swift package init command. You must specify the type of project: executable for a program that runs, or library for code intended to be used by other projects. Once initialized, the swift build command compiles the project, resolving any dependencies defined in the manifest.

# Initialize a new executable project
mkdir MyCLIProject
cd MyCLIProject
swift package init --type executable

# Build the project
swift build

# Run the executable directly
swift run

When you run swift build, SPM creates a .build directory. Inside, it stores intermediate build artifacts and the final binary. If you are working on a library, SPM ensures that the public interface is correctly exposed to importing modules.


Managing Dependencies

One of the most powerful features of SPM is its ability to fetch and update external libraries. Dependencies are added to the dependencies array in the Package.swift file. You must specify the URL of the Git repository and the version requirement (using Semantic Versioning).

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "MyTool",
    dependencies: [
        // Adding a third-party dependency via GitHub
        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
    ],
    targets: [
        .executableTarget(
            name: "MyTool",
            dependencies: [
                // Linking the dependency to a specific target
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
            ]
        ),
    ]
)

Warning

When adding dependencies, SPM creates a Package.resolved file. This file "pins" the exact versions of all dependencies (including sub-dependencies) to ensure that every developer on a team builds with the identical code. You should always commit this file to your version control system.


SPM Commands Reference

The swift package command suite offers various tools for auditing and maintaining your project's health.

Command Purpose Technical Effect
swift package update Updates dependencies Fetches the latest versions allowed by Package.swift.
swift package resolve Resolves dependencies Downloads missing packages without building.
swift package clean Cleans build folder Deletes the .build directory to force a fresh compile.
swift package describe Shows metadata Prints a JSON or text summary of the package structure.
swift test Runs tests Compiles and executes all targets in the Tests/ folder.

Note

If you are using Xcode, you do not need to use the command line to add packages. You can go to File > Add Package Dependencies... and enter a URL. Xcode will automatically update your project settings and handle the integration behind the scenes.

The Basics Last updated: Feb. 25, 2026, 7:30 p.m.

test

Constants and Variables

In Swift, every piece of data is stored as either a constant or a variable. This distinction is fundamental to Swift’s emphasis on safety and performance. By explicitly defining which values can change and which are immutable, the compiler can optimize memory usage and prevent accidental side effects in complex codebases.

Constants and variables associate a name (such as maximumLoginAttempts or welcomeMessage) with a value of a particular type (such as the number 10 or the string "Hello").


Declaration and Immutability

You declare constants with the let keyword and variables with the var keyword. The technical difference lies in the underlying memory protection: once a constant is assigned a value, its value cannot be changed during the program's execution. Attempting to do so results in a compile-time error. Variables, conversely, can be updated as many times as necessary.


// Declaring a constant - its value is set and locked
let maximumNumberOfLoginAttempts = 10

// Declaring a variable - its value can be updated
var currentLoginAttempt = 0

// This is allowed:
currentLoginAttempt = 1
currentLoginAttempt += 1

// This would trigger a COMPILE-TIME ERROR:
// maximumNumberOfLoginAttempts = 12
                        

Warning

Swift follows a "Constants First" philosophy. You should always use let by default. Only use var if the value must actually change. This practice makes your code's intent clearer and allows the compiler to perform more aggressive optimizations.


Type Safety and Type Inference

Swift is a statically typed language, meaning the type of a constant or variable is determined at compile time. However, Swift uses Type Inference, allowing you to omit the explicit type if you provide an initial value. The compiler examines the expression to determine the most appropriate type.

If you need to declare a variable without providing an initial value, or if you want to be explicit about the type, you use a Type Annotation.

Method Syntax Example When to Use
Type Inference let version = 6.0 When the initial value clearly defines the type (Double).
Type Annotation var score: Int When declaring a variable before assigning it a value.
Explicit Type let price: Float = 10 When you want a specific type other than the inferred default (e.g., Float vs Double).

// Inferred as String
let websiteName = "Technical Docs"

// Explicitly annotated as Double
var temperature: Double = 25.0

// Multiple declarations on a single line (with type annotation)
var x = 0.0, y = 0.0, z = 0.0
                        

Naming Conventions and Scope

Constant and variable names can contain almost any character, including Unicode characters. However, they cannot contain whitespace characters, mathematical symbols, or private-use Unicode scalar values. Names cannot begin with a number.

Swift utilizes Block Scoping. A constant or variable is only accessible within the braces {} where it was defined. Defining a constant with the same name as one in an outer scope is known as "Shadowing."


let ? = 3.14159
let ???? = "Lift off"

func processData() {
    let localValue = 100
    print(localValue) // Accessible here
}

// print(localValue) // ERROR: localValue is out of scope here
                        

Note

Swift uses camelCase for constant and variable names (e.g., userProfileImage). Names should be descriptive enough to communicate their purpose without needing comments.


Configuration Options and Metadata

When working with system configurations or API parameters, constants are often grouped together. The following table illustrates how different modifiers affect the behavior of constants and variables within a program.

Modifier Keyword Effect
Global let / var Defined outside of any function; lazily initialized on first access.
Local let / var Defined within a function; initialized immediately when reached.
Static static let Belongs to the type itself rather than instances; used for shared configuration.
Computed var name: Type { ... } Does not store a value; calculates it every time it is accessed.

struct APIConfig {
    // Static constant for shared use across the app
    static let baseURL = "https://api.example.com/v1"
    
    // Computed variable
    var fullPath: String {
        return APIConfig.baseURL + "/users"
    }
}
                        

Basic Data Types (Int, Float, Double, Bool)

Swift provides a robust set of fundamental data types that serve as the building blocks for any application. As a type-safe language, Swift requires every constant and variable to have a defined type. These types are implemented as structures in the Swift Standard Library, meaning they come with built-in methods and properties while maintaining the performance of primitive types in other languages.


Integers (Int)

Integers are whole numbers with no fractional component. Swift provides signed integers (which can be positive, zero, or negative) and unsigned integers (which can be positive or zero). In most cases, you should use the generic Int type. On 32-bit platforms, Int is the same size as Int32, and on 64-bit platforms, it is the same size as Int64.

Swift also provides specific sizes for integers when memory constraints or external data formats require precision.

Type Size (Bits) Range (Approximate)
Int8 8 -128 to 127
UInt8 8 0 to 255
Int32 32 -2,147,483,648 to 2,147,483,647
Int64 64 -9 quintillion to 9 quintillion
UInt Platform-dependent 0 to 264 - 1 (on 64-bit)
let age: Int = 30
let binaryData: UInt8 = 255 // Maximum value for 8-bit unsigned integer

// Accessing min and max properties
let minInt = Int.min
let maxInt = Int.max

Warning

Swift does not allow implicit conversion between different integer types. For example, you cannot add an Int8 and an Int32 directly. You must explicitly convert one type to the other, such as Int32(someInt8) + someInt32.


Floating-Point Numbers (Double and Float)

Floating-point numbers are numbers with a fractional component, such as 3.14159 or -273.15. Swift provides two primary signed floating-point types:

  • Double Represents a 64-bit floating-point number. It has a precision of at least 15 decimal digits and is the default type for inferred floating-point values.
  • Float: Represents a 32-bit floating-point number. It has a precision of as little as 6 decimal digits.
// Inferred as Double by default
let pi = 3.14159 

// Explicitly declared as Float
let distance: Float = 1.5

// Scientific notation
let exponentValue = 1.25e2 // 125.0
let hexExponent = 0xFp2    // 15 * 2^2 = 60.0

Numeric Literals and Readability

Swift provides highly readable numeric literals. You can use underscores to visually separate large numbers, making them easier to read. These underscores are ignored by the compiler during execution.

let oneMillion = 1_000_000
let atmosphericPressure = 1_013.25
let paddedDouble = 000123.456 // Leading zeros are ignored

Booleans (Bool)

The Bool type in Swift represents a logical value that can only be either true or false. Swift’s type safety prevents non-boolean types (like integers) from being treated as booleans. In languages like C, 0 is often false and 1 is true; in Swift, this will result in a compile-time error if used in a conditional statement.

Scenario Logic Result
Equality 5 == 5 true
Inequality 5 != 5 false
Logical NOT !true false

let isServerRunning = true
let accessDenied = false

if isServerRunning {
    print("System is operational.")
}

// This would cause a COMPILE ERROR:
// let i = 1
// if i { ... }

Note

Booleans are most frequently used in control flow statements like if, while, and guard. Because of Swift's strict type checking, the condition provided to these statements must evaluate specifically to a Bool type.


Type Aliases

Type aliases allow you to provide an alternative name for an existing type. This is technically useful when you want to refer to an existing type by a name that is more contextually appropriate for your specific domain (e.g., when working with specific data sizes from an external source).

typealias AudioSample = UInt16

var maxAmplitudeFound = AudioSample.min // Effectively UInt16.min (0)

Tuples

Tuples group multiple values into a single compound value. Unlike arrays or dictionaries, the values within a tuple can be of any type and do not have to be of the same type as each other. Tuples are particularly useful for returning multiple values from a function call in a single, organized package.

While highly flexible, tuples are intended for temporary groups of related values. They are not suited for the creation of complex data structures. If your data structure is likely to persist beyond a local scope, or if it represents a core entity in your application, you should define a struct or class instead.


Defining and Accessing Tuples

A tuple is created by enclosing a comma-separated list of values in parentheses. You can access the individual element values in a tuple using index numbers starting at zero, or by decomposing the tuple into separate constants or variables.


// A tuple containing an HTTP status code and a description
let http404Error = (404, "Not Found")

// Accessing via index
print("The status code is \(http404Error.0)")
print("The status message is \(http404Error.1)")

// Decomposition into separate constants
let (statusCode, statusMessage) = http404Error
print("Status: \(statusCode)")
                        

If you only need some of the tuple’s values, you can use an underscore (_) when decomposing the tuple to ignore parts of the group.


let (justTheCode, _) = http404Error
print("Code: \(justTheCode)")
                        

Named Elements

To improve code readability, you can name the individual elements in a tuple when it is defined. If you name the elements, you can use those names to access the values. This provides a clear, self-documenting interface for the data without the overhead of a custom type definition.

Access Method Example Technical Benefit
Index-based point.0 Minimal syntax, useful for very short-lived data.
Decomposition let (x, y) = point Assigns values to local variables for immediate use.
Named Elements point.x High readability; clarifies the meaning of each value.

let http200Status = (statusCode: 200, description: "OK")

print("The status code is \(http200Status.statusCode)")
print("The message is \(http200Status.description)")
                        

Tuples as Return Values

Tuples are most effectively used as the return type for functions. A function that returns a tuple can provide multiple pieces of information to its caller without requiring the definition of a wrapper object or the use of "in-out" parameters.


func calculateMinMax(array: [Int]) -> (min: Int, max: Int) {
    var currentMin = array[0]
    var currentMax = array[0]
    
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    
    return (currentMin, currentMax)
}

let bounds = calculateMinMax(array: [8, -6, 2, 109, 3, 71])
print("Min is \(bounds.min) and max is \(bounds.max)")
                        

Note

Tuples are strictly "value types." When you pass a tuple to a function or assign it to a new variable, a copy of the entire tuple is created. Modifications to the new copy do not affect the original tuple.


Tuple Comparison

Swift allows you to compare two tuples if they have the same type and the same number of values, provided the individual values within the tuples can be compared. For example, (Int, String) can be compared to another (Int, String) because both Int and String support comparison. Tuples are compared from left to right, one value at a time, until the comparison finds two values that aren't equal.


(1, "zebra") < (2, "apple")   // true because 1 is less than 2
(3, "apple") < (3, "bird")    // true because 3 is equal to 3, and "apple" is less than "bird"
(4, "dog") == (4, "dog")      // true because both elements are equal
                        

Warning

Swift can only compare tuples with fewer than 7 elements. If you attempt to use comparison operators on a tuple with 7 or more elements, the compiler will throw an error. For data structures of that size, you must implement a custom comparison logic within a struct.

Optionals & Nil

Swift’s most distinctive safety feature is the concept of Optionals. In many programming languages, a reference to an object can be "null," often leading to unexpected runtime crashes (the "null pointer exception") when code attempts to access a value that does not exist. Swift eliminates this entire class of errors by requiring you to explicitly state whether a variable can ever hold a "no-value" state.

An optional type is essentially a wrapper—an enumeration with two cases: none (representing nil) and some(Wrapped), which contains the actual value. If you do not define a type as optional, the compiler guarantees that it will always hold a valid value of its declared type.


Declaring Optionals

You define an optional by appending a question mark (?) to the type name. This indicates that the constant or variable can either contain a value of that type or contain nil to represent the absence of a value.


var surveyAnswer: String? = "Yes" // Contains a string
surveyAnswer = nil               // Now contains no value

// Non-optional types cannot be nil
var name: String = "John"
// name = nil // COMPILE-TIME ERROR
                        

Unwrapping Optionals

Because an optional is a wrapper, you cannot use its value directly. You must "unwrap" the optional to access the underlying data. Swift provides several mechanisms for safely and unsafely unwrapping these values.

1. Optional Binding

Optional binding is the preferred method for accessing an optional's value. Using if let or guard let allows you to check if the optional contains a value and, if so, assign that value to a temporary constant in a single step.


let possibleNumber = "123"
let convertedNumber = Int(possibleNumber) // ConvertedNumber is of type Int?

if let actualNumber = convertedNumber {
    print("The string \(possibleNumber) has an integer value of \(actualNumber)")
} else {
    print("The string \(possibleNumber) could not be converted to an integer")
}
                        

2. Nil-Coalescing Operator

The nil-coalescing operator (??) allows you to unwrap an optional and provide a default value to use if the optional is nil. This is highly efficient for setting fallback values.


let defaultColorName = "red"
var userDefinedColorName: String? // defaults to nil

var colorNameToUse = userDefinedColorName ?? defaultColorName
// colorNameToUse is "red" because the user choice was nil
                        

3. Forced Unwrapping

If you are absolutely certain an optional contains a value, you can access it by appending an exclamation mark (!) to the name. This is known as forced unwrapping.


let manualOptional: Int? = 5
print(manualOptional!) // Outputs 5
                        

Warning

Forced unwrapping is dangerous. If you attempt to use the ! operator on an optional that is currently nil, your program will trigger a runtime fatal error and crash immediately. Never use forced unwrapping unless the logic of your code prevents the value from being nil.


Implicitly Unwrapped Optionals

Sometimes it is clear from a program’s structure that an optional will always have a value after that value is first set. In these cases, it is useful to remove the need for manual unwrapping every time the value is accessed. These are defined with an exclamation mark (!) after the type.

Type Syntax Usage Context
Optional String? Standard use; safe; requires explicit unwrapping.
Implicitly Unwrapped String! Class initialization; IBOutlets; assumes value is always present.
 
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // No need for an exclamation mark here
                        

Note

Even implicitly unwrapped optionals can be checked with optional binding (if let) to verify if a value exists before access, providing an extra layer of safety when their state is uncertain.


The nil Keyword

In Swift, nil is not a pointer—it is the absence of a value of a certain type. You can only use nil with optionals. If you define an optional variable without providing a default value, the variable is automatically set to nil for you.


var responseCode: Int? // Automatically set to nil
responseCode = 404     // Now contains a value
                        

Comparison of Unwrapping Techniques

Technique Syntax Safety Level Best For
Optional Binding if let / guard let High Safe conditional logic.
Nil-Coalescing value ?? default High Providing fallback/default values.
Optional Chaining value?.property High Accessing properties of an optional.
Forced Unwrapping value! Low Quick debugging or guaranteed values.

Error Handling Basics (try, catch)

In Swift, error handling is the process of responding to and recovering from error conditions during the execution of your program. Unlike some languages where error handling is performed via unchecked exceptions that can crash an app without warning, Swift requires you to explicitly mark code that can throw an error and handle that error using the do-catch pattern. This ensures that you have accounted for potential failures—such as a missing file or a failed network request—at compile time.

Swift identifies errors as values of types that conform to the Error protocol. Because Error is an empty protocol, you can use any type to represent an error, though enum is the standard choice for grouping related error conditions.


Representing and Throwing Errors

The first step in error handling is defining the possible error states using an enumeration. When a function encounters a condition it cannot handle, it "throws" an error using the throw keyword. Any function that can throw an error must be marked with the throws keyword in its signature.


// 1. Define the error type
enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}

// 2. Mark the function with 'throws'
func vend(item: String, balance: Int) throws {
    guard item == "Candy" else {
        throw VendingMachineError.invalidSelection
    }
    
    guard balance >= 5 else {
        throw VendingMachineError.insufficientFunds(coinsNeeded: 5 - balance)
    }
    
    print("Vending \(item)...")
}
                        

Handling Errors with Do-Catch

To handle a throwing function, you wrap the call in a do block and use the try keyword before the function call. If an error is thrown, execution immediately jumps to the catch blocks. You can provide multiple catch blocks to handle specific error cases or use a generic catch to handle any error that wasn't specifically caught.


do {
    try vend(item: "Candy", balance: 2)
} catch VendingMachineError.invalidSelection {
    print("That item is not available.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Please insert an additional \(coinsNeeded) coins.")
} catch {
    print("An unexpected error occurred: \(error).")
}
                        

Note

Inside a generic catch block, Swift automatically provides a local constant named error that contains the error object that was thrown.


Alternative Handling: try? and try!

Sometimes you don't need to know the specific details of the error; you only care whether the operation succeeded or failed. Swift provides two shorthands for these scenarios:

Keyword Technical Behavior Resulting Type
try Requires a do-catch block; stops execution on error. The return type of the function.
try? Converts an error into nil. If it succeeds, the result is an Optional. Optional<T>
try! Disables error propagation; crashes the app if an error occurs. The return type (unwrapped).

// Using try? - returns nil if an error is thrown
let result = try? vend(item: "Soda", balance: 10)

// Using try! - will CRASH if the function throws
// let forcedResult = try! vend(item: "Soda", balance: 10)
                        

Warning

Use try! only when you are mathematically or logically certain that the function cannot fail in the current context. Just like forced unwrapping of optionals, try! will cause a runtime crash if it encounters an error.


Specifying Cleanup with defer

Regardless of whether an error is thrown or the function completes successfully, you often need to perform cleanup (e.g., closing a file or releasing a lock). The defer statement is used to execute a block of code just before the current scope is exited.


func processFile(filename: String) throws {
    print("Opening \(filename)...")
    
    // This code executes when the function returns, even if an error is thrown
    defer {
        print("Closing \(filename).")
    }
    
    if filename == "corrupted.txt" {
        throw VendingMachineError.outOfStock // Dummy error for example
    }
    
    print("Processing file content...")
}
                        

Typed Throws (Swift 6.0+)

In Swift 6.0 and later, you can specify exactly which error type a function throws. This provides better type safety and allows the caller to know the exact error type without casting.

Syntax Description Use Case
throws Can throw any error conforming to the Error protocol. General purpose; most common.
throws(MyError) Can only throw errors of type MyError. High-precision APIs; avoids generic catch blocks.

// A function that only throws VendingMachineError
func strictVend() throws(VendingMachineError) {
    throw .outOfStock
}
                        

Assertions and Preconditions

Assertions and preconditions are sanity checks that verify essential assumptions at runtime. Unlike the error handling techniques discussed in the previous section—which are designed to catch and recover from predictable failures (like a missing file)—assertions and preconditions are used to identify programmer errors. They represent conditions that, if not met, indicate the program’s internal state is invalid and execution cannot safely continue.

When an assertion or precondition fails, the program terminates immediately. This "fail-fast" behavior is intentional; it prevents a bug from cascading and causing data corruption or security vulnerabilities.


Assertions for Development

Assertions are used to verify conditions during development and testing. They allow the compiler to insert checks that help you catch logic errors while you are writing code. Crucially, assertions are ignored in production builds (when optimizations are enabled, such as -O in Xcode). This means they have zero performance impact on the end-user.

You use the assert(_:_:file:line:) function. It takes a condition that must evaluate to true and an optional message to display if the condition is false.


let age = -5

// Execution stops here during debugging because age < 0
assert(age >= 0, "A person's age cannot be negative.")

// This code only runs if the assertion passes
print("Age is verified.")
                        

If your code has already reached a branch where a failure is guaranteed, use assertionFailure(_:file:line:) to trigger an immediate stop.


if age < 0 {
    assertionFailure("Validation logic failed: age is negative.")
}
                        

Preconditions for Production

Preconditions are similar to assertions but remain active in both development and production builds . You use preconditions when a condition is absolutely essential for the program to remain in a valid state, regardless of the build configuration. For example, if a function requires an index to be within the bounds of a subscript, a failed precondition ensures the app crashes before it attempts an out-of-bounds memory access.

Feature Assertion (assert) Precondition (precondition)
Checked in Debug Yes Yes
Checked in Release No (Optimized out) Yes
Use Case Internal logic validation. API contracts and critical safety.
Performance Impact Zero in production. Minimal runtime check.

func updateProfile(atIndex index: Int) {
    // This check persists in the App Store version
    precondition(index >= 0, "Index must be non-negative.")
    
    // Logic to update profile...
}
                        

Fatal Errors

The fatalError(_:file:line:) function is a specialized tool that always triggers a crash, regardless of the build setting or optimization level. It is technically unique because it returns the Never type, informing the compiler that the function will never return control to the caller. This is frequently used in abstract classes or "must-override" methods to ensure a subclass provides an implementation.


func transitionToState(state: String) {
    switch state {
    case "Active":
        print("Moving to active.")
    case "Inactive":
        print("Moving to inactive.")
    default:
        // This should be unreachable; if reached, crash immediately.
        fatalError("Unrecognized state: \(state)")
    }
}
                        

Comparison of Termination Functions

Function Performance Level Optimization Behavior Best Practice
assert Debug only Removed in -O Validating internal state during dev.
precondition Always active Retained in -O Validating external inputs/API requirements.
fatalError Always active Retained in -O and -Ounchecked Unreachable code or stubbed methods.

Warning

While it may seem counter-intuitive to crash an app in production, using precondition or fatalError is often safer than allowing an app to proceed with corrupted data. However, never use these for "recoverable" errors like a network timeout; use the throws pattern instead.


Debugging with Line and File Metadata

Both assertions and preconditions can automatically capture the file name and line number where the failure occurred. This metadata is invaluable when reviewing logs from beta testers or automated CI/CD pipelines.


// Explicitly providing file and line (usually handled automatically by Swift)
precondition(1 == 2, "Math is broken", file: "MathUtils.swift", line: 42)
                        

Note

If you compile with -Ounchecked (the most aggressive optimization), Swift removes even precondition checks to maximize speed. In this mode, the compiler assumes all preconditions are true. Only fatalError is guaranteed to stop execution in -Ounchecked.

Basic Operators & Strings Last updated: Feb. 25, 2026, 7:30 p.m.

test

Arithmetic & Compound Assignment

When an assertion or precondition fails, the program terminates immediately. This "fail-fast" behavior is intentional; it prevents a bug from cascading and causing data corruption or security vulnerabilities.


Assertions for Development

Assertions are used to verify conditions during development and testing. They allow the compiler to insert checks that help you catch logic errors while you are writing code. Crucially, assertions are ignored in production builds (when optimizations are enabled, such as -O in Xcode). This means they have zero performance impact on the end-user.

Operator Name Description
+ Addition Sums two values. Also performs string concatenation.
- Subtraction Subtracts the right value from the left value.
* Multiplication Multiplies two values.
/ Division Divides the left value by the right value.
% Remainder Calculates the remainder of an integer division.

// Integer Arithmetic
let sum = 10 + 5          // 15
let difference = 10 - 5   // 5
let product = 10 * 5      // 50
let quotient = 10 / 3     // 3 (Integer division truncates)

// Floating-Point Arithmetic
let floatQuotient = 10.0 / 3.0 // 3.3333333333333335

// String Concatenation
let hello = "hello, " + "world" // "hello, world"
                        

Warning

Division by zero (10 / 0) will result in a compile-time error if the values are literals, or a runtime crash if the values are variables. Always validate the divisor if it is provided by user input or an external API.


The Remainder Operator

The remainder operator (a % b) calculates how many multiples of b will fit inside a and returns the value that is left over. In Swift, this is technically a remainder operation rather than a modulo operation, though the behavior is identical for positive integers.


let remainder = 9 % 4 // 1
// Calculation: (2 * 4) + 1 = 9

let negativeRemainder = -9 % 4 // -1
// Calculation: (-2 * 4) + -1 = -9
                        

Compound Assignment Operators

Swift provides compound assignment operators that combine assignment (=) with another operation. The most common example is the addition assignment operator (+=). These operators modify the variable on the left-hand side in place.

Compound Assignment Equivalents

Operator Equivalent Expression
a += b a = a + b
a -= b a = a - b
a *= b a = a * b
a /= b a = a / b

var score = 10
score += 5  // score is now 15
score *= 2  // score is now 30
score -= 10 // score is now 20
                        

Note

Compound assignment operators do not return a value. For example, you cannot write let newValue = (score += 5). This design prevents accidental use of the assignment operator when the equality operator (==) was intended.


Unary Operators

Unary operators operate on a single target. Unary minus (-a) toggles the sign of a numeric value, while unary plus (+a) returns the value without change (often used for symmetry in code when using unary minus).


let positiveThree = 3
let negativeThree = -positiveThree      // -3
let alsoPositiveThree = -negativeThree   // 3

let alsoMinusSix = +6 // Unary plus does nothing
                        

Overflow Operators

By default, Swift crashes on overflow to prevent security vulnerabilities. However, if you specifically require overflow behavior (such as in cryptographic hashing or bitwise manipulation), Swift provides a set of Overflow Operators that begin with an ampersand (&).

Operator Behavior
&+ Overflow Addition
&- Overflow Subtraction
&* Overflow Multiplication

var unsignedOverflow = UInt8.max // 255
// unsignedOverflow += 1 // This would CRASH

unsignedOverflow = unsignedOverflow &+ 1 // Wraps around to 0
                        

Comparison & Ternary Operators

Comparison operators are used to evaluate the relationship between two values, returning a Boolean (Bool) result of either true or false. These operators are essential for control flow, allowing a program to make decisions based on data. The Ternary Conditional Operator is a specialized, concise form of an if-else statement that utilizes these comparison results to choose between two values.


Comparison Operators

Swift supports all standard C comparison operators. Because Swift is type-safe, you can only compare two values if they are of the same type. For example, you cannot compare an Int to a Double without first converting one of the values.

Operator Name Description
== Equal to Returns true if both values are equivalent.
!= Not equal to Returns true if the values are different.
> Greater than Returns true if the left value is larger than the right.
< Less than Returns true if the left value is smaller than the right.
>= Greater than or equal to Returns true if the left value is larger than or equal to the right.
<= Less than or equal to Returns true if the left value is smaller than or equal to the right.

let currentSpeed = 55
let speedLimit = 65

// Basic comparisons
let isSpeeding = currentSpeed > speedLimit        // false
let isAtLimit = currentSpeed == speedLimit         // false
let isSafeSpeed = currentSpeed <= speedLimit       // true

// Comparing strings (alphabetical order)
let nameA = "Alice"
let nameB = "Bob"
let comesFirst = nameA < nameB                     // true
                        

Note

Swift also provides two identity operators, === and !==, which are used specifically to test whether two object references both refer to the same single object instance. These are used with classes rather than value types like Int or String.


Ternary Conditional Operator

The ternary conditional operator is a compact syntax represented by question ? answer1 : answer2. It evaluates a Boolean expression (the question). If the expression is true, it evaluates and returns the first value (answer1); if it is false, it evaluates and returns the second value (answer2).

Technically, the ternary operator is a shortcut for the following logic:


if question {
    answer1
} else {
    answer2
}
                        

let contentHeight = 40
let hasHeader = true

// If hasHeader is true, add 50 to height; otherwise, add 20
let rowHeight = contentHeight + (hasHeader ? 50 : 20)
// rowHeight is 90
                        

The ternary operator is highly effective for setting properties or layout constants that depend on a single condition. It reduces boilerplate code and keeps simple logic inline.


Best Practices and Readability

While the ternary operator is efficient, overusing it—especially by nesting multiple ternary operators inside one another—can make code difficult to read and maintain.

Practice Recommendation Technical Reason
Simple Conditions Encouraged Improves conciseness for simple assignments.
Nested Ternaries Discouraged Leads to "Pyramid of Doom" logic; hard for the compiler to optimize and for humans to parse.
Side Effects Avoid Don't use ternary operators to call functions that modify state; use a standard if statement instead.

// AVOID: Nested ternary (Hard to read)
let category = age < 13 ? "Child" : (age < 20 ? "Teen" : "Adult")

// PREFER: Clearer if-else or switch
var category: String
if age < 13 {
    category = "Child"
} else if age < 20 {
    category = "Teen"
} else {
    category = "Adult"
}
                        

Warning

Both "answers" in a ternary operator must return the same data type. You cannot return a String for the true case and an Int for the false case, as the compiler must be able to determine the resulting type of the expression at compile time.

Nil-Coalescing Operator (??)

The nil-coalescing operator (a ?? b) is a powerful shorthand in Swift used to safely unwrap an optional a if it contains a value, or return a default value b if a is nil. This operator is a specialized application of the ternary conditional operator, designed specifically to handle the "no-value" state of optionals with minimal syntax.

Technically, the expression a ?? b is a more concise and readable version of the code a != nil ? a! : b. It streamlines the process of providing fallback values, which is a common requirement when dealing with user inputs, database queries, or network responses that may fail to return data.


Technical Requirements

For the nil-coalescing operator to function, two conditions must be met regarding the types of the operands:

  • The first operand (a) must be an Optional type.
  • The second operand (b) must match the type that is wrapped inside a.

let userProvidedNickname: String? = nil
let defaultNickname = "Guest"

// Because userProvidedNickname is nil, 'Guest' is assigned to effectiveName
let effectiveName = userProvidedNickname ?? defaultNickname
print("Hello, \(effectiveName)!") 
// Prints "Hello, Guest!"
                        

If userProvidedNickname were instead set to "SwiftDev", the operator would unwrap the value and effectiveName would become "SwiftDev". Note that the resulting variable (effectiveName) is a non-optional type (in this case, String), because the operator guarantees a value will exist.


Short-Circuit Evaluation

The nil-coalescing operator utilizes short-circuit evaluation. This means that the default value (b) is only evaluated if the optional (a) is found to be nil. This is technically significant when the default value is the result of a function call or a complex calculation, as it avoids unnecessary computation.


func fetchExpensiveDefaultValue() -> String {
    print("Performing heavy calculation...")
    return "Standard_Default"
}

var cachedValue: String? = "Existing Value"

// The function 'fetchExpensiveDefaultValue' is NOT called here
// because cachedValue is not nil.
let finalResult = cachedValue ?? fetchExpensiveDefaultValue()
                        

Chaining Operators

You can chain multiple nil-coalescing operators together to provide a sequence of fallback values. The compiler evaluates them from left to right, stopping at the first non-nil value it encounters.

Scenario Evaluation Logic Result
Primary Value exists Stop at the first optional. Primary Value
Primary is nil, Secondary exists Skip Primary, stop at Secondary. Secondary Value
All Optionals are nil Fall back to the final hardcoded default. Hardcoded Default

let savedSelection: String? = nil
let lastUsedSelection: String? = "Dark Mode"
let globalDefault = "System Standard"

// Checks savedSelection, then lastUsedSelection, then uses globalDefault
let currentSelection = savedSelection ?? lastUsedSelection ?? globalDefault
// currentSelection is "Dark Mode"
                        

Note

When chaining, ensure the final value in the chain is a non-optional value to ensure the resulting variable is fully "unwrapped" and ready for use in non-optional contexts.


Best Practices

While the nil-coalescing operator is excellent for simple defaults, it should not replace comprehensive error handling if the absence of a value indicates a critical system failure.

Use Case Recommended Tool Why?
UI Defaults ?? Providing placeholder text or default colors.
Optional configuration ?? Using a system default when a user setting is missing.
Critical Data Missing if let or guard Allows you to log an error or alert the user rather than silently failing with a default.

Warning

Be careful when using the nil-coalescing operator with optionals that contain Booleans (e.g., Bool?). Writing isActive ?? true is valid, but it can be logically confusing. Ensure the default value logically aligns with the "nil" state's meaning in your application.

Range Operators (..., ..<)

Range operators in Swift are concise syntaxes used to represent a series of values, typically integers, though they can also be used with other types that conform to the Comparable protocol (such as String or Character). These operators are essential for iterating over collections, defining slice boundaries, and checking if a value falls within a specific span. Swift provides three primary types of range operators: Closed, Half-Open, and One-Sided.


Closed Range Operator (...)

The closed range operator defines a range that runs from a lower bound to an upper bound, including both values. It is written as a...b, where a must not be greater than b. This is the most common range used in for-in loops when you know exactly how many times a block of code should execute.


// Iterates from 1 to 5, including 5
for index in 1...5 {
    print("\(index) times 5 is \(index * 5)")
}
                        

Half-Open Range Operator (..<)

The half-open range operator defines a range that runs from a lower bound up to, but not including, the upper bound. It is written as a..<b. This operator is particularly useful when working with zero-based lists or arrays, where the count of the array is one greater than the final valid index.


let names = ["Anna", "Alex", "Brian", "Jack"]
let count = names.count

// Iterates from 0 up to 3 (the last index), excluding 4
for i in 0..<count {
    print("Person \(i + 1) is called \(names[i])")
}
                        

One-Sided Ranges

One-sided ranges allow you to define a range that continues as far as possible in one direction. These are primarily used when subscripting collections to indicate that the range should start at a specific index and continue to the end, or start at the beginning and continue to a specific index.

Operator Type Description
a... Partial Range From Starts at a and continues to the end of the collection.
...b Partial Range Through Starts at the beginning and includes b.
..<b Partial Range Up To Starts at the beginning and goes up to, but excludes, b.

let stages = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]

// From index 2 to the end
for stage in stages[2...] {
    print(stage) // Gamma, Delta, Epsilon
}

// From the beginning up to index 2
for stage in stages[...2] {
    print(stage) // Alpha, Beta, Gamma
}
                        

Warning

When using one-sided ranges to subscript a collection, you cannot iterate over them directly (e.g., for x in 2...) because the range has no defined end point. You can only iterate over them if the range is applied to a collection with a finite size.


Range Comparisons and Types

Ranges are not just for loops; they are distinct types in the Swift Standard Library. You can store a range in a constant and use the contains(_:) method to perform efficient boundary checks.

Range Expression Resulting Type
1...5 ClosedRange<Int>
1..<5 Range<Int>
...5 PartialRangeThrough<Int>
5... PartialRangeFrom<Int>

let allowedRange = 0...100
let userScore = 85

if allowedRange.contains(userScore) {
    print("Score is within valid boundaries.")
}

// Checking character ranges
let lowercaseLetters = "a"..."z"
print(lowercaseLetters.contains("g")) // true
                        

Technical Constraints

Swift ranges require the lower bound to be less than or equal to the upper bound. Attempting to create a range where the start is greater than the end will result in a runtime crash.


// This will trigger a RUNTIME ERROR
// let invalidRange = 10...1
                        

Note

To iterate backwards, you cannot use a range like 5...1. Instead, you must use a valid range combined with the .reversed() method, such as (1...5).reversed().

String Literals & Mutability

A string is a series of characters, such as "hello, world" or "albatross." In Swift, strings are represented by the String type, which is a fast, modern, and fully Unicode-compliant structure. Unlike many other languages, Swift strings are value types, meaning they are copied when passed to functions or assigned to new constants, ensuring that the original data remains unchanged unless explicitly intended.


String Literals

A string literal is a fixed sequence of textual characters surrounded by a pair of double quotes ("). Swift also supports multi-line string literals for longer passages of text, which are delimited by triple double quotes (""").

Single-line Literals

Standard string literals are used for short snippets of text. They cannot contain unescaped double quotes or backslashes without using escape sequences.


let simpleString = "This is a single-line string."
let quotedString = "He said, \"Swift is powerful.\"" // Escaping quotes
                        

Multi-line Literals

Multi-line string literals allow you to span text across multiple lines. The indentation of the closing triple quotes (""") determines how much whitespace is stripped from each line of the string, allowing you to format the code cleanly without affecting the resulting string value.


let quotation = """
    The White Rabbit put on his spectacles. "Where shall I begin,
    please your Majesty?" he asked.

    "Begin at the beginning," the King said gravely, "and go on
    till you come to the end; then stop."
    """
                        

String Mutability

In Swift, string mutability is governed by the choice of the let or var keyword. This provides a clear, compile-time distinction between strings that are fixed and those that can be modified.

Declaration Mutability Behavior
let Immutable The string cannot be changed after initialization.
var Mutable The string can be modified via methods or the += operator.

var variableString = "Horse"
variableString += " and carriage"
// variableString is now "Horse and carriage"

let constantString = "Highlander"
// constantString += " and others" 
// ERROR: Cannot modify a let constant.
                        

Special Characters in Literals

String literals can include the following special characters to handle formatting and non-printable data.

Escape Sequence Description
\0 Null character
\\ Backslash
\t Horizontal tab
\n Line feed (newline)
\r Carriage return
\" Double quote
\' Single quote
\u{n} Unicode scalar (where n is a 1–8 digit hexadecimal number)

let sparklingHeart = "\u{1F496}" // ????
let multipleLines = "Line one\nLine two"
                        

Extended String Delimiters (Raw Strings)

You can place a string literal within extended delimiters to include special characters without invoking their effect. This is particularly useful for strings containing many backslashes (like Regular Expressions) or internal quotes. You wrap the string in # symbols; the number of # symbols used at the start must match the number at the end.


// Without raw strings: "\\\\Server\\Share"
let path = #"\\Server\Share"# 

// Using interpolation inside a raw string requires a #
let name = "Alice"
let rawInterpolation = #"Greeting: \#(name)"#
                        

Warning

While Swift's multi-line strings are convenient, remember that if you include a line break immediately after the opening """, that line break is not included in the string. If you want a line break at the start or end, you must add a blank line manually within the quotes.


Value Semantics and Performance

Because String is a value type in Swift, it is highly thread-safe. When you pass a string to a function, a logical copy is made. However, Swift employs an optimization technique called Copy-on-Write (COW) Technically, the actual data is only copied if one of the references attempts to modify the string. If multiple variables point to the same immutable string, they share the same memory buffer, ensuring high performance.

Operation Performance Impact Technical Reason
Assignment O(1) Pointers are shared until modification.
Appending O(n) May require reallocating a larger memory buffer.
Interpolation O(n) Requires building a new string buffer from components.

String Interpolation

String interpolation is a high-level feature in Swift that allows you to construct a new String value from a mix of constants, variables, literals, and expressions. By wrapping a value in backslashes and parentheses—\(value)—you instruct the Swift compiler to evaluate that expression, convert the result to a string, and insert it into the string literal at that exact position.

Unlike simple concatenation using the plus operator (+), string interpolation is more readable and can handle non-string types (like integers, booleans, and custom objects) automatically, provided they conform to the LosslessStringConvertible or CustomStringConvertible protocols.


Basic Usage

Interpolation can be used within both single-line and multi-line string literals. During compilation, Swift replaces the placeholders with the actual string representations of the values.


let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
                        

In the example above, multiplier is an Int. The interpolation mechanism automatically handles the conversion of the integer into its textual representation. Furthermore, the second placeholder contains a mathematical expression, demonstrating that interpolation is not limited to simple variable names.


Interpolation with Multi-line Literals

String interpolation is particularly effective for generating dynamic blocks of text, such as emails, logs, or formatted reports. Because multi-line literals preserve their internal structure, you can maintain clear formatting while injecting dynamic data.


let userName = "Alex"
let loginCount = 42

let welcomeEmail = """
    Hello \(userName),
    Welcome back to the portal. 
    You have logged in \(loginCount) times this month.
    """
                        

Formatting and Precision

By default, interpolation uses the standard string representation of a type. For floating-point numbers, this often includes many decimal places. To control the formatting—such as limiting decimal places or adding currency symbols—you typically use the String(format:...) initializer or modern FormatStyle APIs in conjunction with interpolation.

Goal Technique Result
Default "\(pi)" "3.141592653589793"
Precision String(format: "%.2f", pi) "3.14"
Currency price.formatted(.currency(code: "USD")) "$19.99"

import Foundation

let pi = 3.14159265
// Constructing a formatted string within interpolation
let formattedPi = "The value of pi is approximately \(String(format: "%.2f", pi))."
                        

Custom Interpolation (Advanced)

Swift allows you to extend the behavior of string interpolation for custom types. By extending StringInterpolation, you can define custom logic for how your objects are rendered inside a string. This is technically useful for creating domain-specific languages (DSLs) or specialized logging.


extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: Date) {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        self.appendLiteral(formatter.string(from: value))
    }
}

let now = Date()
print("Today's date is \(now)") 
// Uses the custom formatter defined above
                        

Extended Delimiter Interpolation

As discussed in the previous section, if you use extended string delimiters (raw strings) to avoid escaping characters, the standard interpolation syntax \(value) will be treated as literal text. To trigger interpolation in a raw string, you must add a number of # signs after the backslash equal to the number of # signs used for the string delimiters.


let user = "Jordan"
// Standard raw string treats \(user) as text
let literalRaw = #"Hello \(user)"# // "Hello \(user)"

// Adding the # after the backslash enables interpolation
let interpolatedRaw = #"Hello \#(user)"# // "Hello Jordan"
                        

Warning

Avoid putting complex, multi-line logic inside an interpolation placeholder. While technically legal, it makes code significantly harder to read and debug. If an expression requires more than one operator or a function call, it is best practice to calculate the value in a separate constant before injecting it into the string.


Performance Considerations

String interpolation creates a new string in memory. If you are building a very large string by interpolating many smaller pieces in a loop, it is more performance-efficient to use a String buffer or an array of strings that you later join().

Method Best For Complexity
Interpolation UI Labels, simple logs, short messages. O(n)
Joining Array Building large CSVs or long documents. O(n) (more efficient buffer management)

Unicode & Character Handling

Swift’s String and Character types are fully Unicode-compliant, built from the ground up to support every character defined by the Unicode Standard. Unlike older languages that treat strings as arrays of 8-bit or 16-bit integers, Swift views a string as a collection of Extended Grapheme Clusters. This architectural choice ensures that complex characters—such as accented letters, emojis, and diverse scripts—are handled correctly and predictably.


Extended Grapheme Clusters

An extended grapheme cluster is a sequence of one or more Unicode scalars that, when combined, produce a single human-readable character. This is the technical reason why a single visible character in Swift might actually be composed of multiple underlying data points.

For example, the character Ă© can be represented as a single precomposed scalar (U+00E9) or as a decomposed pair of scalars: the letter e (U+0065) followed by the COMBINING ACUTE ACCENT (U+0301). In Swift, both are treated as a single Character instance.

let precomposed: Character = "\u{E9}"             // Ă©
let decomposed: Character = "\u{65}\u{301}"       // e followed by ´

// Swift treats these as identical characters
if precomposed == decomposed {
    print("These characters are logically equivalent.")
}

String Indices and Offsets

Because different characters can occupy different amounts of memory, Swift strings cannot be indexed by simple integer offsets (like string[5]). To find the position of a specific character, you must use the String.Index type. This forces the system to calculate the boundaries of each grapheme cluster, preventing you from accidentally splitting a character in half.

Index Property Description
startIndex The position of the first character in a non-empty string.
endIndex The position after the last character. Not a valid subscript argument.
index(after:) Moves one grapheme cluster forward.
index(before:) Moves one grapheme cluster backward.
index(_:offsetBy:) Moves a specified number of positions from a starting index.
let greeting = "Hello Swift ????"

// Accessing the first character
let firstChar = greeting[greeting.startIndex] // "H"

// Accessing the last character (must move back from endIndex)
let lastIndex = greeting.index(before: greeting.endIndex)
let lastChar = greeting[lastIndex] // "????"

// Moving to a specific offset
let index = greeting.index(greeting.startIndex, offsetBy: 6)
print(greeting[index]) // "S"

Warning

Attempting to access an index that is out of bounds (such as greeting.endIndex or an offset beyond the string length) will result in a runtime crash. Always validate your offsets when dealing with dynamic data.


Counting Characters

The count property of a string returns the number of extended grapheme clusters, not the number of bytes or scalars. This provides the "human-perceived" length of the string.

var word = "cafe"
print("Count: \(word.count)") // 4

word += "\u{301}" // Appending COMBINING ACUTE ACCENT
print("Word: \(word)")        // "café"
print("Count: \(word.count)") // Still 4

In the example above, the count remains 4 even after adding a scalar, because the new scalar combined with the 'e' to form a single grapheme cluster.


Unicode Representations

If you need to interface with low-level APIs or other systems, Swift allows you to access the string's underlying data in three distinct Unicode-compliant formats:

View Technical Detail Use Case
utf8 A collection of UTF-8 code units (8-bit). Web communication and file I/O.
utf16 A collection of UTF-16 code units (16-bit). Interfacing with Objective-C or Windows APIs.
unicodeScalars A collection of 21-bit Unicode scalar values. Advanced text processing and character analysis.
let dogString = "Dog?????"

for codeUnit in dogString.utf8 {
    print("\(codeUnit) ", terminator: "")
}
// Outputs: 68 111 103 226 128 188 240 159 144 182

Note

Iterating over utf8 or utf16 views is significantly faster than iterating over the String itself because it avoids the overhead of calculating grapheme cluster boundaries. Use these views when character-level precision is not required for your task.

Collection Types Last updated: Feb. 25, 2026, 7:31 p.m.

test

Arrays (Ordered Lists)

Arrays (Ordered Lists)

An array stores values of the same type in an ordered list. In Swift, the Array type is bridgeable to the NSArray class in Foundation but maintains distinct value semantics. The same value can appear in an array multiple times at different positions. Swift arrays are strictly typed, meaning an array declared to hold Int values cannot store a String. This type safety ensures that when you retrieve an element, its type is guaranteed.


Array Syntax and Mutability

Swift provides a shorthand syntax for arrays: [Element], where Element is the type of values the array is allowed to store. Like other basic types in Swift, mutability is defined by the choice of let (constant) or var (variable). If you create a constant array, you cannot add, remove, or change any items within it.

// Array literal syntax
var shoppingList: [String] = ["Eggs", "Milk"]

// Creating an empty array
var someInts: [Int] = []

// Creating an array with a default value
var threeDoubles = Array(repeating: 0.0, count: 3) // [0.0, 0.0, 0.0]

Accessing and Modifying Arrays

You interact with an array through its methods, properties, and subscript syntax. Subscripting allows you to retrieve or replace a value at a specific index.

Property/Method Description Complexity
count Returns the number of items in the array. O(1)
isEmpty A Boolean check for whether count is 0. O(1)
append(_:) Adds a new item to the end of the array. O(1) amortized
insert(_:at:) Inserts an item at a specific index. O(n)
remove(at:) Removes and returns the item at an index. O(n)
// Adding items
shoppingList.append("Flour")
shoppingList += ["Baking Powder", "Chocolate Spread"]

// Accessing and modifying via subscript
var firstItem = shoppingList[0]
shoppingList[0] = "Six eggs"

// Modifying a range of values
shoppingList[4...6] = ["Bananas", "Apples"]

Warning

Attempting to access or modify an index outside of an array's existing bounds (e.g., shoppingList[10] when the count is 5) will trigger a runtime fatal error. Always check count or use a safe access pattern if the index is dynamic.


Iterating Over an Array

You can iterate through the entire set of values in an array with the for-in loop. If you need the integer index of each item as well as its value, use the enumerated() method to decompose the array into tuples containing the index and the item.

let birds = ["Eagle", "Falcon", "Hawk"]

// Basic iteration
for bird in birds {
    print(bird)
}

// Iteration with indices
for (index, value) in birds.enumerated() {
    print("Item \(index + 1): \(value)")
}

Performance Characteristics

Swift arrays are optimized for performance. They use contiguous memory for storage, which allows for very fast access to any element by its index. However, because the memory is contiguous, inserting or removing elements at the beginning or in the middle of an array requires shifting all subsequent elements, leading to O(n) performance.

Operation Time Complexity Technical Reason
Index Access O(1) Direct memory offset calculation.
Appending O(1) Space is usually pre-allocated (amortized).
Prepending O(n) All existing elements must move one slot to the right.
Removal O(n) Elements must shift to fill the gap.

Note

Swift employs Copy-on-Write (COW) for arrays. When you assign an array to a new variable, the underlying storage is shared between the two instances. The actual copying of the data only occurs at the moment one of the instances is modified.

Sets (Unique Unordered Collections)

Sets (Unique Unordered Collections)

A Set stores distinct values of the same type in a collection that has no defined ordering. Unlike arrays, which maintain elements in a specific sequence and allow duplicates, a set guarantees that each element appears only once. You should use a set instead of an array when the order of items is not important, or when you need to ensure that an item only exists in the collection once.

To be stored in a set, a type must be hashable. This means the type must provide a way to compute a hash value for itself—an integer that is the same for all objects that compare equally. All of Swift’s basic types (such as String, Int, Double, and Bool) are hashable by default and can be used as set values or dictionary keys.


Set Syntax and Initialization

The type of a Swift set is written as Set<Element>, where Element is the type that the set is allowed to store. Because sets do not have a shorthand syntax like arrays (e.g., [String]), they must be explicitly declared using the full type name or via type inference from an array literal with a type annotation.

// Initializing an empty set
var letters = Set<Character>()

// Initializing a set with an array literal
// Type annotation is required to distinguish it from an Array
var favoriteGenres: Set<String> = ["Rock", "Classical", "Jazz"]

// Adding a duplicate has no effect
favoriteGenres.insert("Rock") 
print(favoriteGenres.count) // Still 3

Accessing and Modifying a Set

You interact with a set through its properties and methods. Because sets are unordered, you cannot access elements using integer subscripts. Instead, you check for membership or iterate through the collection.

Property/Method Description Complexity
count Returns the number of elements in the set. O(1)
isEmpty Returns true if the set contains no elements. O(1)
insert(_:) Adds an element if it is not already present. O(1) average
remove(_:) Removes an element and returns it, or nil if not found. O(1) average
contains(_:) Checks if a specific element exists in the set. O(1) average
if favoriteGenres.contains("Jazz") {
    print("I love jazz!")
}

// Removing an element
if let removedGenre = favoriteGenres.remove("Rock") {
    print("\(removedGenre)? I'm over it.")
}

Note

The contains(_:) method is significantly faster on a Set than on an Array While an array must perform a linear search (O(n)), a set uses a hash table to jump directly to the value's location, making it an O(1) operation regardless of the collection size.


Fundamental Set Operations

One of the most powerful features of sets is the ability to perform mathematical set operations. These allow you to combine two sets or determine the relationship between them with high efficiency.

Operation Technical Behavior Result
intersection(_:) Creates a new set with values common to both sets. Common elements only.
symmetricDifference(_:) Creates a new set with values in either set, but not both. Non-overlapping elements.
union(_:) Creates a new set with all values from both sets. All unique elements.
subtracting(_:) Creates a new set with values not in the specified set. Unique to the first set.
let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]

oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

oddDigits.intersection(singleDigitPrimeNumbers).sorted()
// [3, 5, 7]

oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9]

Set Membership and Equality

Swift provides methods to compare sets and check their hierarchical relationship (subsets and supersets).

  • Use the "is equal" operator (==) to determine if two sets contain all the same values.
  • Use isSubset(of:) to determine if all values of a set are contained in a specified set.
  • Use isSuperset(of:) to determine if a set contains all values of a specified set.
  • Use isDisjoint(with:) to determine if two sets have no values in common.
let houseAnimals: Set = ["????", "????"]
let farmAnimals: Set = ["????", "????", "????", "????", "????"]
let cityAnimals: Set = ["????", "????"]

houseAnimals.isSubset(of: farmAnimals)    // true
farmAnimals.isSuperset(of: houseAnimals) // true
farmAnimals.isDisjoint(with: cityAnimals) // true

Warning

Because sets are unordered, iterating over a set using a for-in loop will yield elements in a different order every time the program is run. If you require a stable order for display, call the sorted() method, which returns an Array containing the set's elements sorted using the < operator.

Dictionaries (Key-Value Pairs)

Dictionaries (Key-Value Pairs)

A Dictionary stores associations between keys of the same type and values of the same type in an unordered collection. Each value is associated with a unique key, which acts as an identifier for that value within the dictionary. Unlike an array, where you look up items by their integer index, you look up items in a dictionary by their key.

Dictionaries are ideal when you need to look up values based on a specific identifier, such as a user ID, a country code, or a configuration setting name.


Dictionary Syntax and Initialization

The type of a Swift dictionary is written as Dictionary<Key, Value>, or more commonly using the shorthand syntax [Key: Value]. Like sets, the Key type must conform to the Hashable protocol.

// Initializing an empty dictionary
var namesOfIntegers: [Int: String] = [:]

// Initializing with a dictionary literal
var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

// Type inference (if values and keys are of the same type)
var scores = ["Player1": 10, "Player2": 20]

Accessing and Modifying a Dictionary

You access and modify a dictionary through its properties, methods, or by using subscript syntax. When you use a subscript to look up a key, Swift returns an Optional value. If the key exists, you get the value; if the key does not exist, you get nil.

Operation Syntax/Method Result/Effect
Read Value airports["LHR"] Returns an Optional(Value) or nil.
Add/Update dict[key] = value Inserts a new pair or overwrites existing.
Update Method updateValue(_:forKey:) Updates and returns the old value (if any).
Remove dict[key] = nil Deletes the key-value pair.
Count dict.count Returns the number of pairs.
// Adding a new item
airports["LHR"] = "London"

// Updating a value using subscript
airports["LHR"] = "London Heathrow"

// Safe lookup with a default value
let airportName = airports["JFK", default: "Unknown Airport"]

Iterating Over a Dictionary

You can iterate over the key-value pairs in a dictionary with a for-in loop. Each item in the dictionary is returned as a (key, value) tuple, which you can decompose into temporary constants for use within the loop.

for (airportCode, airportName) in airports {
    print("\(airportCode): \(airportName)")
}

// Iterating over just keys or just values
for code in airports.keys {
    print("Airport code: \(code)")
}

Note

Because dictionaries are unordered, the order in which keys and values are retrieved during iteration is not guaranteed. If you need a specific order, you must sort the keys or values property before iterating.


Performance Characteristics

Swift Dictionaries are implemented as hash tables. This provides highly efficient lookup, insertion, and removal operations.

Operation Time Complexity Technical Reason
Lookup O(1) average Key is hashed to find the direct memory bucket.
Insertion O(1) average New entries are placed in a bucket based on hash.
Removal O(1) average Direct access to the key's bucket for deletion.

Warning

While the average case is O(1), a "hash collision" (where many keys generate the same hash) can technically degrade performance to O(n). Swift mitigates this by using high-quality, randomized hashing seeds.


Collection Summary Table

To help you choose the right collection type for your data:

Collection Ordered? Unique? Access Method
Array Yes No Integer Index
Set No Yes Membership check / Hash
Dictionary No Keys only Key-based Subscript

Collection Mutability

In Swift, the mutability of collections (Arrays, Sets, and Dictionaries) is governed by the same principles as basic data types: the use of the let and var keywords. This design choice is a core part of Swift's safety model, allowing the compiler to enforce data integrity and optimize performance through value semantics.


The Role of let vs. var

When you assign a collection to a constant using let, that collection becomes immutable. This means you cannot change its size (add or remove elements) or modify the contents of its elements. Conversely, assigning a collection to a variable using var makes it mutable.

Keyword Collection Type Permitted Actions Technical Constraint
let Immutable Read-only access. Memory is locked; size and content are fixed.
var Mutable Add, remove, or update elements. Memory can be reallocated to accommodate growth.
// A mutable array
var mutableCities = ["New York", "London"]
mutableCities.append("Tokyo") // Allowed

// An immutable array
let fixedCities = ["Paris", "Berlin"]
// fixedCities.append("Rome") // COMPILE-TIME ERROR
// fixedCities[0] = "Lyon"    // COMPILE-TIME ERROR

Value Semantics and Copying

Swift collections are value types, implemented as structures. When you assign a collection to a new constant or variable, or when you pass it into a function, the collection is logically copied. The new instance is independent of the original; changes made to the mutable copy do not reflect in the original collection.

Copy-on-Write (COW) Optimization

To prevent the performance overhead of copying large amounts of data unnecessarily, Swift uses a technique called Copy-on-Write.

  1. When you "copy" a collection, both variables initially point to the same memory buffer.
  2. The actual byte-for-byte copy only happens if one of the instances is modified.
  3. This ensures that passing large arrays to functions is as fast as passing a single pointer, provided the function does not modify the data.
var numbers = [1, 2, 3]
var copyOfNumbers = numbers // No actual copy happens yet

copyOfNumbers.append(4) 
// Modification triggers COW; copyOfNumbers now has its own unique storage
// numbers remains [1, 2, 3]

Mutability in Functions

By default, parameters passed into a Swift function are constants (let). If you pass a collection into a function, the function cannot modify that collection unless it creates its own local variable copy or the parameter is explicitly marked with the inout keyword.

  • Standard Parameter: The function receives a local immutable copy.
  • In-out Parameter:> The function receives a reference that allows it to modify the original collection directly.
func addElement(to list: [Int]) {
    // list.append(4) // ERROR: 'list' is a let constant
}

func modifyOriginal(list: inout [Int]) {
    list.append(4) // This modifies the original array passed in
}

Best Practices for Mutability

  • Default to let: Always declare collections as constants unless you have a specific reason to change them. This prevents accidental bugs where data is modified unexpectedly.
  • Performance: Because of COW, don't worry about the performance cost of passing collections between functions.
  • Thread Safety: Immutable collections are inherently thread-safe because they cannot be changed by one thread while being read by another.

Control Flow Last updated: Feb. 25, 2026, 7:33 p.m.

test

For-In Loops

The for-in loop is Swift’s primary mechanism for iterating over a sequence. Whether you are traversing a range of numbers, the items in an array, the characters in a string, or the key-value pairs in a dictionary, the for-in loop provides a clean and safe syntax that handles the underlying iteration logic for you.


Iterating Over Numerical Ranges

One of the most common uses for a for-in loop is to execute a block of code a specific number of times using range operators. You can use a closed range (...) to include the final value or a half-open range (..<) to exclude it.

// Iterating over a closed range
for index in 1...3 {
    print("\(index) times 5 is \(index * 5)")
}
// Outputs: 1 times 5 is 5, 2 times 5 is 10, 3 times 5 is 15

// Using an underscore if the value isn't needed
let base = 3
let power = 5
var answer = 1
for _ in 1...power {
    answer *= base
}

Note

Use the underscore (_) as a placeholder for the loop variable if you don't actually need to access the current value within the loop body. This signals to the compiler (and other developers) that the value is intentionally ignored.


Iterating Over Collections

Swift collections (Arrays, Sets, and Dictionaries) are all sequences and can be used directly with for-in.

  • Arrays: Elements are accessed in order.
  • Sets: Elements are accessed in an undefined, unordered sequence.
  • Dictionaries: Elements are accessed as (key, value) tuples.
Collection Item Type Iteration Order
Array Element Fixed (0 to count-1)
Set Element Random/Unordered
Dictionary (Key, Value) Random/Unordered
let names = ["Anna", "Alex", "Brian"]
for name in names {
    print("Hello, \(name)!")
}

let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
    print("\(animalName)s have \(legCount) legs")
}

Advanced Iteration: stride

If you need to iterate over a range but want to skip values (e.g., only even numbers) or count backward, the stride functions provide more control than standard ranges.

  • stride(from:to:by:) : Excludes the end value (half-open).
  • stride(from:through:by:) : Includes the end value (closed).
// Skipping values (up to but not including 10)
for tickMark in stride(from: 0, to: 10, by: 2) {
    print(tickMark) // 0, 2, 4, 6, 8
}

// Counting backward (including 0)
for countdown in stride(from: 3, through: 0, by: -1) {
    print(countdown) // 3, 2, 1, 0
}

Filtering and Modification

You can use the where clause inside a for-in loop to filter which items should execute the loop body. This keeps your code flat and avoids nested if statements.

let temperatures = [20, 25, 30, 35, 40]

// Only print temperatures above 30
for temp in temperatures where temp > 30 {
    print("Heat warning: \(temp)°C")
}

Warning

While you can iterate over a mutable collection, you cannot add or remove items from that same collection while the loop is running. Doing so will cause a runtime crash. If you need to modify the collection, iterate over a copy or collect the changes into a new array.

While & Repeat-While Loops

While for-in loops are best when you know the number of iterations in advance (or are iterating over a finite collection), while loops are used when the number of iterations is unknown and depends on a condition being met. Swift provides two flavors of this loop, distinguished by when the condition is evaluated.


The While Loop

A while loop evaluates its condition at the start of each pass. If the condition is false initially, the loop body will never execute. This is ideal for scenarios where the required task might already be completed.

var square = 0
var diceRoll = 0
let finalSquare = 25

while square < finalSquare {
    // Roll the dice and move the player
    diceRoll = Int.random(in: 1...6)
    square += diceRoll
    print("Rolled a \(diceRoll). Now at square \(square)")
}
print("Game over!")

The Repeat-While Loop

The repeat-while loop (known as a do-while loop in other languages) evaluates its condition at the end of each pass. This guarantees that the loop body is executed at least once, regardless of whether the condition is true or false at the start.

var energyLevel = 0

repeat {
    print("Performing a training rep...")
    energyLevel -= 1
} while energyLevel > 0

// Even though energyLevel was 0, the code ran once.

Comparison of Loop Types

Loop Type When to Use Minimum Executions
for-in Iterating over ranges, arrays, or sequences. 0
while When the condition is known before the first execution. 0
repeat-while When the logic must run at least once (e.g., a menu or UI refresh). 1

Control Transfer Statements

Inside any loop, you can use specific keywords to change the flow of execution:

  • break Immediately ends the execution of the entire loop.
  • continue Stops the current iteration and jumps to the start of the next one (re-evaluating the condition).
var count = 0
while count < 10 {
    count += 1
    
    if count % 2 == 0 {
        continue // Skip even numbers
    }
    
    if count == 7 {
        break // Stop the loop entirely when reaching 7
    }
    
    print(count) // Only prints 1, 3, 5
}

Warning

Be careful to ensure your loop condition will eventually become false. An infinite loop (e.g., while true { ... } without a break) will cause your application to hang or consume 100% of the CPU, eventually leading to a system-forced termination.

Conditional Statements (If, Switch, Guard)

Swift provides three primary ways to manage conditional logic. While they all direct the flow of your program based on specific criteria, they are optimized for different scenarios: if for simple checks, switch for complex pattern matching, and guard for early exits and requirement validation.


1. If, Else If, and Else

The if statement is the most basic form of conditional. It executes a block of code only if a condition is true. Unlike many C-based languages, the parentheses around the condition are optional in Swift, but the braces {} are required.

let temperature = 25

if temperature >= 30 {
    print("It's very hot.")
} else if temperature <= 15 {
    print("It's quite cold.")
} else {
    print("The weather is pleasant.")
}

2. Switch Statement

A switch statement compares a value against several possible matching patterns. Swift switch statements are significantly more powerful than those in other languages:

  • Exhaustive: You must handle every possible value of the type (often via a default case).
  • No Implicit Fallthrough: Execution stops at the end of a matched case; you don't need break.
  • Flexible Matching: It supports intervals, tuples, and type casting.
Feature Syntax Example Description
Interval Matching case 1...10: Matches if value is within the range.
Tuples case (0, 0): Matches multiple values at once.
Value Binding case (let x, 0): Binds a portion of the match to a local constant.
Where Clause case let (x, y) where x == y: Adds additional Boolean requirements to a case.
let somePoint = (1, 1)

switch somePoint {
case (0, 0):
    print("Origin")
case (_, 0):
    print("On the x-axis")
case (let x, let y) where x == y:
    print("On the line y = x")
default:
    print("Somewhere else")
}

3. Guard Statement

A guard statement is used to transfer program control out of a scope if one or more conditions aren't met. It is often called an early exit. It is technically similar to an if statement, but it always has an else clause that must exit the current scope (using return, break, continue, or throw).

Aspect if let / if guard let / guard
Focus Handles the "Success" path inside braces. Handles the "Failure" path inside braces.
Variable Scope Unwrapped variables are only available inside the if block. Unwrapped variables remain available for the rest of the scope.
Code Structure Leads to nested "pyramids" of code. Keeps the "happy path" aligned to the left margin.
func greet(person: [String: String]) {
    guard let name = person["name"] else {
        return // Exit early if name is missing
    }
    
    // 'name' is now available here and for the rest of the function!
    print("Hello, \(name)!")
}

Comparison Summary

Statement Best Used For... Key Advantage
If Simple, binary decisions. Familiar and concise for small checks.
Switch Multiple potential states or complex patterns. Safety through exhaustiveness; powerful pattern matching.
Guard Validating requirements at the start of a function. Prevents deep nesting; keeps main logic clean.

Control Transfer (Continue, Break, Fallthrough)

Control transfer statements change the order in which your code executes by moving control from one piece of code to another. In Swift, these are used primarily within loops and switch statements to fine-tune logic flow.


Continue

The continue statement tells a loop to stop what it is doing and start again at the beginning of the next iteration through the loop. It effectively says, "I am done with the current loop item," without leaving the loop altogether.

let input = "great minds think alike"
var output = ""
let vowels: [Character] = ["a", "e", "i", "o", "u", " "]

for character in input {
    if vowels.contains(character) {
        continue // Skip vowels and spaces
    }
    output.append(character)
}
print(output) // "grtmndsthnklk"

Break

The break statement ends execution of an entire control flow statement immediately. This can be used inside a loop to stop execution early, or inside a switch case to ignore a specific pattern.

  • In a Loop: Ends the loop's execution and transfers control to the code immediately after the loop's closing brace.
  • In a Switch: Ends the execution of the switch block. This is often used when you must provide a case for a value but don't want to perform any action (since Swift cases cannot be empty).
let numbers = [1, 2, 3, 4, 5, 100, 6, 7]

for number in numbers {
    if number > 10 {
        print("Invalid number found: \(number). Stopping.")
        break // Exit the loop entirely
    }
    print("Processing \(number)")
}

Fallthrough

In Swift, switch statements do not "fall through" into the next case by default. Once a match is found and the code is executed, the switch completes. If you explicitly want the C-style behavior where execution continues into the next case, use the fallthrough keyword.

let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"

switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
    description += " a prime number, and also"
    fallthrough
default:
    description += " an integer."
}
print(description) 
// "The number 5 is a prime number, and also an integer."

Warning

The fallthrough keyword does not check the case conditions for the case it falls into. It simply causes code execution to move directly to the statements inside the next case (or default) block.


Labeled Statements

When you have nested loops, break and continue only affect the innermost loop. To target an outer loop, you can name the loop using a statement label. You then follow the break or continue keyword with the name of the label.

gameLoop: while true {
    for i in 1...10 {
        if i == 5 {
            break gameLoop // Exits the 'while' loop, not just the 'for' loop
        }
    }
}

Summary of Transfer Keywords

Keyword Use Case Result
continue Loops Skips the rest of the current iteration; starts next pass.
break Loops / Switch Terminates the current loop or switch block immediately.
fallthrough Switch Forces execution to continue into the next case.
return Functions Exits the function and optionally returns a value.
throw Error Handling Exits the current scope and propagates an error.

API Availability Checking (#available)

As Swift and Apple’s platforms (iOS, macOS, watchOS, and tvOS) evolve, new APIs are introduced in every version. To prevent your app from crashing when running on older operating systems that don't recognize a new function or class, Swift uses availability conditions

The #available keyword allows you to perform a runtime check. If the check passes, the compiler knows it is safe to use the newer APIs within that block of code.


Using the #available Condition

The #available syntax is typically used within an if or guard statement. It takes a list of platform names and version numbers, ending with a mandatory asterisk (*).

if #available(iOS 15.0, macOS 12.0, *) {
    // Use iOS 15 / macOS 12 APIs here
    let attribution = UISheetPresentationController.Detent.medium()
    print("Using modern sheet detents.")
} else {
    // Fallback on earlier versions
    print("Using standard full-screen presentation.")
}
  • Platform Names: iOS, macOS, watchOS, tvOS.
  • The Asterisk (*): This is required. It specifies that on any other platform not listed, the condition is handled as true for the minimum deployment target of your project.

Marking Declarations with @available

While #available is a runtime check, the @available attribute is used at the declaration level. It informs the compiler that a specific function, class, or property can only be accessed if the calling code meets the version requirements.

Use Case Attribute / Keyword Level
Inside Logic #available Statement level (if/guard)
Entire Function @available Declaration level
Entire Class @available Declaration level
@available(iOS 14.0, *)
func useNewFeature() {
    // This entire function is only accessible on iOS 14+
}

If you try to call useNewFeature() from a context that supports iOS 13, the Swift compiler will generate an error and suggest you wrap the call in an #available check.


Unavailability Checking (#unavailable)

Introduced in later versions of Swift, the #unavailable condition is the logical inverse of #available. It is used when you want to execute code specifically for older versions of an OS.

@available(iOS 14.0, *)
func useNewFeature() {
    // This entire function is only accessible on iOS 14+
}if #unavailable(iOS 15.0) {
    // Code to run ONLY on iOS 14 and earlier
    print("Running legacy layout logic.")
}

Summary of Availability Tools

Tool Syntax Primary Purpose
Runtime Check if #available(...) Branching logic based on the user's current OS version.
Guard Check guard #available(...) else { return } Early exit if the device OS is too old for the task.
API Restriction @available(...) Restricting a library function to a specific OS minimum.
Deprecation @available(*, deprecated, message: "...") Warning developers that an API will be removed in the future.

Note

These checks are performed at runtime, but they provide compile-time safety. This means the compiler won't even let you build the app if you attempt to use a new API without one of these safety nets in place.

Functions & Closures Last updated: Feb. 25, 2026, 10:03 p.m.

test

Defining and Calling Functions

Functions are self-contained chunks of code that perform a specific task. You give a function a name that identifies what it does, and this name is used to "call" the function to perform its task when needed. In Swift, functions are first-class citizens, meaning they can be passed as arguments, returned from other functions, and assigned to variables.


Function Definition and Syntax

Every function in Swift starts with the func keyword. You specify the function's name, its input parameters (enclosed in parentheses), and its return type (following the -> symbol).

Component Purpose Requirement
func Declares a new function. Mandatory
Parameters Inputs passed into the function. Optional (can be empty ())
-> The return arrow. Required if returning a value
Return Type The data type of the output. Optional (defaults to Void)
func greet(person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

// Calling the function
print(greet(person: "Anna")) // "Hello, Anna!"

Function Parameters and Return Values

Swift functions are highly flexible. They can take multiple parameters or none at all, and they can return a single value, multiple values (via tuples), or nothing.

Functions Without Parameters

func sayHelloWorld() -> String {
    return "hello, world"
}

Functions With Multiple Parameters

Parameters are separated by commas inside the parentheses.

func calculateDifference(start: Int, end: Int) -> Int {
    return end - start
}

Functions Without Return Values

Functions that do not define a return type technically return a special value of type Void, which is an empty tuple written as ().

func greetDirectly(person: String) {
    print("Hello, \(person)!")
}

Returning Multiple Values (Tuples)

You can use a tuple type as the return type for a function to return multiple values as part of one compound return value.

func minMax(array: [Int]) -> (min: Int, max: Int) {
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)
}

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")

Functions with Implicit Returns

If the entire body of a function is a single expression, the function implicitly returns that expression. You can omit the return keyword to make the code more concise.

func greeting(for person: String) -> String {
    "Hello, " + person + "!" // No 'return' keyword needed
}

Function Argument Labels and Parameter Names

Each function parameter has both an argument label (used when calling the function) and a parameter name (used within the function's implementation). By default, parameters use their parameter name as their argument label.

Type Syntax Use Case
Standard func f(name: Int) Call: f(name: 1)
Custom Label func f(from name: Int) Call: f(from: 1)
Omitted Label func f(_ name: Int) Call: f(1)
// 'from' is the label, 'hometown' is the name
func greet(person: String, from hometown: String) -> String {
    return "Hello \(person)! Glad you could visit from \(hometown)."
}

// The call reads like a sentence:
print(greet(person: "Bill", from: "Cupertino"))

Note

Using an underscore _ for an argument label allows you to call a function without a label, which is common for very obvious parameters (e.g., abs(-5) ).

Function Parameters & Return Values

Swift functions are highly versatile, allowing you to define complex input requirements and structured outputs. Beyond basic parameters, Swift supports default values, variable-length arguments, and the ability to modify variables directly via inout parameters.


Default Parameter Values

You can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.


func setAlarm(time: String, sound: String = "Radar") {
    print("Setting alarm for \(time) with sound: \(sound)")
}

setAlarm(time: "7:00 AM")                   // Uses "Radar"
setAlarm(time: "8:00 AM", sound: "Chimes") // Uses "Chimes"

Variadic Parameters

A variadic parameter accepts zero or more values of a specified type. You use a variadic parameter to specify that the parameter can be passed a varying number of input values when the function is called. Write variadic parameters by inserting three period characters (...) after the parameter’s type name.

func arithmeticMean(_ numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }
    // 'numbers' is treated as [Double] inside the function
    return total / Double(numbers.count)
}

arithmeticMean(1, 2, 3, 4, 5) // Returns 3.0

Note

A function can have multiple variadic parameters, but the first parameter that follows a variadic parameter must have an argument label to avoid ambiguity.


In-Out Parameters

Function parameters are constants by default. If you want a function to modify a parameter’s value, and you want those changes to persist after the function call has ended, define that parameter as an in-out parameter instead.

To write an in-out parameter, place the inout keyword right before its type. When calling the function, you must place an ampersand (&) directly before a variable's name to indicate that it can be modified by the function.

Aspect Standard Parameter In-Out Parameter
Mutability Immutable (let) Mutable
Call Syntax funcName(value) funcName(&variable)
Scope Change stays inside function Change persists in original variable
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var x = 3, y = 107
swapTwoInts(&x, &y)
print("x is now \(x), y is now \(y)") // x is 107, y is 3

Function Types

Every function has a specific function type, made up of the parameter types and the return type. You can use function types like any other type in Swift, which allows you to pass functions as arguments to other functions or return them.

Function Type Signature
func add(a: Int, b: Int) -> Int (Int, Int) -> Int
func hello() () -> Void
func printName(name: String) (String) -> Void

Using Function Types

func addTwoInts(_ a: Int, _ b: Int) -> Int { a + b }

// Assigning a function to a variable
var mathFunction: (Int, Int) -> Int = addTwoInts

print("Result: \(mathFunction(2, 3))") // Result: 5

Summary of Parameter Traits

Feature Syntax Key Behavior
Argument Labels func f(label name: Type) Makes the call-site readable as prose.
Default Values name: Type = value Allows omitting the argument during a call.
Variadic name: Type... Collects multiple inputs into a local array.
In-Out name: inout Type Allows the function to modify external variables.

Argument Labels (External vs Internal Names)

In Swift, every function parameter has two names: an argument label (the external name used when calling the function) and a parameter name (the internal name used within the function's body). This distinction allows you to write code that is expressive and sentence-like at the call-site, while remaining clear and concise inside the implementation.


External vs. Internal Names

By default, the parameter name serves as the argument label. However, you can explicitly define a unique external label by writing it before the parameter name.

Name Type Where it's used Purpose
Argument Label At the Call-Site Provides context for the caller; makes the code readable.
Parameter Name Inside the Function Body Acts as a local variable name for the function's logic.

// 'to' is the argument label (external)
// 'recipient' is the parameter name (internal)
func sendMessage(to recipient: String, message: String) {
    // We use 'recipient' inside the function
    print("Sending: '\(message)' to \(recipient)")
}

// We use 'to' when calling the function
sendMessage(to: "Sarah", message: "Hello!")

Omitting Argument Labels

If you don't want an argument label for a parameter, you can write an underscore (_) instead of an explicit argument label. This is typically used when the purpose of the first argument is obvious from the function's name.

func square(_ number: Int) -> Int {
    return number * number
}

// Call-site is concise:
let result = square(5)

Best Practices for Naming

Swift is designed to be highly readable. Follow these conventions to write "Swifty" code:

  • Prepositional Labels: Use labels like from, to, with, or for to make the function call read like a natural sentence.
  • Verb-First Function Names: Start function names with a verb (e.g., calculate, fetch, remove).
  • Clarity over Conciseness: It is better to have a slightly longer name that explains exactly what the parameter does than a short, ambiguous name.
Style Syntax Call-Site Example
Default func move(distance: Int) move(distance: 10)
Omitted func abs(_ value: Int) abs(-5)
Custom Label func greet(for person: String) greet(for: "Joe")

Why the Distinction Matters

The separation of names solves a common naming conflict: you want the variable inside your function to be a noun (like person), but the caller needs a preposition (like to) to make the sentence structure work.

// Without distinct names:
func greet(person: String) { /* ... */ }
greet(person: "Alice") // "Greet person Alice" (A bit clunky)

// With distinct names:
func greet(for person: String) { /* ... */ }
greet(for: "Alice") // "Greet for Alice" (Reads better)

Tip

If you find yourself using a parameter name that is identical to its label, just use the default syntax (e.g., func log(message: String)). Swift will treat message as both the label and the name.

Variadic Parameters

A variadic parameter allows a function to accept zero or more values of a specific type. This is useful when you don't know exactly how many input values will be passed to the function, but you want to process all of them using the same logic.

You define a variadic parameter by appending three periods (...) to the parameter's type name.


How Variadic Parameters Work

Inside the function, the values passed to a variadic parameter are made available as an array of the appropriate type. For example, a variadic parameter of type Double... is made available inside the function body as a constant array of type [Double].

func arithmeticMean(_ numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }
    // numbers is [Double], so we check .count
    return numbers.isEmpty ? 0 : total / Double(numbers.count)
}

print(arithmeticMean(1, 2, 3, 4, 5)) 
// Returns 3.0 (the average of 5 numbers)

print(arithmeticMean(3, 8.25, 18.75)) 
// Returns 10.0 (the average of 3 numbers)

Syntax Rules and Limitations

Swift provides specific guidelines to ensure that variadic parameters do not make your function calls ambiguous.

Rule Description
Type Consistency All values passed to the variadic parameter must be of the same type.
Multiple Variadics A function can have more than one variadic parameter.
Labeling The parameter following a variadic parameter must have an argument label to clarify where the variadic list ends.
Internal Type The values are always treated as an Array within the function scope.
// Example with multiple parameters and labels
func displayScores(teamName: String, scores: Int..., bonuses: Int...) {
    print("Team: \(teamName)")
    print("Base Scores: \(scores)")
    print("Bonuses: \(bonuses)")
}

displayScores(teamName: "Dragons", scores: 10, 20, 30, bonuses: 5, 10)

Variadic vs. Array Parameters

While they look similar, choosing between a variadic parameter and an array parameter depends on how you want the function to be called.

Feature Variadic (Type...) Array ([Type])
Call-site Syntax funcName(1, 2, 3) funcName([1, 2, 3])
Flexibility Easier for quick, ad-hoc lists. Better for passing existing variables.
Readability Feels like a standard list of arguments. Explicitly indicates a collection.
// Variadic: Best for "print" style functions
print("A", "B", "C") 

// Array: Best when the data already exists in a variable
let results = [90, 85, 95]
func upload(results: [Int]) { /* ... */ }
upload(results: results)

Practical Example: String Joining

Variadic parameters are frequently used for formatting or joining tasks where the number of items is arbitrary.

func buildSentence(words: String...) -> String {
    return words.joined(separator: " ") + "."
}

let sentence = buildSentence(words: "Swift", "is", "exceptionally", "versatile")
print(sentence) // "Swift is exceptionally versatile."

Warning

You cannot pass an existing array directly into a variadic parameter. For example, if you have let list = [1, 2, 3], you cannot call arithmeticMean(list). In such cases, you must change the function signature to accept an array [Double] instead of Double... .

In-Out Parameters

By default, function parameters in Swift are constants. Attempting to modify the value of a parameter from within the body of a function results in a compile-time error. If you need a function to modify a variable's value and want those changes to persist after the function call ends, you must define that parameter as an in-out parameter.


Definition and Mechanism

An in-out parameter has a value that is passed into the function, is modified by the function, and is then passed back out of the function to replace the original value. You define this by placing the inout keyword before the parameter's type.

Step Action Responsibility
1 The value is copied into the function parameter. Swift Runtime
2 The function modifies the local copy. Function Logic
3 The modified value is copied back to the original variable. Swift Runtime

Syntax and Usage

To use an in-out parameter, you must follow two strict syntax rules:

  1. Declaration: Use the inout keyword in the function signature.
  2. Call-site: Pass a variable (not a constant) and prefix it with an ampersand (&).
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var someInt = 3
var anotherInt = 107

// Passing with ampersands to indicate modification
swapTwoInts(&someInt, &anotherInt)

print("someInt is now \(someInt), anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, anotherInt is now 3"

Constraints and Limitations

Because in-out parameters involve modifying external state, they have specific safety requirements:

  • Variables Only: You can only pass a variable as the argument for an in-out parameter. You cannot pass a constant (let) or a literal value (like 5), because neither can be modified.
  • No Default Values: In-out parameters cannot have default values.
  • Variadic Limits: An in-out parameter cannot be variadic.
  • Memory Safety: You cannot pass the same variable to multiple in-out parameters of the same function call, as this would violate Swift's memory exclusivity rules (simultaneous access to the same memory location).

Comparison: Standard vs. In-Out Parameters

Feature Standard Parameter In-Out Parameter
Internal Mutability Immutable (let) Mutable
External Impact None (Original is safe) Original is updated
Argument Type Constants, Variables, Literals Variables only
Prefix at Call-site None & (Ampersand)

Technical Note

In-out parameters are an alternative to returning a value. While a function can only return one primary value (or a tuple), in-out parameters allow a function to have "side effects" that modify multiple existing variables simultaneously.

Function Types

In Swift, functions are first-class citizens. This means every function has a specific function type, consisting of its parameter types and its return type. You can use these types just like Int or String, allowing you to store functions in variables, pass them as arguments to other functions, or return them from functions.


Identifying Function Types

A function type is written by listing the parameter types in parentheses, followed by an arrow (->) and the return type.

Function Example Function Type Description
func add(a: Int, b: Int) -> Int (Int, Int) -> Int Takes two Ints, returns an Int.
func hello() () -> Void Takes no parameters, returns nothing.
func printName(name: String) (String) -> Void Takes a String, returns nothing.

Using Function Types as Variables

You can define a variable or constant to be of a function type and assign an actual function to it. This allows you to swap out the logic a variable performs at runtime.

func addTwoInts(_ a: Int, _ b: Int) -> Int { a + b }
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int { a * b }

// Define a variable that can hold any (Int, Int) -> Int function
var mathFunction: (Int, Int) -> Int = addTwoInts

print("Result: \(mathFunction(2, 3))") // Result: 5

// Change the variable to hold a different function
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))") // Result: 6

Function Types as Parameter Types

You can pass a function as a parameter to another function. This is a powerful technique for creating Higher-Order Functions, where the behavior of a function is customized by the logic passed into it.

func printMathResult(_ mathFunc: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFunc(a, b))")
}

printMathResult(addTwoInts, 3, 5) // Result: 8

Function Types as Return Types

A function can return another function. To do this, you write a complete function type as the return type of the "wrapper" function.

func stepForward(_ input: Int) -> Int { input + 1 }
func stepBackward(_ input: Int) -> Int { input - 1 }

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    return backward ? stepBackward : stepForward
}

var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)

currentValue = moveNearerToZero(currentValue)
print(currentValue) // 2

Summary Table: Function Type Operations

Operation Concept Benefit
Assignment let f = myFunction Simplifies passing logic around code blocks.
Injection func exec(f: () -> Void) Allows decoupling of logic (Dependency Injection).
Selection return someCondition ? f1 : f2 Dynamically chooses strategy at runtime.

Tip

When using function types as parameters, you can use Type Aliases to make your code more readable. For example, typealias MathOp = (Int, Int) -> Int allows you to use MathOp instead of the full signature everywhere.

Closures & Syntax Optimization

Closures are self-contained blocks of functionality that can be passed around and used in your code. They are similar to functions but have a more compact syntax and the ability to "capture" and store references to variables and constants from the context in which they are defined.


Closure Expression Syntax

Closure expressions provide a way to write "inline" closures in a brief, focused syntax. The general form is:

{ (parameters) -> return type in statements }

let names = ["Chris", "Alex", "Ewa", "Barry", "Dani"]

// Standard function approach
func backward(_ s1: String, _ s2: String) -> Bool { return s1 > s2 }
var reversedNames = names.sorted(by: backward)

// Inline closure approach
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

Syntax Optimization

Swift provides several ways to reduce the "boilerplate" code of a closure, making it cleaner without losing clarity.

1. Inferring Type From Context

Because sorted(by:) is called on an array of strings, Swift infers that the closure's parameters must be (String, String) and the return type must be Bool.


reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

2. Implicit Returns

If the closure contains only a single expression, you can omit the return keyword.


reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })

3. Shorthand Argument Names

Swift automatically provides shorthand names to inline closures, which can be used to refer to the values of the closure’s arguments by the names $0, $1, $2, and so on.


reversedNames = names.sorted(by: { $0 > $1 })

Trailing Closures

If you need to pass a closure expression to a function as the function’s final argument, you can write it as a trailing closure. A trailing closure is written after the function call’s parentheses, even though it is still an argument to the function.

// Without trailing closure
func performAction(action: () -> Void) { action() }
performAction(action: { print("Hello") })

// With trailing closure
performAction {
    print("Hello")
}

Capturing Values

A closure can capture constants and variables from the surrounding context in which it is defined. The closure can then refer to and modify those values from within its body, even if the original scope that defined the values no longer exists.

Operation Concept Benefit
Assignment let f = myFunction Simplifies passing logic around code blocks.
Injection func exec(f: () -> Void) Allows decoupling of logic (Dependency Injection).
Selection return someCondition ? f1 : f2 Dynamically chooses strategy at runtime.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    let incrementer: () -> Int = {
        runningTotal += amount // 'runningTotal' and 'amount' are captured
        return runningTotal
    }
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen()) // 10
print(incrementByTen()) // 20

Optimization Summary Table

Feature Original Syntax Optimized Syntax
Type Inference (a: Int, b: Int) -> Int (a, b)
Implicit Return { return a + b } { a + b }
Shorthand Args { a, b in a + b } { $0 + $1 }
Trailing Closure map(transform: { ... }) map { ... }

Trailing Closures & Capturing Values

As you move toward writing more advanced Swift code—especially in SwiftUI or asynchronous programming—you will find that closures are most frequently used as Trailing Closures. Additionally, understanding how closures capture values is vital for managing state and avoiding memory leaks.


Trailing Closures

A trailing closure is a closure expression that is written outside of (and after) the parentheses of the function call it belongs to. You should use this syntax whenever the closure is long enough that it would be difficult to read if nested inside the function's argument list.

Single Trailing Closure

If a closure is the only or final argument to a function, you can omit the argument label and move the closure outside the parentheses. If the closure is the only argument, you can omit the parentheses entirely.

func loadData(completion: () -> Void) {
    // Perform task...
    completion()
}

// Without trailing closure
loadData(completion: {
    print("Data loaded.")
})

// With trailing closure
loadData {
    print("Data loaded.")
}

Multiple Trailing Closures

If a function takes multiple closures, you omit the argument label for the first trailing closure and include the labels for the subsequent ones.

func downloadFile(from url: String, 
                  success: () -> Void, 
                  failure: (Error) -> Void) {
    // Logic...
}

downloadFile(from: "https://apple.com") {
    print("Success!")
} failure: { error in
    print("Error: \(error)")
}

Capturing Values

A closure can capture constants and variables from the surrounding context in which it is defined. Even if the original scope (like a function) where those variables were created has finished executing, the closure "holds onto" them so they remain available when the closure is eventually called.

How Capture Works

Swift optimizes memory by only capturing variables that are actually used within the closure. If the closure modifies a captured variable, that change is reflected in the original variable because the closure captures a reference to it.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    
    // This closure captures 'runningTotal' and 'amount'
    let incrementer: () -> Int = {
        runningTotal += amount
        return runningTotal
    }
    
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen()) // 10
print(incrementByTen()) // 20 (runningTotal was kept alive and updated)

Escaping Closures (@escaping)

By default, closures passed to functions are non-escaping, meaning they are executed during the function's body and cannot outlive it. A closure is said to escape a function when it is passed as an argument but is called after the function returns (e.g., in an asynchronous task or stored in a property).

You must mark such parameters with the @escaping attribute.

Attribute Behavior Typical Use Case
Non-escaping (Default) Called immediately; cannot be stored. map, filter, sorted
@escaping Can be stored and called later. Network requests, Timers, Completion handlers.

var completionHandlers: [() -> Void] = []

func appendHandler(handler: @escaping () -> Void) {
    // This escapes because it's stored in an array outside the function
    completionHandlers.append(handler)
}

Trailing Closure & Capture Summary

Concept Key Takeaway
Syntax Use trailing closures for the final argument to keep code clean.
Readability Omit parentheses if a trailing closure is the only argument.
Memory Closures capture variables by reference, keeping them alive.
Safety Use @escaping if the closure needs to persist after the function ends.

Warning

Capturing self (a reference to a class instance) inside a closure can lead to a strong reference cycle (memory leak). We will cover how to break these cycles using "capture lists" in the upcoming sections on Memory Management.

Escaping vs Non-Escaping Closures

In Swift, closures passed as function parameters have a specific lifecycle. By default, they are non-escaping, but they can be marked as escaping depending on when they are executed. Understanding this distinction is critical for memory management and asynchronous programming.


Non-Escaping Closures (Default)

A closure is "non-escaping" if it is called within the body of the function it was passed to, and before the function returns. The compiler knows that the closure will not outlive the function, allowing for significant performance optimizations.

  • Lifecycle: Starts and ends within the function call.
  • Memory: The closure does not need to be stored permanently.
  • Default: All closure parameters are non-escaping unless specified otherwise.
func performTask(then closure: () -> Void) {
    print("Task started.")
    closure() // Executed before the function ends
    print("Task finished.")
}

Escaping Closures (@escaping)

A closure is said to escape a function when it is passed into the function as an argument but is called after the function returns. This usually happens in two scenarios:

  1. Asynchronous Operations: The closure is stored to be called once a network request or timer finishes.
  2. Storage: The closure is assigned to a variable or property outside the function's scope.

To allow a closure to escape, you must write @escaping before the parameter's type.

var completionHandlers: [() -> Void] = []

func addHandler(completion: @escaping () -> Void) {
    // Escapes because it's stored in an array outside this function
    completionHandlers.append(completion)
}

Comparison of Closure Lifecycles

Feature Non-Escaping Escaping (@escaping)
Execution Before function returns. After function returns.
Storage Cannot be stored in external variables. Can be stored in properties/arrays.
self requirement Can use self implicitly. Must use self explicitly if in a class.
Performance Higher (optimized by compiler). Slightly lower (requires heap allocation).

Why the @escaping Requirement?

The requirement to explicitly mark closures as @escaping is a safety feature for Memory Management

  • Intent: It forces the developer to acknowledge that the closure will hang around in memory for an indeterminate amount of time.
  • Reference Cycles: Because an escaping closure is stored, it might capture self. If self also owns the closure, you create a Strong Reference Cycle (memory leak).
  • Explicit Self: Inside an escaping closure, you must write self.property instead of just property. This serves as a "warning sign" that you are capturing the current instance.
class DataManager {
    var data = ""
    
    func fetchData(completion: @escaping (String) -> Void) {
        // Simulating a network delay
        DispatchQueue.global().async {
            completion("New Data")
        }
    }
    
    func update() {
        fetchData { [weak self] result in
            // 'self' must be explicit because closure is escaping
            self?.data = result 
        }
    }
}

Summary Table: When to use @escaping

Use Case Attribute Reason
Sorting/Filtering Default Work is done immediately.
Completion Handlers @escaping Usually called after a background task.
Animation Blocks @escaping Animations run over time after the setup function exits.
Storing in Properties @escaping The closure must exist as long as the object does.

Enumerations Last updated: Feb. 25, 2026, 7:34 p.m.

test

Enum Syntax

An Enumeration (or Enum) defines a common type for a group of related values and enables you to work with those values in a type-safe way within your code. Unlike enums in other languages (like C or Objective-C), Swift enums are highly flexible and do not have to provide a value for each case. If a value (known as a "raw" value) is provided, it can be a string, a character, or a value of any integer or floating-point type.


Basic Syntax

You introduce enumerations with the enum keyword and place their entire definition within a pair of braces. You use the case keyword to introduce new enumeration cases.

enum CompassPoint {
    case north
    case south
    case east
    case west
}

// Multiple cases can appear on a single line
enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

Note

Swift enumeration cases do not have an implicit integer value by default. In the CompassPoint example above, north, south, east, and west do not implicitly equal 0, 1, 2, and 3. Instead, they are fully fledged values in their own right, with an explicit type of CompassPoint.


Initializing and Type Inference

Once a variable or constant is declared as a certain enum type, you can change its value using a shorter "dot syntax."

Step Code Example Note
Declaration var direction = CompassPoint.west Type is inferred as CompassPoint.
Reassignment direction = .north Type is already known, so the prefix is omitted.

var directionToHead = CompassPoint.west
directionToHead = .east // Swift knows the type, so we use the shorthand

Matching Enumeration Values with a Switch Statement

The most common way to interact with an enum is using a switch statement. Swift requires the switch to be exhaustive —you must handle every case defined in the enum.

directionToHead = .south

switch directionToHead {
case .north:
    print("Lots of planets have a north")
case .south:
    print("Watch out for penguins")
case .east:
    print("Where the sun rises")
case .west:
    print("Where the skies are blue")
}
// Prints "Watch out for penguins"

If it is not appropriate to provide a case for every enumeration case, you can use a default case to cover any cases that are not addressed explicitly.


Features of Swift Enums

Swift enums are more powerful than traditional enumerations because they share many features typically reserved for classes or structures:

  • Computed Properties: To provide additional information about the current value.
  • Instance Methods: To provide functionality related to the values.
  • Initializers: To provide an initial case value.
  • Extensions: To expand their functionality beyond their original definition.
  • Protocols: To provide standard functionality (like CustomStringConvertible).

Comparison: Enum vs. Other Types

Feature Enumeration Array/Set
Purpose A fixed set of choices. A collection of arbitrary data.
Safety Compiler ensures all cases are handled. Runtime checks for bounds/existence.
Structure Discrete, named states. Indexed or hashed sequences.

Associated Values

While basic enums define a set of distinct cases, Swift allows you to store additional information alongside these case values. This feature is called Associated Values. It allows you to attach custom data to each case, and this data can vary for every instance of that case.


Definition and Syntax

You define associated values by including a tuple of types next to the case name. Unlike Raw Values (which are fixed for the entire enum), associated values are set when you create a new constant or variable based on one of the enumeration’s cases.

enum Barcode {
    case upc(Int, Int, Int, Int)      // Stores 4 integers
    case qrCode(String)               // Stores a single string
}

// Creating instances with specific data
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

Extracting Associated Values

To access the information stored within a case, you use a switch statement or an if case statement. You can extract each associated value as a constant (using let) or a variable (using var) for use within the body of the case.

switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
    print("QR Code: \(productCode).")
}

Shorthand Extraction

If all the associated values for a case are extracted as constants (or all as variables), you can place a single let or var annotation before the case name for brevity.

switch productBarcode {
case let .upc(ns, manufacturer, product, check):
    print("UPC: \(ns)-\(manufacturer)-\(product)-\(check)")
case let .qrCode(code):
    print("QR: \(code)")
}

Use Cases and Benefits

Associated values are incredibly powerful for modeling data that can exist in multiple, mutually exclusive states, each requiring different metadata.

Scenario Case A (Data) Case B (Data)
Network Request .success(Data) .failure(Error)
User Status .loggedIn(username: String) .loggedOut
Shape .circle(radius: Double) .rectangle(width: Int, height: Int)

Comparison: Associated Values vs. Raw Values

It is a common point of confusion, but a Swift enum cannot have both raw values and associated values simultaneously.

Feature Raw Values Associated Values
Nature Fixed, predefined values. Dynamic, set at initialization.
Type Must be same for all cases (e.g., Int). Can be different for every case.
Storage Stored as part of the Type definition. Stored as part of the Instance.
Access Via .rawValue property. Via switch or if case pattern matching.

Pattern Matching with if case

If you only care about one specific case and its associated value, you can use if case let to avoid a full switch statement.

if case let .qrCode(code) = productBarcode {
    print("The QR code is \(code)")
}

Raw Values

As an alternative to associated values, enumeration cases can come prepopulated with default values (called raw values), which are all of the same type. These act as fixed identifiers for each case, similar to constants assigned to an enum in other programming languages.


Raw Value Syntax

You specify the type of the raw values by placing the type name after the enumeration’s name. Raw values can be strings, characters, or any integer or floating-point number type.

enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

Important

Unlike associated values, raw values are set when you first define the enumeration in your code. Every instance of a particular case will always have the same raw value.


Implicitly Assigned Raw Values

When you work with enumerations that store integer or string raw values, Swift can automatically assign values for you so that you don’t have to explicitly type them for every case.

Raw Value Type Implicit Behavior Example
Integer Values increment by 1 from the previous case (or start at 0). case mercury = 1 makes Venus 2, Earth 3.
String The raw value for each case is the text of that case's name. case north has a raw value of "north".
enum Planet: Int {
    case mercury = 1, venus, earth, mars // venus is 2, earth is 3...
}

enum CompassPoint: String {
    case north, south, east, west // .south rawValue is "south"
}

You access the raw value of an enumeration case with its rawValue property:

let earthOrder = Planet.earth.rawValue // 3


Initializing from a Raw Value

If you define an enumeration with a raw-value type, the enumeration automatically receives an initializer that takes a value of the raw value’s type (as a parameter called rawValue) and returns either an enumeration case or nil.

Because not every possible raw value will match an enum case, this initializer is a failable initializer, meaning it returns an Optional.

let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type 'Planet?' and equals Planet.uranus

let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
    print("Planet at position 11 exists.")
} else {
    print("There isn't a planet at position 11.")
}

Summary: Raw Values vs. Associated Values

Feature Raw Values Associated Values
Definition Part of the declaration. Part of the instance creation.
Consistency Same for every instance of a case. Unique to each instance created.
Type Single type for all cases (Int, String). Different types for different cases.
Retrieval Accessed via .rawValue. Accessed via switch or if case.

Recursive Enumerations

A recursive enumeration is an enumeration that has another instance of the enumeration as the associated value for one or more of its cases. This is particularly useful for modeling data structures that have a naturally nested or hierarchical nature, such as mathematical expressions or file systems.


The indirect Keyword

Because enumerations are value types, the compiler needs to know exactly how much memory an enum will occupy. A recursive enum could theoretically be infinite in size, so Swift requires you to use the indirect keyword. This tells the compiler to store the associated value behind a layer of indirection (a pointer), giving the enum a fixed size.

You can apply indirect in two ways:

  1. Before a specific case: If only some cases are recursive.
  2. Before the entire enum: If you want all cases to support recursion.

Example: Arithmetic Expressions

Consider an expression that can be a simple number, an addition of two expressions, or a multiplication of two expressions.

// Marking the entire enum as indirect
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

// Alternatively, marking specific cases:
enum ArithmeticExpressionAlt {
    case number(Int)
    indirect case addition(ArithmeticExpressionAlt, ArithmeticExpressionAlt)
    indirect case multiplication(ArithmeticExpressionAlt, ArithmeticExpressionAlt)
}

Working with Recursive Enums

To evaluate or process a recursive enumeration, you typically use a recursive function. This function uses a switch statement to break down the nested layers until it reaches a base case (a non-recursive case).

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case let .number(value):
        return value
    case let .addition(left, right):
        return evaluate(left) + evaluate(right) // Recursive call
    case let .multiplication(left, right):
        return evaluate(left) * evaluate(right) // Recursive call
    }
}

print(evaluate(product)) // Prints "18" ((5 + 4) * 2)

Comparison: Recursive Enums vs. Linked Lists

Feature Recursive Enum Class-based Linked List
Type Category Value Type (with indirection). Reference Type.
Primary Use Tree structures, Math expressions. Linear sequences, Queues.
Memory Efficiently managed by Swift. Subject to ARC (Automatic Reference Counting).
Safety Pattern matching ensures all cases are handled. Requires manual null-checks (Optionals).

Summary of Recursive Usage

  • Purpose: To define types that can contain instances of themselves.
  • Syntax: Use the indirect keyword to enable pointer-based storage.
  • Logic: Best paired with recursive functions and pattern matching.
  • Common Examples: Binary trees, JSON structures, and mathematical formulas.

Structures and Classes Last updated: Feb. 25, 2026, 7:34 p.m.

test

Comparing Structs vs Classes

Structures and classes are the building blocks of your program’s code. They allow you to define custom data types with properties and methods. However, they have a fundamental difference in how they are stored and passed around in your app: Structures are Value Types, while Classes are Reference Types .


Shared Capabilities

Both structures and classes in Swift share many common features that make them versatile for modeling data:

  • Properties: Store values to provide data.
  • Methods: Provide functionality to the type.
  • Subscripts: Provide access to their values using square bracket syntax.
  • Initializers: Set up their initial state.
  • Extensions: Expand functionality beyond a default implementation.
  • Protocols: Conform to standards to provide specific behavior.

Key Differences

While they share many traits, classes have additional capabilities that structures do not. These extra features come at the cost of increased complexity.

Feature Structure (struct) Class (class)
Type Category Value Type Reference Type
Inheritance No Yes (One class can inherit from another)
Type Casting No Yes (Check and interpret type at runtime)
Deinitializers No Yes (Free up assigned resources)
Reference Counting No Yes (Allows more than one reference)
Default Initializer Memberwise initializer provided Must define own initializer

Value Types vs. Reference Types

The most significant difference is how they behave when you assign them to a new variable or pass them into a function.

1. Structures (Value Types)

A value type is a type whose value is copied when it’s assigned to a variable or constant, or when it’s passed to a function. Changing the copy does not change the original.

struct Resolution {
    var width = 0
    var height = 0
}

let hd = Resolution(width: 1920, height: 1080)
var vga = hd // A full copy is made here
vga.width = 640

print(hd.width)  // 1920 (Original remains unchanged)
print(vga.width) // 640

2. Classes (Reference Types)

Reference types are not copied when they are assigned to a variable or constant. Instead, a reference to the same existing instance is used. Multiple variables can point to the same object in memory.

class VideoMode {
    var resolution = Resolution()
    var frameRate = 0.0
}

let tenEighty = VideoMode()
tenEighty.frameRate = 25.0

let alsoTenEighty = tenEighty // No copy; both point to the same object
alsoTenEighty.frameRate = 30.0

print(tenEighty.frameRate) // 30.0 (The original instance was updated)

Identity Operators

Because multiple constants and variables can refer to the same single instance of a class, Swift provides two identity operators to check if two variables refer to the exact same instance:

  • Identical to (===)
  • Not identical to (!==)

Note

"Identical to" (===) is not the same as "Equal to" (==). Identical means the two variables point to the same memory address. Equal means the two instances contain the same data values.


Choosing Which to Use

Apple generally recommends using Structures by default because they are safer and easier to reason about in a multithreaded environment.

Use a Structure if... Use a Class if...
You want to model simple data values. You need to use inheritance.
You want the data to be copied when passed. You need to model "identity" (one shared source of truth).
The data doesn't need to be modified by multiple parts of the app. You need to manage the lifecycle of an object (via deinitializers).

Value Types vs Reference Types

Understanding the distinction between Value Types and Reference Types is perhaps the most fundamental concept in Swift development. It determines how data is stored in memory, how it is passed between different parts of your app, and how you manage state.


Value Types (Structs, Enums, Tuples)

A Value Type is a type whose value is copied when it is assigned to a variable or constant, or when it is passed to a function. Each instance maintains a unique copy of its data.

Characteristics of Value Types:

  • Memory Allocation: Usually stored on the Stack, making them very fast to create and destroy.
  • Thread Safety: Inherently thread-safe because each thread gets its own copy of the data; modifications in one place cannot affect another.
  • Predictability: Since they are copied, you don't have to worry about a function "behind the scenes" changing your local data.
struct Point {
    var x: Int
    var y: Int
}

var pointA = Point(x: 10, y: 10)
var pointB = pointA  // A brand new copy is created here

pointB.x = 50

print(pointA.x) // 10 (Remains unchanged)
print(pointB.x) // 50 (Only the copy changed)

Reference Types (Classes, Functions, Closures)

A Reference Type is not copied when assigned or passed. Instead, a reference (a pointer to the memory address) to the same existing instance is used.

Characteristics of Reference Types:

  • Memory Allocation: Stored on the Heap. The variable itself only stores the "address" of where the object lives.
  • Shared State: Multiple variables can point to, and modify, the exact same instance.
  • Lifecycle Management: Swift uses Automatic Reference Counting (ARC) to track how many references exist. The memory is only freed when the reference count drops to zero.
class Car {
    var color: String = "Red"
}

let myCar = Car()
let yourCar = myCar // Both variables now point to the SAME instance

yourCar.color = "Blue"

print(myCar.color)   // "Blue" (The original was modified!)
print(yourCar.color) // "Blue"

Side-by-Side Comparison

Feature Value Types (Struct/Enum) Reference Types (Class)
Assignment Copy: Creates a new instance. Reference: Shares the same instance.
Storage Primarily Stack. Primarily Heap.
Efficiency Faster (low overhead). Slower (requires ARC and heap management).
Mutability Controlled by let vs var. Properties can change even if the variable is let.
Identity No concept of "identity." Identifiable via memory address (===).

The mutating Keyword in Value Types

Because value types are immutable by default when declared as constants, methods that modify properties of a struct or enum must be explicitly marked with the mutating keyword. This tells Swift that the method will replace the existing instance with a new one containing the updated values.

struct Counter {
    var count = 0
    mutating func increment() {
        count += 1
    }
}

When to Use Which?

In modern Swift development (and especially in SwiftUI), the rule of thumb is: Start with a Struct.

  • Use Value Types for data models, coordinates, configuration, and small pieces of information where the data itself is what matters.
  • Use Reference Types when you need a single "source of truth" that multiple parts of your app must observe, or when you need to use inheritance.

Identity Operators (===)

Because multiple constants and variables can point to the same instance of a class in memory, simply checking for equality (==) isn't always enough. Swift provides Identity Operators specifically to determine if two references point to the exact same instance.


Identical To vs. Equal To

It is vital to distinguish between Identity and Equality.

  • Equality (==): Means two instances are "equal" or "equivalent" in value (usually by comparing their properties). This requires the type to conform to the Equatable protocol.
  • Identity (===): Means two constants or variables refer to the exact same class instance in memory (the same heap address).
Operator Name Requirement Use Case
=== Identical To Classes only Checking if two variables share the same memory address.
!== Not Identical To Classes only Checking if two variables point to different instances.

Identity in Action

In this example, even if two objects have the same property values, they are only "identical" if they are the same instance.

class Person {
    var name: String
    init(name: String) { self.name = name }
}

let personA = Person(name: "Alice")
let personB = Person(name: "Alice")
let personC = personA // personC points to the same instance as personA

// Identity Checks
print(personA === personB) // false (Same data, but different memory locations)
print(personA === personC) // true  (Exact same instance)

// Inequality vs. Non-Identity
print(personA !== personB) // true  (They are different objects)

Why Identity Operators Don't Work on Structs

Identity operators only apply to Reference Types (classes). Since Value Types (structs and enums) are always copied when passed or assigned, they do not have a stable shared identity in the way classes do.

If you attempt to use === on two struct instances, the Swift compiler will throw an error:

Binary operator '===' cannot be applied to two 'MyStruct' operands


Summary Table

Context Use == (Equality) Use === (Identity)
Structs Yes (if Equatable) No (Compiler Error)
Enums Yes No (Compiler Error)
Classes Yes (if Equatable) Yes
Meaning "Do they have the same data?" "Are they the same object?"

Properties (Stored, Computed, Lazy)

Properties associate values with a particular class, structure, or enumeration. In Swift, properties are divided into two main categories: Stored Properties (which store constant or variable values as part of an instance) and Computed Properties (which calculate a value rather than storing it).


1. Stored Properties

A stored property is a constant or variable that is stored as part of an instance of a particular class or structure.

  • Variable Stored Properties: Defined with var.
  • Constant Stored Properties: Defined with let.
struct FixedLengthRange {
    var firstValue: Int  // Variable
    let length: Int      // Constant
}

var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
rangeOfThreeItems.firstValue = 6 // Allowed
// rangeOfThreeItems.length = 4  // Error: length is a constant

2. Lazy Stored Properties

A lazy stored property is a property whose initial value isn't calculated until the first time it is used. You indicate a lazy stored property by writing the lazy modifier before its declaration.

Requirement Description
Must use var Constant properties (let) must always have a value before initialization completes.
Complex Setup Ideal for properties whose initial value requires expensive setup.
Dependency Use when the property depends on values that aren't known until after initialization.
class DataImporter {
    var filename = "data.txt"
}

class DataManager {
    lazy var importer = DataImporter() // Only created when accessed
    var data: [String] = []
}

let manager = DataManager()
manager.data.append("Some data")
// The 'importer' instance has not yet been created.
print(manager.importer.filename) 
// 'importer' is created now.

3. Computed Properties

Classes, structures, and enumerations can define computed properties, which do not actually store a value. Instead, they provide a getter and an optional setter to retrieve and set other properties and values indirectly.

struct Point {
    var x = 0.0, y = 0.0
}

struct Rect {
    var origin = Point()
    var size = (width: 10.0, height: 10.0)
    
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}

Read-Only Computed Properties

A computed property with a getter but no setter is known as a read-only computed property. You can simplify the declaration by removing the get keyword and its braces.

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
        return width * height * depth
    }
}

Summary Table: Property Types

Requirement Description
Must use var Constant properties (let) must always have a value before initialization completes.
Complex Setup Ideal for properties whose initial value requires expensive setup.
Dependency Use when the property depends on values that aren't known until after initialization.

Property Observers (willSet, didSet)

Property observers observe and respond to changes in a property’s value. They are called every time a property’s value is set, even if the new value is the same as the property’s current value.

You can add property observers to any stored properties you define (except for lazy stored properties). You can also add them to inherited properties by overriding them within a subclass.


Types of Observers

Swift provides two specific observers to hook into the property lifecycle:

Observer Timing Context Provided
willSet Just before the value is stored. newValue (The incoming value).
didSet Immediately after the value is stored. oldValue (The previous value).

Basic Syntax

Inside the observer, Swift provides default parameter names (newValue and oldValue), though you can define your own if preferred.

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}

let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps

stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps

Use Cases and Behavior

Property observers are commonly used for updating user interfaces, logging data changes, or maintaining synchronization between related properties.

  • In-Out Parameters: If you pass a property with observers to a function as an inout parameter, the observers are always triggered. This is because the value is copied back to the property at the end of the function.
  • Initialization: Observers are not called when a property is set in an initializer. They only trigger when the value is assigned after initialization is complete.
  • Computed Properties: You do not need observers for computed properties because you can put that logic directly into the set block of the property itself.

Comparison: Computed Property set vs. Property Observer didSet

Feature Computed Property set Property Observer didSet
Storage Does not store a value. Attached to a stored value.
Trigger When the property is assigned. When the stored value changes.
Primary Goal To update other state based on input. To react to a change in this state.

Practical Example: UI Updates

In app development, didSet is frequently used to refresh the screen whenever data changes.

var score: Int = 0 {
    didSet {
        scoreLabel.text = "Score: \(score)"
        print("UI Updated to show \(score)")
    }
}

Caution

Updating a property inside its own didSet observer is allowed, but be careful not to create an infinite loop. Assigning a value to a property within its own didSet will trigger the observer again, though Swift has built-in protections to prevent recursion in simple cases.

Property Wrappers (@propertyWrapper)

A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. It allows you to write the management logic (like validation, transformation, or persistence) once and reuse it across multiple properties by "wrapping" them.


Basic Structure

To create a property wrapper, you define a structure, enumeration, or class with the @propertyWrapper attribute. The only strict requirement is that it must contain a property named wrappedValue.

@propertyWrapper
struct Capitalized {
    private var value: String = ""
    
    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
    
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

Using the Wrapper

Once defined, you apply the wrapper to a property by prefixing the property declaration with the name of the wrapper (using the @ symbol).

struct User {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
}

var user = User(firstName: "john", lastName: "appleseed")
print(user.firstName) // "John"
print(user.lastName)  // "Appleseed"

Projected Values

In addition to the wrappedValue, a property wrapper can expose additional functionality through a projected value. You define this by adding a property named projectedValue to the wrapper. You access the projected value by prefixing the property name with a dollar sign ($).

Feature Syntax Purpose
Wrapped Value user.firstName Accesses the primary data (The "Result").
Projected Value user.$firstName Accesses additional state/metadata (The "Tool").
@propertyWrapper
struct SmallNumber {
    private var number: Int = 0
    var projectedValue: Bool = false // Tracks if the value was clipped
    
    var wrappedValue: Int {
        get { number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
}

struct Player {
    @SmallNumber var level: Int
}

var player = Player()
player.level = 20
print(player.level)  // 12
print(player.$level) // true (The projected value tells us it was clipped)

Common Use Cases

Property wrappers are heavily used in modern Swift frameworks (like SwiftUI and Combine) to handle state management and data flow.

Wrapper Example Common Purpose
@State Manages local storage for SwiftUI views.
@AppStorage Automatically reads/writes to UserDefaults.
@Published Notifies observers when a value changes (Combine).
@Clamping Ensures a numeric value stays within a specific range.

Summary Checklist

  • Attribute: Must be marked with @propertyWrapper.
  • Property: Must have a wrappedValue.
  • Initialization: Can accept arguments (e.g., @SmallNumber(max: 10)) if an initializer is provided.
  • Reuse: Eliminates repetitive "boilerplate" code for validation or formatting.

Methods & Subscripts Last updated: Feb. 25, 2026, 7:36 p.m.

test

Instance Methods

Instance methods are functions that belong to instances of a particular class, structure, or enumeration. They support the functionality of those instances by providing ways to access and modify properties, or by providing functionality related to the instance’s purpose.


Basic Syntax

Instance methods have exactly the same syntax as functions. They are written within the opening and closing braces of the type they belong to.

class Counter {
    var count = 0
    
    // An instance method to increment the counter
    func increment() {
        count += 1
    }
    
    // An instance method that takes an argument
    func increment(by amount: Int) {
        count += amount
    }
}

let tracker = Counter()
tracker.increment()      // count is now 1
tracker.increment(by: 5) // count is now 6

The self Property

Every instance of a type has an implicit property called ,self, which is exactly equivalent to the instance itself. You use the self property to refer to the current instance within its own instance methods.

When to Use self

In most cases, you don't need to write self explicitly. Swift assumes you are referring to a property or method of the current instance. However, you must use self when a parameter name for an instance method matches the name of a property (known as name shadowing).

struct Point {
    var x = 0.0, y = 0.0
    
    func isToTheRightOf(x: Double) -> Bool {
        // 'self.x' refers to the property
        // 'x' refers to the method parameter
        return self.x > x
    }
}

Modifying Value Types (The mutating Keyword)

Structures and enumerations are value types. By default, their properties cannot be modified from within their own instance methods. If you need to modify the properties of a struct or enum, you must opt into mutating behavior for that method.

struct Point {
    var x = 0.0, y = 0.0
    
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

var somePoint = Point(x: 1, y: 1)
somePoint.moveBy(x: 2, y: 3)
// somePoint is now (3.0, 4.0)

Note

You cannot call a mutating method on a constant (let) instance of a struct, even if the method's pr


Comparing Methods by Type

Feature Class Methods Struct/Enum Methods
Modification Allowed by default. Requires mutating keyword.
self usage Refers to the class instance. Refers to the value instance.
Inheritance Can be overridden by subclasses. Cannot be inherited.
Calling Syntax instance.method() instance.method()

Assigning to self within Mutating Methods

Mutating methods can assign an entirely new instance to the implicit self property. This is a common pattern in enumerations to switch between states.

enum TriStateSwitch {
    case off, low, high
    
    mutating func next() {
        switch self {
        case .off:
            self = .low
        case .low:
            self = .high
        case .high:
            self = .off
        }
    }
}

Type Methods (static, class)

While instance methods are called on a specific instance of a type, type methods are called on the type itself. You use type methods for functionality that is relevant to the type as a whole, rather than any single instance (e.g., a factory method or a global configuration).


Defining Type Methods

You indicate type methods by writing the static keyword before the method’s func keyword. For classes, you can also use the class keyword to allow subclasses to override the superclass’s implementation.

Keyword Used In Overridable? Notes
static Structs, Enums, Classes No The "final" implementation for that type.
class Classes only Yes Allows subclasses to provide their own logic.
class Vehicle {
    // Only the Vehicle class can use this logic
    static func alert() {
        print("General vehicle alert!")
    }
    
    // Subclasses like 'Car' can override this logic
    class func engineSound() -> String {
        return "Vroom"
    }
}

class ElectricCar: Vehicle {
    override class func engineSound() -> String {
        return "Whirrr"
    }
}

Calling Type Methods

You call type methods using the type name, not an instance variable.

// Correct: Calling on the type
Vehicle.alert() 

// Incorrect: Calling on an instance
let myCar = Vehicle()
// myCar.alert() -> Compiler Error

The self Property in Type Methods

Inside a type method, the implicit self property refers to the type itself, rather than an instance of that type. This means:

  • You can call other type methods using just the method name (without the type prefix).
  • You can access type-level properties (static properties) without a prefix.
  • You cannot access instance properties or instance methods directly, because no instance exists when the type method is called.
struct LevelTracker {
    static var highestUnlockedLevel = 1
    
    static func unlock(_ level: Int) {
        if level > highestUnlockedLevel { 
            highestUnlockedLevel = level 
        }
    }
    
    static func isUnlocked(_ level: Int) -> Bool {
        return level <= highestUnlockedLevel
    }
}

When to Use Type Methods

Scenario Example
Utility/Helper Functions Double.abs(-5.0)
Factory Methods UIColor.blue (Returns a pre-configured instance)
Shared State Management Updating a global high score in a game.
Validation User.isValidEmail("test@me.com")

Summary Checklist

  • static Use for Structs, Enums, and "final" Class methods.
  • class Use in Classes when you want to allow subclasses to override the logic.
  • Syntax: Always called via TypeName.method().
  • Scope: Only has access to other static or class members of the same type.

Mutating Methods

In Swift, structures and enumerations are value types. By default, the properties of a value type cannot be modified from within its own instance methods. To allow a method to modify properties or assign a completely new instance to self, you must explicitly mark the method with the mutating keyword.

The Need for mutating

When you modify a value type, Swift essentially creates a new copy of that value with the updated data. The mutating keyword tells the compiler that this method will "mutate" (change) the instance, which affects how memory is handled.

struct Point {
    var x = 0.0, y = 0.0
    
    // This would fail without the 'mutating' keyword
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// Prints "The point is now at (3.0, 4.0)"

Mutating and Constants

You cannot call a mutating method on a constant (let) instance of a structure, even if the properties being modified are variables. This is because a constant struct is immutable in its entirety.

Instance Type Can call mutating method? Reason
var (Variable) Yes The instance is allowed to change.
let (Constant) No The entire structure is fixed in memory.
let fixedPoint = Point(x: 0, y: 0)
// fixedPoint.moveBy(x: 1, y: 1) // COMPILER ERROR

Assigning to self

A mutating method can assign an entirely new instance to the implicit self property. This effectively replaces the existing instance with a new one when the method ends.

In Structures:

struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        self = Point(x: x + deltaX, y: y + deltaY)
    }
}

In Enumerations:

Mutating methods for enumerations can set the implicit self parameter to be a different case from the same enumeration, effectively acting as a state machine.

enum TriStateSwitch {
    case off, low, high
    
    mutating func next() {
        switch self {
        case .off:
            self = .low
        case .low:
            self = .high
        case .high:
            self = .off
        }
    }
}

var ovenLight = TriStateSwitch.low
ovenLight.next() // ovenLight is now .high
ovenLight.next() // ovenLight is now .off

Summary Checklist

  • Applicability: Only required for Value Types (Structs and Enums). Classes do not need it.
  • Keyword: mutating goes before func.
  • Behavior: Allows changing properties or reassigning self.
  • Constraint: Cannot be called on constants (let).

Subscript Syntax & Usage

Subscripts are shortcuts for accessing the member elements of a collection, list, or sequence. You use them to set and retrieve values by index without needing separate methods for setting and getting. For example, you access elements in an Array instance as someArray[index] and elements in a Dictionary instance as someDictionary[key].


Subscript Syntax

Subscript definitions look similar to both instance method and computed property definitions. You write the subscript keyword, followed by input parameters and a return type. Like computed properties, subscripts can be read-write or read-only.

subscript(index: Int) -> Int {
    get {
        // Return an appropriate subscript value here
    }
    set(newValue) {
        // Perform a suitable setting action here
    }
}

If you only need a getter, you can simplify it to a read-only declaration:

subscript(index: Int) -> Int {
    // Read-only logic here
}

Basic Usage Example

The following example defines a TimesTable structure to represent a n-times-table of integers:

struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
}

let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// Prints "six times three is 18"

Subscript Options

Subscripts are not limited to a single dimension or a single parameter type. They can take any number of input parameters, and these parameters can be of any type.

  • Multiple Parameters: Useful for multidimensional grids or matrices.
  • Overloading: A single type can define multiple subscripts with different parameter types (e.g., accessing by Int index or String key).

Example: Matrix (Multidimensional Subscript)

struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows * columns)
    }
    
    subscript(row: Int, column: Int) -> Double {
        get {
            return grid[(row * columns) + column]
        }
        set {
            grid[(row * columns) + column] = newValue
        }
    }
}

var matrix = Matrix(rows: 2, columns: 2)
matrix[0, 1] = 1.5 // Sets the value at row 0, column 1

Type Subscripts

You can also define subscripts that are called on the type itself rather than on an instance. You indicate a type subscript by writing the static keyword before the subscript keyword.

enum Planet: Int {
    case mercury = 1, venus, earth, mars
    
    static subscript(n: Int) -> Planet {
        return Planet(rawValue: n)!
    }
}

let mars = Planet[4]
print(mars) // Prints "mars"

Summary Checklist

Feature Details
Keyword subscript
Parameters Can take any number/type; labels are omitted by default in calls.
Return Type Must always define a return type.
Mutability Can be read-write (get / set) or read-only.
Type Level Use static subscript for type-level access.

Type Subscripts

Just as you can define type methods and type properties that belong to the type itself rather than an instance, Swift allows you to define Type Subscripts. These are subscripts called directly on the class, structure, or enumeration name.


Defining Type Subscripts

To define a type subscript, you write the static keyword before the subscript keyword. For classes, you can use the class keyword instead of static if you want to allow subclasses to override the subscript’s implementation.

enum CompassPoint: Int {
    case north = 1, south, east, west
    
    // Type subscript allows accessing cases by raw value directly on the Enum type
    static subscript(n: Int) -> CompassPoint? {
        return CompassPoint(rawValue: n)
    }
}

// Usage: Called on the type name 'CompassPoint'
if let direction = CompassPoint[2] {
    print("Direction is \(direction)") // Prints "south"
}

Use Cases for Type Subscripts

Type subscripts are most commonly used as factory-style lookups or for global configuration shortcuts where creating an instance isn't necessary.

Scenario Example
Enum Lookups Retrieving an enum case from a raw value or index.
Global Cache Storing and retrieving data from a shared global static dictionary.
Configuration Accessing environment variables or app settings via Settings["theme"].
Custom Collections Creating a singleton-like interface for a data store.

Static vs. Class Subscripts

Similar to methods, the distinction between static and class only applies to classes.

  • static subscript Cannot be overridden by subclasses. Used in Structs, Enums, and Classes.
  • class subscript Can be overridden by a subclass to provide a different implementation. Used in Classes only.
class Database {
    static var cache: [String: String] = [:]
    
    // Subclasses cannot change how this lookup works
    static subscript(key: String) -> String? {
        return cache[key]
    }
    
    // Subclasses CAN override this behavior
    class subscript(index: Int) -> String {
        return "Base implementation"
    }
}

Comparison: Instance vs. Type Subscripts

Feature Instance Subscript Type Subscript (static / class)
Called On The instance (e.g., myObject[0]). The type name (e.g., MyType[0]).
Access Can access instance properties. Can only access other type properties/methods.
Storage Usually acts on instance-specific data. Acts on global or shared type-level data.
Keyword subscript static subscript or class subscript.

Summary Checklist

  • Syntax: Prefix with static or class.
  • Logic: Can be read-write (get/set) or read-only.
  • Overloading: You can have multiple type subscripts with different parameter types on the same type.
  • Accessibility: Provides a clean, concise API for type-level data retrieval.

Inheritance & Initialization Last updated: Feb. 25, 2026, 7:46 p.m.

test

Inheritance & Overriding

Inheritance is a fundamental behavior that differentiates classes from other types in Swift. It allows one class to inherit the characteristics (properties, methods, and other features) of another class. The class that inherits is called a subclass, and the class it inherits from is known as its superclass.


Base Classes

A class that does not inherit from another class is known as a base class. Unlike other languages (like Objective-C), Swift classes do not require a universal base class (like NSObject). Any class you define without specifying a superclass automatically becomes a base class.

class Vehicle {
    var currentSpeed = 0.0
    var description: String {
        return "traveling at \(currentSpeed) mph"
    }
    func makeNoise() {
        // do nothing - an arbitrary vehicle doesn't necessarily make a noise
    }
}

Subclassing

Subclassing is the act of basing a new class on an existing class. The subclass inherits characteristics from the existing class, which you can then refine or add to. To indicate subclassing, write the subclass name before the superclass name, separated by a colon.

class Bicycle: Vehicle {
    var hasBasket = false
}

let bicycle = Bicycle()
bicycle.currentSpeed = 15.0 // Inherited property
print("Bicycle: \(bicycle.description)")

Overriding

A subclass can provide its own custom implementation of an instance method, type method, instance property, type property, or subscript that it would otherwise inherit from a superclass. This is known as overriding

To override a characteristic, you must prefix your overriding definition with the override keyword. This prompts the Swift compiler to check that the superclass actually has a matching definition to be overridden.

1. Overriding Methods

class Train: Vehicle {
    override func makeNoise() {
        print("Choo Choo")
    }
}

2. Overriding Properties

You can override an inherited property to provide your own custom getter and setter, or to add property observers (willSet/didSet) so that the overriding property can respond when the underlying property value changes.

class Car: Vehicle {
    var gear = 1
    override var description: String {
        return super.description + " in gear \(gear)"
    }
}

Accessing Superclass Members

When you provide an override, it is often useful to use the existing superclass implementation as part of your override. You access the superclass version of a method, property, or subscript by using the super prefix:

  • super.someMethod()
  • super.someProperty
  • super[index]

Preventing Overrides

You can prevent a method, property, or type subscript from being overridden by marking it as final Any attempt to override a final member in a subclass will result in a compile-time error. You can also mark an entire class as final to prevent it from being subclassed at all.

Keyword Level Effect
final func Method Subclasses cannot override this method.
final var Property Subclasses cannot override this property.
final class Class The class cannot be inherited from (leaf class).

Inheritance Summary

Feature Description
Inheritance Only available to Classes (Reference Types).
override Required when redefining an inherited member.
super Used to call the superclass's original implementation.
final Restricts further inheritance or overriding for safety/performance.

Preventing Overrides (final)

While inheritance allows for flexibility, there are times when you want to ensure that a specific method, property, or an entire class cannot be changed or extended by a subclass. In Swift, you achieve this using the final keyword.


Why Use final?

Using final serves two primary purposes:

  1. Safety & Intent: It explicitly communicates to other developers that a piece of logic is complete and should not be altered. It prevents accidental bugs caused by overriding critical logic in subclasses.
  2. Performance: The Swift compiler can optimize your code better when it knows a method won't be overridden. It can use Static Dispatch (calling the method directly) instead of Dynamic Dispatch (looking up the method in a table at runtime), which is slightly faster.

Level of Restriction

You can apply the final modifier at different levels of your class hierarchy.

Keyword Placement Result
final class Before class name The class cannot be subclassed. All members become implicitly final.
final func Before a method Subclasses can inherit the class, but cannot override this specific method.
final var Before a property Subclasses cannot override the getter, setter, or observers of this property.
final subscript Before a subscript Subclasses cannot override the subscript logic.

Examples in Code

Preventing Class Inheritance

If you have a utility class or a specific manager that should never be specialized, mark the whole class as final.

final class DatabaseManager {
    func connect() {
        print("Connected to secure DB.")
    }
}

// Error: Inheritance from a final class 'DatabaseManager'
// class CloudManager: DatabaseManager { }

Preventing Specific Overrides

You might allow a class to be subclassed but want to protect a specific method that handles sensitive logic, like authentication or data validation.

class UserAccount {
    var username: String = ""
    
    // Subclasses can't change how authentication is triggered
    final func authenticate() {
        print("Security check for \(username)...")
    }
    
    func updateProfile() {
        print("Profile updated.")
    }
}

class AdminAccount: UserAccount {
    // This is allowed
    override func updateProfile() {
        print("Admin profile updated with elevated privileges.")
    }
    
    // Error: Instance method overrides a 'final' instance method
    // override func authenticate() { } 
}

Comparison: final vs. static

It is common to confuse final with static. While both prevent certain types of changes, they operate differently:

Feature final static
Context Class members (instance or type). Type-level members only.
Overriding Prevents a subclass from override. Prevents overriding (it's implicitly final).
Calling Called on an instance (unless on a type property). Always called on the Type Name.

Summary Checklist

  • Use final when your class design is "complete" and specialization would lead to errors.
  • Performance is improved because the compiler bypasses the dynamic dispatch table.
  • Static properties/methods are implicitly final and do not need the keyword.
  • Errors are caught at compile-time if a developer attempts to bypass a final restriction.

Initialization (init)

Initialization is the process of preparing an instance of a class, structure, or enumeration for use. This involves setting an initial value for each stored property on that instance and performing any other setup or initialization required before the new instance is ready for use.


Initializers

Initializers are like special methods that can be called to create a new instance of a particular type. Unlike methods, initializers do not return a value. Their primary role is to ensure that all properties of an instance are valid before it is used.

Syntax

You define an initializer using the init keyword:

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")

Customizing Initialization

You can provide initialization parameters as part of an initializer’s definition to define the types and names of values that customize the initialization process.

Initialization Parameters

struct Celsius {
    var temperatureInCelsius: Double
    
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)

Memberwise Initializers for Structures

Structure types automatically receive a memberwise initializer if they do not define any of their own custom initializers. Unlike a default initializer, the structure receives a memberwise initializer even if it has stored properties that do not have default values.

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

Initializer Delegation

Initializers can call other initializers to perform part of an instance’s initialization. This process, known as initializer delegation, avoids duplicating code across multiple initializers.

Type Category Delegation Rules
Value Types (Structs/Enums) Can only delegate to other initializers from the same type (self.init).
Class Types Can delegate to other initializers in the same class OR to a superclass (super.init).

Two-Phase Initialization in Classes

Swift’s class initialization is a two-phase process. In the first phase, each stored property is assigned an initial value by the class that introduced it. In the second phase, each class is given the opportunity to customize its stored properties further before the instance is considered ready for use.

Safety Checks

  1. A designated initializer must ensure that all properties introduced by its class are initialized before it delegates up to a superclass initializer.
  2. A designated initializer must delegate up to a superclass initializer before assigning a value to an inherited property.
  3. A convenience initializer must delegate to another initializer before assigning a value to any property.

Summary Checklist

  • Property Coverage: Every stored property must have a value by the end of initialization (unless it's an Optional).
  • Structs: Get free memberwise initializers if no custom ones are defined.
  • Classes: Require explicit initializers if properties don't have default values.
  • Failable Initializers: Use init? if the initialization could fail and return nil.

Default Initializers & Memberwise Initializers

Swift provides automatic initializers for structures and classes that meet certain criteria. These allow you to create new instances without writing any custom init() code yourself.


1. The Default Initializer

Swift provides a default initializer for any class or structure that provides default values for all of its properties and does not provide at least one initializer itself. This default initializer simply creates a new instance with all of its properties set to their default values.

class ShoppingListItem {
    var name: String?    // Automatically nil
    var quantity = 1     // Default value 1
    var purchased = false // Default value false
}

// Uses the default initializer
var item = ShoppingListItem()

2. The Memberwise Initializer (Structs Only)

Structure types receive a memberwise initializer even if their stored properties do not have default values (as long as they haven't defined any custom initializers). This is a powerful feature unique to structures that allows you to initialize all properties of a new instance by name.

Behavior and Rules

  • Automatic Generation: Swift creates this for you automatically.
  • Partial Defaults: If some properties have default values, the memberwise initializer will still include them as parameters, often allowing you to omit them if you wish (using the default).
  • Loss of Generation: As soon as you define a custom initializer inside the struct’s main declaration, you lose the automatic memberwise initializer.
struct Size {
    var width = 0.0
    var height = 0.0
}

// 1. Full initialization
let twoByTwo = Size(width: 2.0, height: 2.0)

// 2. Partial initialization (if defaults are present)
let defaultHeight = Size(width: 5.0)

Retaining Memberwise Initializers

If you want to keep the automatic memberwise initializer but also provide your own custom initializer, define your custom initializer in an extension rather than the main definition of the structure.

struct Point {
    var x = 0.0, y = 0.0
}

extension Point {
    init(atOrigin: Bool) {
        if atOrigin {
            self.init(x: 0.0, y: 0.0)
        } else {
            self.init(x: 1.0, y: 1.0)
        }
    }
}

// Both are now available:
let p1 = Point(x: 5, y: 5) // Memberwise
let p2 = Point(atOrigin: true) // Custom

Comparison: Default vs. Memberwise

Feature Default Initializer Memberwise Initializer
Applicability Classes and Structures. Structures only.
Requirement All properties must have default values. Properties do not need default values.
Parameters Takes no arguments (()). Takes arguments for every property.
Custom Initializers Lost if any init is defined. Lost if any init is defined in main body.

Summary Table

Scenario Resulting Initializer
Class with all default values & no custom init. init() (Default).
Struct with no default values & no custom init. init(prop1:prop2:) (Memberwise).
Struct with all default values & no custom init. Both init() and init(prop1:prop2:).
Any type with a custom init in the main body. No automatic initializers provided.

Initializer Delegation & Inheritance

In Swift, classes have strict rules for how initializers call one another to ensure that every property is fully initialized before use. This process is known as Initializer Delegation.


Class Initializer Types

To simplify the complex process of inheritance, Swift distinguishes between two types of initializers for classes:

Initializer Type Purpose Responsibility
Designated The "primary" initializer. Must fully initialize all properties introduced by its class and call a superclass initializer.
Convenience A "secondary" supporting initializer. Must call a designated initializer from the same class (ultimately).

Three Rules of Delegation

Swift applies three rules to ensure that delegation across a class hierarchy is safe and predictable:

  1. Rule 1: A designated initializer must call a designated initializer from its immediate superclass.
  2. Rule 2: A convenience initializer must call another initializer from the same class.
  3. Rule 3: A convenience initializer must ultimately call a designated initializer.

Mnemonic

Designated initializers always delegate up; convenience initializers always delegate across.


Two-Phase Initialization

To prevent property access before the instance is ready, Swift uses a two-phase initialization process:

Phase 1: Working Up the Chain

  • The designated initializer is called.
  • Memory for the instance is allocated, but not yet initialized.
  • The designated initializer confirms that all stored properties introduced by its class have an initial value.
  • It hands off to the superclass initializer to do the same for its own properties.
  • This continues up the chain to the base class.

Phase 2: Working Down the Chain

  • Once the top of the chain is reached, the instance is considered fully initialized.
  • Each designated initializer in the chain now has the opportunity to customize the instance further.
  • Initializers can now use self, access properties, and call instance methods.

Initializer Inheritance and Overriding

Unlike subclasses in Objective-C, Swift subclasses do not inherit their superclass initializers by default. This prevents a situation where a simple superclass initializer is used to create a subclass, leaving the subclass's specific properties uninitialized.

Automatic Initializer Inheritance

Subclasses can inherit superclass initializers automatically if:

  • Rule A: The subclass doesn't define any designated initializers of its own. It inherits all of the superclass’s designated initializers.
  • Rule B: The subclass provides an implementation of all its superclass’s designated initializers (either by inheriting them via Rule A or by overriding them). It then inherits all of the superclass’s convenience initializers.
class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheels"
    }
}

class Bicycle: Vehicle {
    override init() { // Overriding the default init
        super.init()
        numberOfWheels = 2
    }
}

Summary Checklist

  • Designated init: The main entry point; calls super.init().
  • Convenience init: Marked with the convenience keyword; calls self.init().
  • super.init() Must be called after initializing the subclass's own properties but before modifying inherited properties.
  • Overriding Use override when a subclass designated initializer matches a superclass designated initializer.

Failable Initializers (init?)

Sometimes, the initialization of an instance can fail. This might be due to invalid input parameters, the absence of a required external resource, or a condition that prevents the object from reaching a valid state. In Swift, you handle these scenarios using Failable Initializers.


Syntax and Behavior

A failable initializer is defined by placing a question mark after the init keyword (init?). It creates an optional instance of the type it initializes.

  • Success: If initialization succeeds, the instance is returned.
  • Failure: You trigger a failure by writing return nil within the initializer.
struct Animal {
    let species: String
    
    init?(species: String) {
        if species.isEmpty { 
            return nil // Initialization fails if string is empty
        }
        self.species = species
    }
}

let dog = Animal(species: "Canine") // Returns an Animal? (Optional)
let anonymous = Animal(species: "") // Returns nil

Failable Initializers for Enumerations

Enumerations are common candidates for failable initializers, especially when mapping raw values to cases where the raw value might not match any valid case.

Note

Enumerations with raw values automatically receive a failable initializer, init?(rawValue:), provided by Swift.

enum TemperatureUnit {
    case kelvin, celsius, fahrenheit
    
    init?(symbol: Character) {
        switch symbol {
        case "K": self = .kelvin
        case "C": self = .celsius
        case "F": self = .fahrenheit
        default: return nil
        }
    }
}

Propagation of Failure

Failable initializers can delegate to other initializers across a class or struct hierarchy.

Delegation Type Behavior
Across (Same type) A failable init? can delegate to another init? or a non-failable init.
Up (Superclass) A subclass init? can call a superclass init?. If the superclass fails, the whole process fails immediately.
Non-failable to Failable A non-failable init cannot delegate to a failable init? (because it cannot return nil).

Overriding Failable Initializers

You have flexibility when overriding failable initializers in subclasses:

  1. Override init? with init?: Standard behavior.
  2. Override init? with init: You can make a failable superclass initializer non-failable in a subclass if you can guarantee success (e.g., by providing a default value).
  3. Force Unwrapping: You can call a failable superclass initializer from a non-failable subclass initializer using super.init()!, though this will trigger a runtime crash if the superclass fails.

Summary Checklist

  • Result: Always returns an Optional (Type?).
  • Trigger: Use return nil to signify a setup failure.
  • Safety: Allows you to handle invalid data gracefully at the moment of creation.
  • Requirement: In classes, all properties must be initialized (or the class must have reached a state where nil can be returned) before returning nil

Deinitialization (deinit)

A deinitializer is called immediately before a class instance is deallocated. While initializers (init) prepare an instance for use, deinitializers handle any necessary "cleanup" to ensure resources are released properly.


Key Characteristics

Deinitialization is unique to class types Because structures and enumerations are value types, they are destroyed as soon as they go out of scope, and do not use deinitializers.

Feature Details
Keyword deinit
Parameters None (cannot take arguments).
Parentheses None (written as deinit { ... }).
Manual Calling You cannot call a deinitializer yourself; Swift calls it automatically.
Availability Classes only.

How Deinitialization Works

Swift automatically deallocates your instances when they are no longer needed, to free up resources. Swift handles the memory management of instances through Automatic Reference Counting (ARC).

However, when you are working with your own resources—such as closing a file, disconnecting from a network, or returning a manually allocated buffer—you might need to perform some extra cleanup yourself.

class FileHandler {
    let filename: String
    
    init(filename: String) {
        self.filename = filename
        print("Opening file: \(filename)")
    }
    
    // This runs automatically when the instance is about to be destroyed
    deinit {
        print("Closing file: \(filename) and saving changes.")
    }
}

var handler: FileHandler? = FileHandler(filename: "data.txt")
handler = nil // This triggers the deinitializer
// Prints: "Closing file: data.txt and saving changes."

Deinitializers in Inheritance

In a class hierarchy, deinitializers are called in order from the subclass to the superclass.

  1. The subclass deinitializer is called first.
  2. Once the subclass deinitializer finishes, the superclass deinitializer is called automatically.
  3. This chain continues until the base class deinitializer is reached.

Even if a subclass doesn't provide its own deinitializer, the superclass deinitializer will still be triggered when the subclass instance is deallocated.


Practical Use Case: Managing a Shared Resource

Imagine a game where players contribute to a global bank of coins. When a player leaves the game (is deallocated), they must return their remaining coins to the bank.

class Bank {
    static var coinsInBank = 10_000
    static func receive(coins: Int) {
        coinsInBank += coins
    }
}

class Player {
    var coinsInPurse: Int
    init(coins: Int) {
        coinsInPurse = coins
    }
    
    deinit {
        // Return coins to the bank before the player object disappears
        Bank.receive(coins: coinsInPurse)
    }
}

var playerOne: Player? = Player(coins: 100)
print(Bank.coinsInBank) // 10,000
playerOne = nil         // playerOne is deallocated
print(Bank.coinsInBank) // 10,100

Summary Checklist

  • Automatic: Triggered by ARC when the reference count reaches zero.
  • State Access: Because the instance is not deallocated until after the deinitializer is called, you can still access all properties of that instance within the deinit block.
  • Cleanup: Use it for non-memory resources (sockets, files, observers).
  • Scope: Only exists in Classes.

Memory Management (ARC) Last updated: Feb. 25, 2026, 7:46 p.m.

test

Automatic Reference Counting Basics

Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, memory management "just works" in Swift, and you don’t need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.


How ARC Works

Every time you create a new instance of a class, ARC allocates a chunk of memory to store information about that instance. This memory holds the type of the instance, together with the values of any stored properties associated with that instance.

To ensure that instances don’t disappear while they are still needed, ARC tracks how many properties, constants, and variables are currently referring to each class instance.

  • Increment: When you assign a class instance to a property, constant, or variable, that variable makes a strong reference to the instance, and the reference count increases by 1.
  • Decrement: When that variable is reassigned or goes out of scope, the reference count decreases by 1.
  • Deallocation: When the reference count reaches zero, ARC deallocates the instance and calls its deinit method.

ARC in Action

The following example shows how reference counting changes as variables point to the same object.

class Person {
    let name: String
    init(name: String) { self.name = name }
    deinit { print("\(name) is being deinitialized") }
}

var reference1: Person?
var reference2: Person?

// Reference count becomes 1
reference1 = Person(name: "John Appleseed")

// Reference count becomes 2 (both point to the same instance)
reference2 = reference1 

// Reference count becomes 1
reference1 = nil 

// Reference count becomes 0 -> Deinitialization triggered
reference2 = nil 
// Prints: "John Appleseed is being deinitialized"

Strong Reference Cycles

While ARC is efficient, it is possible to write code where two class instances hold a strong reference to each other, such that each instance keeps the other alive. This is known as a strong reference cycle (or a memory leak).

In a cycle, the reference count for these instances will never drop to zero, and the memory will never be freed for the duration of your app’s life. To resolve this, Swift provides two ways to define relationships without increasing the reference count:

  1. Weak References (weak)
  2. Unowned References (unowned)

Comparison: Value Types vs. Reference Types

It is important to remember that ARC only applies to classes.

Feature Classes (Reference Types) Structs/Enums (Value Types)
Memory Management Managed by ARC (Automatic Reference Counting). Copied on assignment; no ARC needed.
Reference Count Tracked at runtime. Not applicable.
Storage Stored on the Heap. Primarily stored on the Stack.

Summary Checklist

  • Automatic: You don't manually call retain or release.
  • Strong by Default: Every variable assignment creates a strong reference unless specified otherwise.
  • Deinit: Use deinit to verify if your objects are being properly cleared from memory.
  • Classes Only: ARC does not track the lifecycle of structs or enums.

Strong Reference Cycles (Class Instances)

A strong reference cycle occurs when two class instances hold strong references to each other, preventing their reference counts from ever reaching zero. This results in a memory leak, where memory is occupied by objects that are no longer accessible or useful to the application.


How a Cycle is Created

In a typical cycle, Class A has a property that points to Class B, and Class B has a property that points back to Class A. Even if you set your external variables to nil, the internal "handshake" keeps both instances alive.

class Tenant {
    let name: String
    var apartment: Apartment? // Strong reference
    init(name: String) { self.name = name }
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    var tenant: Tenant? // Strong reference
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john: Tenant? = Tenant(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")

// Linking the two creates a cycle
john?.apartment = unit4A
unit4A?.tenant = john

// Setting variables to nil does NOT trigger deinit
john = nil
unit4A = nil
// (No print statements appear; memory is leaked)

Resolving Cycles

Swift provides two ways to resolve strong reference cycles: weak references and unowned references. These allow one instance to refer to another without keeping a "strong" hold on it, thus not increasing the reference count.

Reference Type Use Case Requirement
weak Used when the other instance has a shorter lifetime (it can become nil). Must be a variable (var) and an Optional type.
unowned Used when the other instance has the same or longer lifetime. Can be a constant or variable; must be non-optional.

Using the weak Keyword

By marking one of the properties as weak, you break the cycle. When the strong reference to the object is broken, the weak reference does not prevent ARC from deallocating the instance.

class Apartment {
    let unit: String
    weak var tenant: Tenant? // Now a weak reference
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) is being deinitialized") }
}

// Now, when john is set to nil, the Tenant instance is deallocated.
// This sets unit4A.tenant to nil automatically.

Comparison: Strong vs. Weak vs. Unowned

Property Strong (Default) Weak Unowned
Increments Ref Count Yes No No
Optionality Can be optional or not Must be Optional Must be Non-Optional*
ARC Behavior Keeps object alive Becomes nil on dealloc Crashes if accessed after dealloc
Mutability var or let Must be var var or let

*Note: Swift 5.0+ supports unowned optionals, but they are rarely used compared to weak.


Summary Checklist

  • Identify Cycles: Look for "back-references" in parent-child relationships.
  • Break Cycles: Use weak for the "child" to "parent" link.
  • Memory Safety: Remember that weak properties automatically become nil when the object they point to is freed, which is why they must be Optionals.

Weak & Unowned References

To resolve strong reference cycles, Swift provides two "non-strong" reference types: weak and unowned. These allow one instance to refer to another instance in a reference cycle without keeping a strong hold on it, allowing ARC to deallocate the instance when appropriate.


Weak References

A weak reference is a reference that does not keep a strong hold on the instance it refers to. Because a weak reference does not increment the reference count, it does not prevent ARC from deallocating the instance.

Key Rules for weak :

  • Must be Optional: Because the instance can be deallocated while the weak reference is still pointing to it, ARC automatically sets a weak reference to nil when the instance is deallocated.
  • Must be a Variable: Since the value can change to nil at runtime, you must declare them using var (not let).
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    // Break the cycle here
    weak var tenant: Person? 
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) is being deinitialized") }
}

Unowned References

Like weak references, an unowned reference does not keep a strong hold on the instance it refers to. However, an unowned reference is used when the other instance has the same lifetime or a longer lifetime than the current instance.

Key Rules for unowned

  • Non-Optional: Swift expects an unowned reference to always have a value. It does not wrap the value in an Optional.
  • Danger of Zombies: If you try to access an unowned reference after the instance it refers to has been deallocated, your app will trigger a runtime crash.
class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
}

class CreditCard {
    let number: Int
    // A card cannot exist without a customer; customer will outlive the card
    unowned let customer: Customer 
    init(number: Int, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

Choosing Between Weak and Unowned

Deciding which to use depends on the relationship and "ownership" between the two objects.

Feature weak unowned
Reference Count Does not increment. Does not increment.
Optionality Always an Optional (?). Non-optional (usually).
Lifetime Use when the other instance can die sooner. Use when the other instance dies at the same time or later.
Safety Safe (returns nil if object is gone). Unsafe (crashes if object is gone).
Mutability Must be var. Can be let or var.

Summary Table: Reference Relationships

Relationship Type Best Reference Choice Example Scenario
Independent Strong Two separate objects that don't own each other.
Parent -> Child Strong A Department "owns" many Employees.
Child -> Parent weak An Employee refers back to their Department.
Total Dependence unowned A Credit Card cannot exist without its Owner.

Unowned Optional References (Advanced)

Since Swift 5.0, you can also mark an optional reference as unowned. This behaves similarly to weak, but you are responsible for ensuring the reference is either valid or set to nil manually, as ARC will not zero it out for you in the same way it does for weak. Generally, weak is preferred for optional relationships.

Reference Cycles in Closures

A strong reference cycle can also occur if you assign a closure to a property of a class instance, and the body of that closure captures that instance. This capture occurs because closures, like classes, are reference types.

When you access a property or method of the instance inside the closure (e.g., self.someProperty), the closure "captures" self, creating a strong reference back to the object. If the object also holds a strong reference to the closure, a cycle is formed.


How the Cycle Forms

In this scenario, neither the instance nor the closure will ever be deallocated, even if all external references to the instance are set to nil.

class HTMLElement {
    let name: String
    let text: String?

    // The closure is stored in a strong property
    lazy var asHTML: () -> String = {
        // Accessing 'self' inside the closure creates a strong capture
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit { print("\(name) is being deallocated") }
}

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello world")
print(paragraph!.asHTML()) 
paragraph = nil 
// 'deinit' is NOT called because of the cycle between paragraph and asHTML

Resolving Cycles: Capture Lists

To break this cycle, you define a capture list at the beginning of the closure’s definition. A capture list declares the rules to use when capturing one or more reference types within the closure’s body.

Syntax

The capture list is written in square brackets [] before the closure's parameters and return type.

lazy var someClosure = { [unowned self, weak delegate = self.delegate] (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

Weak vs. Unowned in Closures

Just like with class-to-class relationships, the choice between weak and unowned depends on the lifetime of the objects.

Capture Type Use Case Resulting Type inside Closure
[unowned self] Use when the closure and the instance will always be deallocated at the same time. Self (Non-optional)
[weak self] Use when the captured reference might become nil at some point in the future. Self? (Optional)

Refined Example with unowned

Since the asHTML closure is essentially a part of the HTMLElement's identity, we can use unowned :

lazy var asHTML: () -> String = { [unowned self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}
// Now, setting paragraph = nil will trigger deinit.

Handling weak self

If you use [weak self], you must handle the fact that self might be nil by the time the closure executes. This is commonly done using optional binding (the "guard-let-self" pattern).

func fetchData() {
    API.request { [weak self] result in
        // Safely unwrap self
        guard let self = self else { return }
        
        // If we got here, self is valid for the rest of this scope
        self.updateUI(with: result)
    }
}

Summary Checklist

  • Reference Type: Closures are reference types, just like classes.
  • The "Self" Trap: Accessing any property or method of self inside a stored closure creates a strong capture.
  • Capture List: Use [unowned self] if the instance and closure die together.
  • Safety First: Use [weak self] and guard let if the closure might outlive the instance (common in network calls).

Capture Lists

A capture list defines the rules for how a closure should handle the variables and constants it "captures" from the surrounding scope. It is the primary tool used to break strong reference cycles between closures and class instances.


Syntax and Placement

The capture list is placed in square brackets [] at the very beginning of the closure's body, before the parameters or return type.

// Format: [capture rules] (parameters) -> returnType in
let closure = { [weak self, unowned delegate = self.delegate] (index: Int) in
    // code using self or delegate
}

How Capture Lists Work

When a closure captures a reference type (like a class instance), it creates a strong reference by default. A capture list allows you to change that reference to weak or unowned.

1. Value Type Capture (Snapshots)

If you capture a value type (like an Int or String) in a capture list, the closure takes a copy (snapshot) of that value at the moment the closure is created. Changes made to the original variable outside the closure will not affect the captured copy.

var count = 0
let closure = { [count] in 
    print("Captured count: \(count)") 
}

count = 10
closure() // Prints: "Captured count: 0" (Snapshot taken at 0)

2. Reference Type Capture (ARC Management)

For classes, the capture list determines whether the closure keeps the instance alive.

Capture Rule Reference Type Nullability Safety
Default Strong Non-optional Keeps instance alive (risk of cycles).
weak Weak Optional Returns nil if instance is deallocated.
unowned Unowned Non-optional Crashes if accessed after deallocation.

Practical Patterns

The "Guard-Let-Self" Pattern

When using [weak self], it is standard practice to use optional binding to create a temporary strong reference to self for the duration of the closure's execution. This ensures self doesn't disappear halfway through your logic.

func performAction() {
    DispatchQueue.global().async { [weak self] in
        // 1. Safely unwrap self
        guard let self = self else { return }
        
        // 2. self is now a strong reference for the rest of this scope
        print("Starting task for \(self.name)")
        self.longRunningTask()
        print("Finished task for \(self.name)")
    }
}

Naming Captured Variables

You can assign names to captured values to clarify intent or to capture specific properties of an object.

let closure = { [weak weakSelf = self] in
    weakSelf?.doSomething()
}

Comparison: Weak vs. Unowned Captures

Choice Use When... Risk
[weak self] The instance might be deallocated before the closure is called (e.g., Network requests). None (safe handling of nil).
[unowned self] The closure and the instance have the same lifetime (e.g., lazy property closure). Runtime Crash if accessed after dealloc.

Summary Checklist

  • Square Brackets: Always at the start of the closure body.
  • Breaking Cycles: Use weak or unowned to prevent memory leaks.
  • Value Types: Capture lists create snapshots for value types.
  • self Requirement: If you use a capture list to change the strength of self, you must explicitly write self. inside the closure to acknowledge the capture.

Advanced Abstractions Last updated: Feb. 25, 2026, 7:47 p.m.

test

Optional Chaining

Optional chaining is a process for querying and calling properties, methods, and subscripts on an optional that might currently be nil. If the optional contains a value, the call succeeds; if the optional is nil, the call returns nil

Multiple queries can be chained together, and the entire chain fails gracefully if any link in the chain is nil.


Basic Syntax and Behavior

You place a question mark (?) after the optional value on which you wish to call a property, method, or subscript. This is very similar to forced unwrapping (!), but with one major difference: optional chaining fails gracefully, whereas forced unwrapping triggers a runtime crash if the optional is nil.

Approach Operator Result if nil
Forced Unwrapping ! Runtime Crash ????
Optional Chaining ? Returns nil gracefully ?????
class Residence {
    var numberOfRooms = 1
}

class Person {
    var residence: Residence?
}

let john = Person()

// Forced unwrapping would crash here because residence is nil
// let roomCount = john.residence!.numberOfRooms 

// Optional chaining returns nil
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}

Key Rules of Optional Chaining

  1. Result is always Optional: Even if the property you are accessing is non-optional (like an Int), the result of the chain will be an optional (Int?) because the chain might fail.
  2. Short-Circuiting: If any part of the chain is nil, the rest of the chain is not even evaluated.
  3. Assignment via Chaining: You can use optional chaining to set a value. If the chain is broken, the assignment simply doesn't happen.

Chaining Multiple Levels

Optional chaining can be used to drill down into complex data models with multiple levels of nested properties.

class Address {
    var street: String?
}

class Residence {
    var address: Address?
}

class Person {
    var residence: Residence?
}

let john = Person()
// This chain will return an optional String (String?)
let streetName = john.residence?.address?.street

Calling Methods and Subscripts

You can also use optional chaining to call methods and access subscripts on optional values.

  • Methods: If the method returns a value, that value becomes optional. If the method returns Void, the result of the call is Void? (allowing you to check if the method was actually executed).
  • Subscripts: Place the ? before the opening bracket of the subscript.
// Method call
john.residence?.printRooms()

// Subscript access on an optional array
var testScores = ["Dave": [86, 82, 91], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91  // Succeeds
testScores["John"]?[0] = 100 // Fails gracefully (John doesn't exist)

Summary Checklist

  • Syntax: Use ? for a safe check; use ! only if you are 100% sure the value exists.
  • Return Type: The final result of any optional chain is always an optional version of the expected type.
  • Safety: Perfect for navigating JSON-like structures or deep object graphs where data might be missing.

Type Casting (is, as, as?, as!)

Type casting in Swift is a way to check the type of an instance, or to treat ? instance as a different superclass or subclass from somewhere else in its own class hierarchy. It is particularly useful when working with collections that store a variety of related types.


Type Checking with is

The type check operator (is) is used to check whether an instance is of a certain subclass type. It returns a Boolean: true if the instance is of that subclass type, and false if it is not.

let library = [
    Movie(name: "Casablanca", director: "Michael Curtiz"),
    Song(name: "Blue Suede Shoes", artist: "Elvis Presley")
]

var movieCount = 0
for item in library {
    if item is Movie {
        movieCount += 1
    }
}

Downcasting with as? and as!

A constant or variable of a certain class type may actually refer to an instance of a subclass behind the scenes. When you need to treat that instance as its actual subclass type, you must downcast it.

Because downcasting can fail, Swift provides two forms:

Operator Type Behavior
as? Conditional Returns an optional value of the type you are downcasting to. Returns nil if the cast fails.
as! Forced Attempts the downcast and force-unwraps the result. Triggers a runtime crash if the cast fails.

Example: Conditional Downcasting

for item in library {
    if let movie = item as? Movie {
        print("Movie: \(movie.name), dir. \(movie.director)")
    } else if let song = item as? Song {
        print("Song: \(song.name), by \(song.artist)")
    }
}

Upcasting with as

The upcast operator (as) is used when the cast is guaranteed to succeed, such as casting a subclass to one of its superclasses. It is also used for literal expressions and to provide type hints to the compiler.

let myMovie = Movie(name: "Citizen Kane", director: "Orson Welles")
let item = myMovie as MediaItem // Guaranteed upcast to superclass

Type Casting for Any and AnyObject

Swift provides two special types for working with non-specific types:

  • Any Can represent an instance of any type at all, including function types and value types (structs/enums).
  • AnyObject Can represent an instance of any class type
var things: [Any] = []
things.append(0)
things.append(3.14159)
things.append("hello")
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))

for thing in things {
    switch thing {
    case let someInt as Int:
        print("An integer: \(someInt)")
    case let someString as String:
        print("A string: \(someString)")
    case let movie as Movie:
        print("A movie: \(movie.name)")
    default:
        print("Something else")
    }
}

Summary Checklist

  • is Check the type (returns Bool).
  • as? Safe downcast (returns Optional). Use this in if let or guard let statements.
  • as! Dangerous downcast. Only use when you are certain the type is correct.
  • as Upcast or type hint; always succeeds.

Nested Types

Swift allows you to define types within the scope of other types. This is known as nesting. You can nest enumerations, classes, and structures within another type to reflect a natural relationship or to group related functionality together.


Purpose of Nested Types

Nesting provides several benefits for code organization:

  • Namespacing: It prevents name collisions by scoping a type within its "parent."
  • Context: It clarifies that a specific type is only intended for use in the context of the outer type.
  • Encapsulation: It keeps related logic tightly bundled together.

Example: Blackjack Card

Consider a structure representing a playing card. A card has a Suit and a Rank. Since suits and ranks are only meaningful in the context of a card, we can nest them inside the BlackjackCard struct.

struct BlackjackCard {
    // Nested Suit enumeration
    enum Suit: Character {
        case spades = "?", hearts = "?", diamonds = "?", clubs = "?"
    }

    // Nested Rank enumeration
    enum Rank: Int {
        case two = 2, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king, ace
        
        // Even further nesting!
        struct Values {
            let first: Int, second: Int?
        }
        
        var values: Values {
            switch self {
            case .ace:
                return Values(first: 1, second: 11)
            case .jack, .queen, .king:
                return Values(first: 10, second: nil)
            default:
                return Values(first: self.rawValue, second: nil)
            }
        }
    }

    let rank: Rank, suit: Suit
}

Referring to Nested Types

To use a nested type outside of its definition context, you prefix its name with the name of the type it is nested within.

Usage Context Syntax Example
Internal (Inside parent) let s = Suit.hearts
External (Outside parent) let suit = BlackjackCard.Suit.spades
let aceOfSpades = BlackjackCard(rank: .ace, suit: .spades)
print("Suit is \(aceOfSpades.suit.rawValue)")
// Prints "Suit is ?"

Comparison: Global vs. Nested

Feature Global Type Nested Type
Visibility Visible throughout the module. Scoped to the outer type.
Naming Needs a unique, often long name (e.g., CardSuit). Can use simple names (e.g., Suit).
Organization Can lead to a cluttered global namespace. Keeps the global namespace clean.

Summary Checklist

  • Nesting Levels: You can nest types multiple levels deep (e.g., Outer.Middle.Inner).
  • Access Control: Nested types follow standard access modifiers (private, internal, public).
  • Value vs Reference: You can nest any type within any other (e.g., a class inside a struct).

Extensions

Extensions add new functionality to an existing class, structure, enumeration, or protocol type. This includes the ability to extend types for which you do not have access to the original source code (known as retroactive modeling).


Capabilities of Extensions

Extensions in Swift are powerful because they can add several types of functionality to an existing type:

Feature Global Type Nested Type
Visibility Visible throughout the module. Scoped to the outer type.
Naming Needs a unique, often long name (e.g., CardSuit). Can use simple names (e.g., Suit).
Organization Can lead to a cluttered global namespace. Keeps the global namespace clean.

Note

Extensions can add new functionality to a type, but they cannot override existing functionality. They also cannot add stored properties or property observers.


Computed Properties in Extensions

You can use extensions to add computed instance properties and computed type properties to existing types.

extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
}

let oneMeter = 1.0.m
let fitnessTarget = 5.km
print("Target distance is \(fitnessTarget) meters") 
// Prints "Target distance is 5000.0 meters"

Initializers in Extensions

Extensions can add new initializers to a type.

  • For Structs: If you add an initializer in an extension, the struct will still keep its automatically generated memberwise initializer, provided the original properties have default values.
  • For Classes: Extensions can add new convenience initializers, but they cannot add new designated initializers. Designated initializers must always be provided by the original class implementation.
struct Size {
    var width = 0.0, height = 0.0
}

extension Size {
    init(side: Double) {
        self.init(width: side, height: side)
    }
}

let square = Size(side: 10.0)
let rectangle = Size(width: 5.0, height: 10.0) // Memberwise still available!

Methods in Extensions

Extensions can add new instance methods and type methods to existing types. For value types (structs and enums), methods that modify self must be marked asmutating .

extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..<self {
            task()
        }
    }
    
    mutating func square() {
        self = self * self
    }
}

3.repetitions {
    print("Hello!")
}

var someInt = 4
someInt.square() // someInt is now 16

Protocol Conformance

One of the most common uses for extensions is to group code that handles protocol requirements. This keeps the original type definition clean and organizes the protocol-specific logic in one place.

extension MyType: Equatable {
    static func == (lhs: MyType, rhs: MyType) -> Bool {
        return lhs.id == rhs.id
    }
}

Summary Checklist

  • Retroactive: You can extend built-in types like String, Int, or Array.
  • Clean Code: Use extensions to group related functionality or protocol implementations.
  • Limitations: No stored properties and no overriding of existing methods.
  • Syntax: Use the extension keyword followed by the type name.

Protocols & Delegation

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements.


Protocol Syntax

You define a protocol similarly to a class or structure. To make a type conform to a protocol, you list the protocol name after the type’s name, separated by a colon.

protocol Identifiable {
    var id: String { get set }
    func logIdentifier()
}

struct User: Identifiable {
    var id: String
    func logIdentifier() {
        print("User ID is: \(id)")
    }
}

Property Requirements

A protocol can require any conforming type to provide an instance property or type property with a particular name and type. The protocol doesn't specify whether the property should be stored or computed—it only specifies the required name, type, and gettable/settable requirements.

Requirement Syntax Description
Read-Write { get set } Must be a variable (var); cannot be a constant or read-only computed property.
Read-Only { get } Can be satisfied by any kind of property (stored or computed, let or var).

The Delegation Pattern

Delegation is a design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type. This pattern is implemented by defining a protocol that encapsulates the delegated responsibilities.

How it works:

  1. The Protocol: Defines what the delegate is capable of doing.
  2. The Delegator: Has a property (usually weak) of the protocol type.
  3. The Delegate: Conforms to the protocol and handles the logic.
// 1. The Protocol
protocol PrinterDelegate: AnyObject {
    func didFinishPrinting(jobName: String)
}

// 2. The Delegator
class Printer {
    weak var delegate: PrinterDelegate?
    
    func printJob(name: String) {
        print("Printing \(name)...")
        // Hand off the notification to the delegate
        delegate?.didFinishPrinting(jobName: name)
    }
}

// 3. The Delegate
class OfficeManager: PrinterDelegate {
    func didFinishPrinting(jobName: String) {
        print("Manager notified: \(jobName) is ready.")
    }
}

let printer = Printer()
let manager = OfficeManager()
printer.delegate = manager
printer.printJob(name: "Quarterly Report")

Why Use weak in Delegation?

As discussed in Section 11, the property holding the delegate is almost always marked as weak.

  • The Delegator usually has a reference to the Delegate.
  • Often, the Delegate also owns the Delegator (e.g., a View Controller owning a custom View).
  • Marking the delegate property as weak prevents a strong reference cycle.

Note

To use weak, the protocol must be restricted to class types only by inheriting from AnyObject.


Protocol Summary Checklist

Feature Detail
Blueprint Protocols only define "what" to do, not "how" (implementation).
Inheritance Protocols can inherit from other protocols.
Composition A type can conform to multiple protocols simultaneously.
Delegation Used to decouple a generic object from its specific behavioral logic.

Protocol Extensions & Default Implementations

Protocols in their basic form only define requirements. However, Protocol Extensions allow you to provide actual implementations for those requirements. This is a powerful feature that enables "Protocol-Oriented Programming" in Swift.


Default Implementations

By extending a protocol, you can provide a default implementation for any method or computed property requirement of that protocol. If a conforming type provides its own implementation of a requirement, that implementation is used; otherwise, the default implementation from the extension is used.

protocol Togglable {
    var isOn: Bool { get set }
    mutating func toggle()
}

// Provide a default implementation so every Togglable type doesn't have to write it
extension Togglable {
    mutating func toggle() {
        isOn = !isOn
    }
}

struct LightSwitch: Togglable {
    var isOn = false
    // No need to implement toggle() here; it's inherited from the extension
}

Adding New Functionality

Extensions can also add new methods or properties to a protocol that were not part of the original requirement list. These are available to all types that conform to the protocol.

extension Collection {
    // A new method available to Array, Dictionary, and Set
    func summarize() {
        print("This collection has \(count) elements.")
    }
}

[1, 2, 3].summarize() // "This collection has 3 elements."

Conditional Mapping (where clauses)

You can use a where clause to limit a protocol extension so that its functionality is only available if the conforming type meets certain criteria.

extension Collection where Element: Equatable {
    // This method is only available if the elements can be compared for equality
    func allItemsEqual() -> Bool {
        for item in self {
            if item != self.first { return false }
        }
        return true
    }
}

let uniform = [1, 1, 1]
print(uniform.allItemsEqual()) // true

// [1, "A"].allItemsEqual() -> Would result in a compiler error

Protocol Extensions vs. Base Classes

While protocol extensions might seem similar to base classes in inheritance, they offer several advantages:

Feature Base Classes (Inheritance) Protocol Extensions
Applicability Only Classes. Classes, Structs, and Enums.
Multiplicity Can only inherit from one superclass. Can conform to multiple protocols.
Retroactive Cannot add a superclass to an existing type. Can add protocol conformance to existing types.
Weight Often carries heavy state and overhead. Lightweight and behavior-focused.

Summary Checklist

  • Redundancy: Use protocol extensions to avoid duplicating code across multiple conforming types.
  • Flexibility: Default implementations can still be overridden by the conforming type if a specialized version is needed.
  • Hierarchy: Protocol extensions provide a way to share logic horizontally across unrelated types (e.g., making both a Bird and a Plane conform to Flyable).
  • Constraints: Use where clauses to provide specialized logic for specific subsets of types.

Generics Last updated: Feb. 25, 2026, 7:48 p.m.

test

Generic Functions

Generics are one of the most powerful features of Swift. They allow you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.


The Problem Generics Solve

Without generics, if you wanted a function to swap two values, you would have to write a separate version for every data type, even though the logic remains identical.

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Generic Function Syntax

A generic function uses a placeholder type name (usually the letter T) instead of an actual type name (like Int or String). The placeholder tells Swift: "I don't know what this type is yet, but both parameters will be of this same type."

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Key Components:

  • Type Parameter (<T>): Placed immediately after the function name in angle brackets. This tells Swift that T is a placeholder and not a real class or struct name.
  • Placeholder Usage: T is then used as the type for the parameters.
  • Type Inference: When you call the function, Swift looks at the values you pass in and "infers" what T should be (e.g., if you pass two Double values, T becomes Double).

Type Parameters

You can provide more than one type parameter by writing multiple names within the angle brackets, separated by commas (e.g., <T, U>).

Parameter Naming Convention
Single Placeholder Usually T, U, or V.
Descriptive Names Use names like Key and Value (for a Dictionary) or Element (for an Array) to indicate the relationship between the type parameter and the function/type.

Type Constraints

Sometimes, a generic function needs to perform operations that aren't available on every type (like addition or comparison). You can use type constraints to specify that a type parameter must inherit from a specific class or conform to a particular protocol.

// T must conform to Comparable to use the '<' operator
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind { // '==' requires Equatable
            return index
        }
    }
    return nil
}

Summary Checklist

  • Reusability: Write the logic once; use it for any type.
  • Type Safety: Unlike using Any, generics allow the compiler to ensure that types remain consistent (e.g., you can't swap an Int with a String).
  • Performance: Swift’s compiler optimizes generic code (via specialization) so it runs as fast as non-generic code.
  • Inference: You rarely need to specify the type manually at the call site; Swift figures it out.

Generic Types

In addition to generic functions, Swift allows you to define your own generic types These are custom classes, structures, and enumerations that can work with any type, in a similar way to built-in collections like Array and Dictionary


Defining a Generic Type

The most common example of a generic type is a Stack—a collection where values are pushed onto the top and popped off from the top (Last-In, First-Out). By using a type parameter <Element>, we can create a stack for Int, String, or any custom object.

struct Stack<Element> {
    var items: [Element] = []
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

// Initializing for specific types
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")

var stackOfInts = Stack<Int>()
stackOfInts.push(1)

Extending a Generic Type

When you extend a generic type, you do not provide the type parameter list as part of the extension’s definition. Instead, the type parameter list from the original definition is available within the body of the extension.

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

Associated Types (Generics in Protocols)

Protocols do not use the <T> syntax for generics. Instead, they use the associatedtype keyword. This acts as a placeholder for a type that is used as part of the protocol until the protocol is adopted.

Parameter Naming Convention
Single Placeholder Usually T, U, or V.
Descriptive Names Use names like Key and Value (for a Dictionary) or Element (for an Array) to indicate the relationship between the type parameter and the function/type.
protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

// Struct conforming to Container
struct IntStack: Container {
    // Swift infers that 'Item' is 'Int'
    typealias Item = Int 
    var items: [Int] = []
    mutating func append(_ item: Int) { self.items.append(item) }
    var count: Int { return items.count }
    subscript(i: Int) -> Int { return items[i] }
}

Generic Where Clauses

Just as with protocol extensions, you can use a where clause with generic types and functions to require that associated types or type parameters satisfy certain requirements.

func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {
        
    // Check if two containers of different types hold the same content
    if someContainer.count != anotherContainer.count { return false }
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] { return false }
    }
    return true
}

Summary Checklist

  • Reusability: Generic types allow you to define the structure of data once and use it for many types.
  • Naming: Use descriptive names like Element, Key, or Value for clarity.
  • Protocols: Use associatedtype when you want a protocol to be generic.
  • Constraints: Use where clauses to add granular requirements to your generic logic.

Type Constraints

While generic functions and types can work with any type, there are many scenarios where you need to guarantee that a type supports specific functionality—such as being comparable, equatable, or inheriting from a specific base class. Type constraints allow you to specify these requirements.


Syntax of Type Constraints

You place a type constraint by placing a class or protocol constraint after a type parameter’s name, separated by a colon, within the type parameter list.

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

Common Use Cases

1. Protocol Constraints

The most frequent use of constraints is ensuring a type conforms to a standard protocol like Equatable, Comparable, or Numeric.

// This function fails to compile without the : Equatable constraint
// because not every type in Swift can be compared with '=='
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let strings = ["cat", "dog", "llama"]
if let index = findIndex(of: "dog", in: strings) {
    print("Found dog at index \(index)")
}

2. Class Constraints

You can constrain a type parameter to be a subclass of a specific class. This is useful when you need to access properties or methods defined in a base class.

class View { /* base view logic */ }
class Button: View { /* button logic */ }

func refreshView<T: View>(view: T) {
    // We can safely treat 'view' as a 'View' instance
}

Comparison of Constraint Types

Constraint Type Syntax Requirement
Protocol T: Hashable Type must implement the requirements of that protocol.
Class T: UIViewController Type must be that class or a subclass of it.
Composition T: ProtocolA & ProtocolB Type must conform to all listed protocols.

The where Clause

For more complex constraints—such as requiring that two different generic types be the same, or that an associated type conform to a protocol—Swift uses the where clause.

func compareProportional<T: BinaryFloatingPoint, U: BinaryFloatingPoint>(item1: T, item2: U) -> Bool 
    where T == U {
    // Ensures both types are exactly the same floating point type
    return item1 > item2
}

Summary Checklist

  • Safety: Constraints prevent you from calling methods (like + or ==) on types that don't support them.
  • Flexibility: You can mix and match multiple protocols using the & symbol.
  • Standard Protocols: Common constraints include Equatable (for ==), Comparable (for <), and Codable (for JSON).

Associated Types

When defining a protocol, it is sometimes useful to declare one or more associated types as part of the protocol’s definition. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted.


The associatedtype Keyword

Associated types are the protocol equivalent of Type Parameters in classes or structs. They allow you to define a protocol without knowing the exact type of the data it will handle.

protocol Container {
    // Defines a placeholder for whatever the container will hold
    associatedtype Item
    
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Implementing a Conforming Type

When a type conforms to a protocol with an associated type, it provides a specific type to fulfill that requirement. This can be done explicitly with a typealias, or implicitly through Swift's type inference.

1. Explicit Implementation

struct IntStack: Container {
    // Explicitly stating that 'Item' is 'Int'
    typealias Item = Int
    
    var items: [Int] = []
    mutating func append(_ item: Int) { items.append(item) }
    var count: Int { return items.count }
    subscript(i: Int) -> Int { return items[i] }
}

2. Implicit Implementation (Type Inference)

struct StringStack: Container {
    var items: [String] = []
    
    // Swift looks at the method signature and infers 'Item' is 'String'
    mutating func append(_ item: String) { items.append(item) }
    var count: Int { return items.count }
    subscript(i: Int) -> String { return items[i] }
}

Constraints on Associated Types

You can add constraints to an associated type to ensure that conforming types use only types that meet certain requirements (e.g., must be Equatable).

protocol Container {
    // Now, every Item in a Container must be Equatable
    associatedtype Item: Equatable
    // ... rest of protocol
}

Comparison: Type Parameters vs. Associated Types

Feature Type Parameters (<T>) Associated Types (associatedtype)
Used In Classes, Structs, Enums. Protocols.
Declaration At the head (e.g., struct Stack<T>). Inside the body.
Binding When the instance is created. When the type conforms to the protocol.
Flexibility High (can change per instance). Fixed per conforming type.

Recursive Associated Types

A protocol can have an associated type that itself conforms to the same protocol. For example, a hierarchy where an object contains a list of objects of its own kind.

protocol SuffixableContainer: Container {
    // Suffix must be a SuffixableContainer and its Item must match the parent Item
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

Summary Checklist

  • Placeholder: Use associatedtype when a protocol needs to refer to a type it doesn't know yet.
  • Inference: You don't usually need typealias if your method signatures make the type obvious.
  • Constraints: You can use : or where to limit what types can fill the placeholder.
  • Specialization: This is the foundation for Swift's Collection and Sequence protocols.

Generic Where Clauses

While type constraints allow for simple requirements, generic where clauses enable you to define more complex relationships between type parameters and associated types. They allow you to require that an associated type conform to a protocol, or that two specific type parameters be identical.


Purpose of a where Clause

A generic where clause is used to specify that:

  • An associated type must conform to a particular protocol.
  • Two types or associated types must be identical (using the == operator).
  • A type must be a subclass of a specific class.

Usage in Functions

You can use where clauses to ensure that two different generic containers hold the same type of data before performing an operation on them.

func allItemsMatch<C1: Container, C2: Container>(_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {
        
    // Check if both containers have the same number of items
    if someContainer.count != anotherContainer.count {
        return false
    }
    
    // Check each pair of items for equality
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }
    
    return true
}

Requirements met in the example above:

  1. C1 must conform to Container.
  2. C2 must conform to Container.
  3. C1.Item must be the same type as C2.Item.
  4. C1.Item must conform to Equatable (so we can use !=).

Usage in Extensions

Generic where clauses are frequently used in extensions to provide specialized functionality only when certain conditions are met. This is a cornerstone of Swift's "Protocol-Oriented Programming."

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else { return false }
        return topItem == item // Requires Equatable
    }
}

In this case, the isTop method is only available on Stack instances where the elements are Equatable. If you have a Stack of non-equatable objects, this method simply won't exist for that instance.


Complex Constraints Comparison

Feature Type Constraint (T: Protocol) Generic where Clause
Complexity Simple inheritance/conformance. Complex relationships and equality.
Location Inside angle brackets < >. After the signature, before the body.
Associated Types Cannot easily constrain them. Primary way to constrain associatedtype.
Readability High for simple cases. High for multiple specific requirements.

Contextual where Clauses

You can also use where clauses on specific methods within a generic type, rather than the whole extension.

extension Container {
    func average() -> Double where Item: BinaryInteger {
        var sum = 0
        for i in 0..<count { sum += Int(self[i]) }
        return Double(sum) / Double(count)
    }
}

Summary Checklist

  • Equality: Use == in a where clause to force two types to be identical.
  • Specialization: Use where in extensions to add methods to specific versions of a generic type.
  • Granularity: Use where to constrain associatedtype properties that are defined inside protocols.

Concurrency (Modern Swift) Last updated: Feb. 25, 2026, 7:49 p.m.

test

Defining & Calling Asynchronous Functions (async/a

Concurrency in Swift allows multiple pieces of code to run at the same time. The modern async/await syntax makes asynchronous code—code that pauses to wait for a long-running task—look and behave like standard, synchronous code. This improves readability and safety compared to older completion-handler patterns.


Key Concepts: async and await

Asynchronous functions are defined by two main keywords:

  • async : Marks a function, method, or property as asynchronous, indicating it can pause its execution while waiting for a task to complete.
  • await Marks a potential suspension point. When the code reaches await, it pauses, stays in place, and yields its thread to the system so other work can be done while the task completes.
Keyword Purpose Placement
async Defines that a function can be paused. After parameters, before the return type.
await Calls an async function and waits for results. Before the function call.

Defining an Asynchronous Function

To make a function asynchronous, add the async keyword to its declaration. If the function also throws errors, async must come before throws.

func fetchPhoto(named name: String) async -> UIImage {
    // Imagine a network request happens here
    let result = // ... some long-running work
    return result
}

Calling an Asynchronous Function

When you call an async function, you must use the await keyword. Because the function can suspend execution, you can only call it from another asynchronous context (like another async function) or a Task.

func showPhotos() async {
    print("Fetching photo...")
    
    // Execution pauses here until fetchPhoto returns
    let photo = await fetchPhoto(named: "AlpineSunrise")
    
    // Execution resumes once the photo is ready
    display(photo)
}

Benefits over Completion Handlers

Before async/await, developers used completion handlers (closures). The comparison shows how much cleaner the modern syntax is:

Feature Completion Handlers (Old) async/await (New)
Structure Nested closures ("Pyramid of Doom"). Linear, top-to-bottom.
Error Handling Manual Result checking. Standard try/catch.
Readability Difficult to follow logic flow. Easy to read like sync code.
Memory High risk of strong reference cycles. Much safer ARC management.

Asynchronous Sequences

You can also use await with a for loop to iterate over an Asynchronous Sequence. This is used when the elements of a collection aren't all available at once (e.g., reading lines from a large file or receiving web socket updates).

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}

Summary Checklist

  • Suspension: await doesn't block the thread; it "gives it back" to the system to do other work.
  • Context: You cannot call an async function from a standard synchronous function without wrapping it in a Task { }.
  • Order: In a function that both waits and errors, the syntax is always async throws.

Asynchronous Sequences

While a standard async function returns a single value after a period of time, an Asynchronous Sequence delivers a series of values over time. You can think of it as an Array or Sequence where each element is fetched asynchronously.


The for-await-in Loop

To iterate over an asynchronous sequence, Swift uses the for-await-in syntax. Just like a standard async call, the loop pauses execution at each iteration to wait for the next element to become available.

import Foundation

func processLogFile() async throws {
    let url = URL(fileURLWithPath: "/path/to/log.txt")
    
    // .lines returns an AsyncSequence of Strings
    for try await line in url.lines {
        print("Processing log entry: \(line)")
    }
    // This line only runs after the sequence completes
    print("Finished processing all lines.")
}

Key Characteristics of Async Sequences

Feature Sequence (Standard) AsyncSequence (Modern)
Delivery All elements are ready immediately. Elements arrive over time (e.g., over a network).
Looping for item in collection for await item in collection
Potential Failure Usually does not throw during iteration. Can be combined with try (for try await).
Termination Ends when the collection is exhausted. Ends when the source closes or an error occurs.

Common Sources of Async Sequences

Many system APIs in Swift now return AsyncSequence types instead of using older notification or delegation patterns:

  • File I/O: Reading lines or bytes from a file or URL.
  • Notifications: Observing NotificationCenter posts as a stream.
  • Network: Streaming data chunks from a URLSession.
  • Custom Streams: Using AsyncStream to wrap your own asynchronous data sources.

Example: NotificationCenter Stream

let center = NotificationCenter.default
let notifications = center.notifications(named: .UIApplicationDidBecomeActiveNotification)

Task {
    for await notification in notifications {
        print("Application became active!")
    }
}

Creating Your Own: AsyncStream

If you have an existing callback-based API (like a location manager or a timer), you can wrap it in an AsyncStream to make it compatible with for-await-in.

let digitStream = AsyncStream<Int> { continuation in
    var count = 0
    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        count += 1
        continuation.yield(count) // Send value to the sequence
        
        if count >= 5 {
            continuation.finish() // End the sequence
            timer.invalidate()
        }
    }
}

// Consuming the stream
Task {
    for await digit in digitStream {
        print("Tick: \(digit)")
    }
}

Summary Checklist

  • Execution: for-await-in suspends the current task while waiting for the next value; it does not block the thread.
  • Error Handling: Use for try await if the sequence can potentially fail (like a network stream).
  • Termination: You can break out of an async loop early using break or return, which tells the sequence to stop producing values.

Tasks & Task Groups

While async/await handles the flow of a single asynchronous operation, Tasks and Task Groups allow you to manage the execution and lifecycle of multiple operations. They provide the foundation for Structured Concurrency, ensuring that asynchronous work is organized, cancellable, and predictable.


What is a Task?

A Task is a unit of asynchronous work. Every async function in Swift runs as part of some task.

  • Task { } (Unstructured): Creates a top-level task that runs on the current actor. This is how you call async code from a synchronous context (like a button click).
  • Task.detached (Advanced): Creates a task that doesn't inherit information from its parent (like priority or actor context). Use sparingly.
// Calling async code from a sync function
func buttonTapped() {
    Task {
        let data = await fetchData()
        updateUI(with: data)
    }
}

Parallelism with async let

If you have several independent asynchronous tasks, you can run them in parallel using async let. This is the simplest form of structured concurrency.

func loadHomeContent() async {
    // These start running at the same time
    async let weather = fetchWeather()
    async let news = fetchNews()
    async let scores = fetchScores()

    // The function only waits when you actually need the values
    let dashboard = await Dashboard(weather: weather, news: news, scores: scores)
    display(dashboard)
}

Task Groups

When you don't know the number of tasks you need to run at compile time (for example, downloading a list of images based on a URL array), you use a Task Group.

A task group provides a scope where you can add multiple child tasks. The group automatically manages the results and ensures all children finish before the group itself finishes.

Component Purpose
withTaskGroup Standard group for tasks that return values.
withThrowingTaskGroup Used if the child tasks can throw errors.
group.addTask Adds a new parallel operation to the group.
func downloadPhotos(names: [String]) async -> [UIImage] {
    return await withTaskGroup(of: UIImage?.self) { group in
        for name in names {
            // Spin up a child task for each photo
            group.addTask {
                return await fetchPhoto(named: name)
            }
        }

        var images: [UIImage] = []
        // Collect results as they finish (order is not guaranteed)
        for await photo in group {
            if let photo = photo { images.append(photo) }
        }
        return images
    }
}

Task Cancellation

Swift uses a cooperative cancellation model. This means a task isn't killed instantly; instead, the system marks it as "cancelled," and the code inside the task must check for that status and stop its own work.

  • Task.isCancelled : Returns true if the current task has been cancelled.
  • Task.checkCancellation() : Throws a CancellationError immediately if the task is cancelled.
group.addTask {
    // Check before starting a heavy operation
    try Task.checkCancellation()
    return await performExpensiveCalculation()
}

Comparison: async let vs. Task Groups

Feature async let Task Groups
Quantity Fixed/Known (static). Dynamic (variable count).
Results Individual variables. Stream of results (via for await).
Complexity Simple and lightweight. More robust for complex data patterns.
Best For Fetching 3 specific items for a UI. Processing a list of items from an API.

Summary Checklist

  • Hierarchy: Child tasks (inside groups or async let) are tied to their parent. If the parent is cancelled, the children are cancelled too.
  • Efficiency: Task groups allow you to limit concurrency or handle results as they arrive.
  • Safety: Structured concurrency prevents "leaked" tasks that keep running in the background after you've moved away from a screen.

Unstructured Concurrency

While Structured Concurrency (async let, Task Groups) has a clear parent-child relationship and automatic lifetime management, Unstructured Concurrency provides the flexibility to create tasks that are not tied to a specific scope.


What is Unstructured Concurrency?

Unstructured tasks do not have a parent task. They are useful when you need to start an asynchronous operation from a synchronous context (like a UI lifecycle method) or when a task needs to outlive the scope in which it was created.

Feature Structured (async let, Groups) Unstructured (Task, Task.detached)
Parent-Child Yes (automatic). No.
Cancellation Propagates automatically. Must be managed manually.
Context Inherits priority and local variables. Optional inheritance (depends on type).
Lifetime Tied to the defining block. Can outlive the defining block.

Creating Unstructured Tasks

1. The Task Initializer

The most common way to create an unstructured task is using Task { ... }.

  • It inherits the priority and actor context of the current scope.
  • If called from a @MainActor (like a View Controller), the task also runs on the Main Actor.
func viewDidLoad() {
    super.viewDidLoad()
    
    // We are in a sync function, so we use an unstructured Task
    Task {
        let profile = await fetchUserProfile()
        self.nameLabel.text = profile.name // Safe to update UI (inherits MainActor)
    }
}

2. Detached Tasks

A Detached Task is a top-level task that does not inherit anything from its surrounding context. It has its own priority and is not tied to the actor where it was created. Use this for background work that shouldn't block the current actor.

Task.detached(priority: .background) {
    // Does NOT inherit the MainActor, even if called from a View Controller
    await performHeavyImageProcessing()
}

Managing Unstructured Tasks

Because the system doesn't automatically clean up unstructured tasks, you must manage them yourself if you want the ability to cancel them later.

When you create a Task, it returns a Task Handle. You can store this handle and call .cancel() on it when the task is no longer needed (e.g., when a user dismisses a screen).

class ProfileViewController: UIViewController {
    var downloadTask: Task<Void, Never>?

    func startDownload() {
        downloadTask = Task {
            let data = await largeDownload()
            if !Task.isCancelled {
                show(data)
            }
        }
    }

    func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Manually cancel the unstructured task
        downloadTask?.cancel()
    }
}

Comparison: Task vs. Task.detached

Property Task { } Task.detached { }
Actor Context Inherits current actor. Does not inherit.
Priority Inherits current priority. Can be specified independently.
Task-local values Inherits. Does not inherit.
Primary Use UI Actions, simple async calls. Resource-intensive background work.

Summary Checklist

  • Bridge to Sync: Use Task to call async code from synchronous functions.
  • Manual Control: You are responsible for the lifecycle and cancellation of unstructured tasks.
  • UI Safety: Task (non-detached) is generally safer for UI code because it stays on the @MainActor.
  • Resource Management: Avoid creating too many detached tasks, as they can bypass the system's built-in concurrency optimizations.

Actors & Data Isolation

In a concurrent environment, multiple threads might try to access and modify the same data simultaneously. This leads to data races, which cause unpredictable crashes and memory corruption. Actors are a reference type designed specifically to eliminate these races by providing data isolation.


What is an Actor?

An actor is similar to a class (it's a reference type), but with one crucial difference: it allows only one task to access its mutable state at a time. Think of it as a class with a built-in synchronized queue.

Key Characteristics:

  • Reference Type: Like classes, actors are passed by reference.
  • Synchronization: The compiler ensures that no two tasks can access the actor's properties at the same time.
  • Await Requirement: Accessing an actor's properties or methods from outside the actor must be done asynchronously using await.
actor BankAccount {
    let accountNumber: Int
    var balance: Double

    init(accountNumber: Int, initialBalance: Double) {
        self.accountNumber = accountNumber
        self.balance = initialBalance
    }

    func deposit(amount: Double) {
        balance += amount // Safe: Only one task can be in here at once
    }
}

Actor Isolation and await

When you are inside an actor, you can access its properties synchronously. However, when you are outside the actor, you must "wait your turn" to gain access. This is why actor access requires the await keyword.

let account = BankAccount(accountNumber: 1234, initialBalance: 500)

Task {
    // We must 'await' because another task might be depositing right now
    await account.deposit(amount: 100)
    print(await account.balance)
}

The @MainActor

The @MainActor is a globally unique actor that represents the main thread. In iOS/macOS development, all UI updates must happen on the main thread. By marking a class or function with @MainActor, Swift guarantees that the code will always execute on the main thread.

@MainActor
class ProfileViewModel: ObservableObject {
    @Published var userName = "Guest"

    func updateName(to newName: String) {
        // This is guaranteed to run on the main thread
        self.userName = newName
    }
}

Nonisolated Access

Sometimes an actor has a property or method that doesn't actually touch any mutable state (like a constant). You can mark these as nonisolated so they can be accessed synchronously from anywhere without the overhead of await.

actor BankAccount {
    let accountNumber: Int
    var balance: Double
    
    // This is a constant; it's safe to read without synchronization
    nonisolated var accountSuffix: String {
        return String(String(accountNumber).suffix(4))
    }
    // ...
}

print(account.accountSuffix) // No 'await' needed!

Actor vs. Class Comparison

Feature Class Actor
Type Reference Type. Reference Type.
Inheritance Supports inheritance. Does not support inheritance.
Thread Safety Manual (Locks, Queues). Automatic (Compiler-enforced).
Access Synchronous. Asynchronous (await) from outside.
Data Races Susceptible. Prevented by design.

Summary Checklist

  • Safety: Actors are the primary tool for preventing data races in modern Swift.
  • Suspension: Calling an actor method can pause your code (at the await point) if the actor is busy with another task.
  • UI Work: Use @MainActor for any logic that interacts with the user interface.
  • State: Actors protect mutable state; constants (let) can often be accessed without isolation.

Sendable Types

In a concurrent system, data is frequently passed between different tasks and actors. To ensure safety, Swift needs to know which types can be shared across these boundaries without creating data races. This is where the Sendable protocol comes in.


What is Sendable?

A Sendable type is a type whose values can be safely passed across concurrency boundaries (e.g., from one actor to another, or from a parent task to a child task).

How Concurrency Boundaries Work:

When you pass an object to an async function or an actor method, you are crossing a "boundary." If that object is mutable and shared, both sides could try to change it at once. Sendable tells the compiler: "This data is safe to share."


What Types are Sendable?

Many types in Swift are implicitly sendable because they are inherently thread-safe.

Category Why it is Sendable Examples
Value Types They are copied when passed, so there is no shared state. Int, String, Bool, Double.
Value Collections If the elements they contain are also Sendable. Array<String>, Optional<Int>.
Actors They protect their own mutable state internally. Any actor instance.
Immutables Classes that contain only let (constant) sendable properties. Final classes with no var.

Sendable Classes

Most classes are not Sendable because they are reference types that allow multiple parts of a program to point to and modify the same memory. However, a class can conform to Sendable if:

  1. It is marked final.
  2. It contains only immutable (let) properties that are themselves Sendable.
  3. It has no designated superclass (or inherits only from NSObject).
final class TemperatureReading: Sendable {
    let measure: Double
    let date: Date
    
    init(measure: Double, date: Date) {
        self.measure = measure
        self.date = date
    }
}

The @Sendable Attribute for Closures

Closures can also be passed between tasks. To be safe, a closure must not capture mutable variables from its original scope. You mark such closures with the @Sendable attribute.

func doWork(operation: @Sendable @escaping () -> Void) {
    Task {
        operation()
    }
}

var counter = 0
// This would trigger a compiler warning/error 
// because 'counter' is a mutable variable being captured in a @Sendable closure.
/*
doWork {
    counter += 1 
}
*/

Implicit vs. Explicit Conformance

  • Structs and Enums: Usually get implicit Sendable conformance if all their members are Sendable.
  • Generics: A generic type like struct Container<T> is Sendable only if T is Sendable.
  • Manual Conformance: You can explicitly add : Sendable to a type, and the compiler will verify that the type actually meets the safety requirements.

Summary Checklist

  • Thread Safety: Sendable is the compiler’s way of "proofing" your code against data races at the architectural level.
  • Strict Checking: In modern Swift (Swift 6 mode), the compiler will strictly enforce Sendable checks, turning potential runtime crashes into compile-time errors.
  • Value Types Rule: When in doubt, use struct or enum. They are almost always Sendable by default and make concurrency much easier to manage.

Macros (New in Swift 5.9) Last updated: Feb. 25, 2026, 7:49 p.m.

test

Freestanding Macros (#)

Macros are a powerful feature introduced in Swift 5.9 that generate code at compile time. Instead of writing repetitive "boilerplate" code by hand, you can use a macro to generate that code for you. Freestanding macros are used on their own, outside of a specific declaration, and are always prefixed with the pound sign (#).


How Macros Work

When the compiler encounters a macro, it "expands" it. It sends the source code of the macro call to a separate compiler plug-in, which returns new Swift code that is then compiled along with the rest of your project.


Types of Freestanding Macros

There are two primary roles for freestanding macros:

Feature Sequence (Standard) AsyncSequence (Modern)
Delivery All elements are ready immediately. Elements arrive over time (e.g., over a network).
Looping for item in collection for await item in collection
Potential Failure Usually does not throw during iteration. Can be combined with try (for try await).
Termination Ends when the collection is exhausted. Ends when the source closes or an error occurs.

Expression Macros

These are used to produce a piece of code that returns a value. A common example is the #predicate macro used in SwiftData, or a custom macro that checks a string format at compile time.

// Example of a hypothetical #URL macro that validates the string at compile time
let url = #URL("https://www.apple.com")

// Without a macro, this would be:
// let url = URL(string: "https://www.apple.com")! 
// (which could crash at runtime if the string is invalid)

Declaration Macros

These macros generate entirely new chunks of code. For example, you might use a macro to generate a series of constants or a specialized function based on input metadata.

#warning("This is a freestanding declaration macro that warns the developer")

// Another example could be a macro that generates a standard set of roles:
#CreateRoles("Admin", "Editor", "Viewer")

// After expansion, the compiler sees:
// struct Admin { ... }
// struct Editor { ... }
// struct Viewer { ... }

Comparison: Macros vs. Functions

Feature Functions Macros
Execution Occurs at Runtime. Occurs at Compile-time.
Visibility Implementation is hidden in binary. You can "Expand" a macro in Xcode to see the code.
Logic Operates on values. Operates on Source Code (AST).
Safety Errors found when running. Errors caught immediately by the compiler.

Summary Checklist

  • Syntax: Always starts with # (e.g., #colorLiteral, #fileID, #predicate).
  • Transparency: In Xcode, you can right-click any macro and select "Expand Macro" to see exactly what code it is generating behind the scenes.
  • Validation: Macros can provide custom compiler errors. If you pass an invalid argument to a macro, the compiler can flag it with a specific message before you even run the app.

Attached Macros (@)

While freestanding macros stand alone, attached macros are used to modify a specific declaration—such as a class, structure, enum, or variable. They are prefixed with the at-sign (@), making them look similar to attributes like @escaping or @MainActor.


How Attached Macros Work

Attached macros take the declaration they are attached to as input, analyze it, and then generate additional code related to that declaration.

There are five main roles for attached macros:

Role Description
@member Adds new members (properties or methods) inside the type.
@memberAttribute Adds attributes (like @objc or @deprecated) to members of the type.
@accessor Turns a stored property into a computed property by adding get and set blocks.
@extension Adds a protocol conformance or new methods via a separate extension.
@peer Adds new declarations alongside the original one (e.g., creating a related class).

Common Example: @Observable

Starting with iOS 17 and Swift 5.9, the @Observable macro replaced the older ObservableObject protocol. It is an attached macro that automatically generates the boilerplate code needed to track property changes for SwiftUI.

@Observable 
class UserProfile {
    var name: String = "Taylor"
    var age: Int = 30
}

Behind the scenes, the macro expands to:

  • Adding a hidden observation registrar property.
  • Adding get and set accessors to name and age to notify the registrar.
  • Making the class conform to the Observable protocol.

Peer Macros

A peer macro creates a new declaration that lives next to the one it is attached to. This is often used to generate asynchronous versions of synchronous functions automatically.

@AddAsync
func fetchUser(completion: (User) -> Void) {
    // legacy completion handler code
}

// The macro generates the "peer" function:
// func fetchUser() async -> User { ... }

Accessor Macros

An accessor macro is attached to a property. It allows a macro to manage how a value is read or written. This is frequently used for database integration or property wrapping logic that needs to be efficient at compile time.

struct MyData {
    @Logged var count: Int = 0
}

// Expands to:
// var count: Int {
//    get { ... log the access ... }
//    set { ... log the change ... }
// }

Summary Checklist

  • Syntax: Always starts with @ and is placed directly above a declaration.
  • Scope: They can only "see" and "modify" the specific code block they are attached to.
  • Safety: Like freestanding macros, they provide compile-time validation. If you attach a macro to a struct that it only supports for class, the compiler will throw an error immediately.
  • Usage: Use attached macros to reduce "boilerplate" and ensure that related code (like observers or protocol requirements) stays perfectly in sync with your properties.

Macro Implementation Basics

Implementing a macro is different from writing standard Swift code. Because macros manipulate source code at compile time, they are defined in a separate compiler plugin and use the SwiftSyntax library to inspect and generate code.


The Macro Architecture

A macro implementation consists of two distinct parts:

  1. The Declaration: This defines the macro's signature (how it looks to the user).
  2. The Implementation: This is the logic that performs the code transformation, living in a separate target.

Step 1: Defining the Macro

The declaration uses the macro keyword and specifies which macro roles it fulfills and where the implementation resides.

// In your main library
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacros", type: "StringifyMacro")

Step 2: Implementing with SwiftSyntax

The implementation must be a type that conforms to a specific protocol based on its role (e.g., ExpressionMacro). It works with an Abstract Syntax Tree (AST).

Component Purpose
SwiftSyntax The library used to parse, inspect, and generate Swift source code.
AST (Syntax Tree) A tree representation of the source code's structure.
Context Provides metadata about where the macro is being used (e.g., file name, line number).
import SwiftSyntax
import SwiftSyntaxMacros

public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        // Extract the argument passed to #stringify(x)
        guard let argument = node.arguments.first?.expression else {
            fatalError("compiler error: argument required")
        }

        // Return a new piece of code: (value, "value")
        return "(\(argument), \(literal: argument.description))"
    }
}

Macro Protocols

Depending on the type of macro you are building, you must conform to the corresponding protocol:

Protocol Macro Role Input Provided
ExpressionMacro #freestanding(expression) The macro call expression.
MemberMacro @attached(member) The type declaration it's attached to.
PeerMacro @attached(peer) The declaration alongside which code is generated.
ExtensionMacro @attached(extension) The type being extended.

Validation and Errors

One of the best features of macro implementation is the ability to throw compile-time errors. If the user uses the macro incorrectly, you can provide a helpful error message directly in the IDE.

if arguments.count > 1 {
    // This shows up as a red error in Xcode
    throw MyMacroError.tooManyArguments
}

Summary Checklist

  • Separation: Macros must live in a separate "Compiler Plugin" target in your Swift Package.
  • SwiftSyntax: You must be comfortable working with the AST (Syntax Trees) rather than actual runtime values.
  • Testing: Macros are highly testable. You can write unit tests that compare a "String of Input Code" to a "String of Expected Output Code."
  • Expansion: Remember that macros can only generate code; they cannot delete or modify existing user code (except for accessor macros).

Access Control & Advanced Operators Last updated: Feb. 25, 2026, 7:50 p.m.

test

Modules & Source Files

Access control restricts access to parts of your code from code in other source files and modules. This allows you to hide the implementation details of your code and specify a preferred interface through which that code can be accessed and used.


Key Concepts: Modules and Source Files

Before choosing an access level, it is important to understand the two units of organization in Swift:

  • Module: A single unit of code distribution and building. A framework or application that is built and shipped as a single unit and can be imported by another module with the import keyword. Each target in Xcode (like an App target or a Library target) is treated as a separate module.
  • Source File: A single Swift source code file within a module (typically a file ending in .swift).

The Five Access Levels

Swift provides five different access levels for entities within your code. These levels are relative to the module and source file in which an entity is defined.

Access Level Keyword Scope of Visibility Best Use Case
Open open Anywhere in the module, and any module that imports it. Classes only. Allows subclassing and overriding outside the module (Framework APIs).
Public public Anywhere in the module, and any module that imports it. Interface for a library; cannot be subclassed/overridden outside the module.
Internal internal Anywhere within the defining module only. Default level. Shared helper code within an app or framework.
Fileprivate fileprivate Only within its own defining source file. Hiding details used by multiple related types in one file.
Private private Only within the enclosing declaration (and extensions in the same file). True encapsulation; hiding state from everything else.

Guiding Principle of Access Control

    No entity can be defined in terms of another entity that has a lower (more restrictive) access level.

For example:

  • A public variable cannot be defined as having an internal or private type.
  • A function cannot have a higher access level than its parameter types or return type.
internal class InternalType {}
private class PrivateType {}

// ERROR: Function cannot be public because InternalType is internal
public func someFunction(with item: InternalType) { ... } 

// OK: Function is private, so it can use a private type
private func anotherFunction(with item: PrivateType) { ... }

Default Access Level

If you do not specify an access level, most entities in your code are internal by default. This allows you to write an entire app without ever typing an access control keyword, as all code within the app module will be able to talk to each other.


Summary Checklist

  • Encapsulation: Use private or fileprivate to hide data that shouldn't be touched by other parts of the app.
  • Frameworks: If you are building a library, use public or open to designate what your users are allowed to see and use.
  • Open vs. Public: Use open only if you explicitly want developers to inherit from your class or override your method in their own modules.
  • Unit Testing: To test internal code, you must use the @testable import attribute in your test files.

Access Levels (Open, Public, Internal, Fileprivate

Access levels in Swift allow you to specify the visibility of your code. By applying an access level to a type (class, struct, enum), property, or function, you control where that entity can be accessed.


Comparison of Access Levels

The following table ranks access levels from least restrictive (top) to most restrictive (bottom).

Level Keyword Visibility Subclassing / Overriding
Open open Anywhere (including outside the module). Allowed outside the module.
Public public Anywhere (including outside the module). Not allowed outside the module.
Internal internal Only within the defining module. Allowed within the module only.
File-private fileprivate Only within the source file. Allowed within the file only.
Private private Only within the enclosing declaration. Allowed within the same scope/extension.

Detailed Breakdown

1. Open and Public

These levels allow code to be used in any source file from their defining module, and also in a source file from another module that imports the defining module.

  • Open applies only to classes and class members. It is specifically designed for frameworks to allow users to subclass and override behavior.
  • Public prevents subclassing or overriding outside the module. Use this for stable API surfaces.

2. Internal (The Default)

Internal access is the default for almost all code. You don't need to write internal explicitly. It allows an entity to be used within any source file from its defining module, but not outside that module.

3. File-private

fileprivate restricts the use of an entity to its own defining source file. It is useful when you have multiple related classes or structures in a single file that need to share implementation details but hide them from the rest of the project.

4. Private

private is the most restrictive. It restricts use to the enclosing declaration and to extensions of that declaration that are in the same file.


Customizing Getters and Setters

You can give a setter a lower access level than its corresponding getter. This is a common pattern to create a property that is read-only to the outside world but writeable within the type.

public struct TrackedStore {
    // Getter is public, but setter is private
    public private(set) var numberOfEdits = 0
    
    public var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
    
    public init() {}
}

var store = TrackedStore()
store.value = "New Value"
print(store.numberOfEdits) // 1 (Readable)
// store.numberOfEdits = 5 // ERROR: Setter is private

Access Control in Extensions

If you mark an extension with an access level, all members defined inside that extension inherit that level by default.

private extension String {
    func validateEmail() -> Bool {
        return self.contains("@")
    }
}
// 'validateEmail' is automatically private.

Summary Checklist

  • Rule of Thumb: Always use the most restrictive access level possible for your data.
  • Open vs. Public: If you aren't building a framework intended for inheritance, use public.
  • Testing: Use @testable import in your unit tests to grant them internal access to your app module.
  • Inconsistency: Remember that a property cannot have a higher access level than its type (e.g., you can't have a public variable of a private struct).

Bitwise Operators

Content goes here...

Operator Overloading

Bitwise operators enable you to manipulate the individual raw data bits within a data structure. They are often used in low-level programming, such as graphics programming, device driver creation, or when working with raw data from external sources.


The Five Bitwise Operators

Swift supports all the standard bitwise operators found in C-based languages.

Operator Name Description
~ Bitwise NOT Inverts all bits (0 becomes 1, 1 becomes 0).
& Bitwise AND Results in 1 only if bits in both numbers are 1.
| Bitwise OR Results in 1 if bits in either number are 1.
^ Bitwise XOR Results in 1 if bits are different; 0 if they are the same.
<< / >> Bitwise Shift Moves all bits to the left or right by a specified number of places.

Detailed Breakdown

1. Bitwise NOT (~)

The bitwise NOT operator is a prefix operator that flips all bits in a number.

let initialBits: UInt8 = 0b00001111 // Decimal 15
let invertedBits = ~initialBits      // 0b11110000 (Decimal 240)

2. Bitwise AND, OR, and XOR

These operators compare bits in two numbers to produce a third number.

let firstBits: UInt8 =  0b11110000
let secondBits: UInt8 = 0b10101010

let andResult = firstBits & secondBits // 0b10100000
let orResult  = firstBits | secondBits // 0b11111010
let xorResult = firstBits ^ secondBits // 0b01011010

3. Bitwise Shift Operators (<< and >>)

Shifting bits to the left or right effectively multiplies or divides a number by a power of two.

  • Left Shift (<<): Every bit moves left. Bits that move past the end are discarded; empty spaces are filled with zeros. (Multiplying by 2).
  • Right Shift (>>): Every bit moves right. (Dividing by 2).

Use Case: Bitmasks

Bitwise operators are commonly used to store multiple Boolean flags in a single integer to save memory.

struct Permissions {
    static let read: UInt8    = 0b0001
    static let write: UInt8   = 0b0010
    static let execute: UInt8 = 0b0100
}

var userPerms: UInt8 = Permissions.read | Permissions.write // 0b0011

// Check for permission using AND
let canWrite = (userPerms & Permissions.write) != 0 // true

Summary Checklist

  • Safety: Unlike C, Swift's bitwise operators do not overflow into the sign bit for signed integers by default; they use Logical Shifts for unsigned types and Arithmetic Shifts for signed types to preserve the sign.
  • Speed: Bitwise operations are extremely fast as they map directly to CPU instructions.
  • Binary Literals: Use the 0b prefix to make bitwise code easier to read (e.g., 0b1010).

Operator Overloading

In Swift, you can provide your own implementations of existing operators for your custom types. This is known as operator overloading. It allows you to use standard operators (like +, -, *, or ==) with your own structs, classes, and enums, making your code more intuitive and readable.


Basic Operator Overloading

To overload an operator, you define a static method on the type that the operator will work with. The method name must match the operator symbol.

struct Vector2D {
    var x = 0.0, y = 0.0
}

extension Vector2D {
    // Overloading the addition (+) operator
    static func + (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y + right.y)
    }
}

let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector 
// combinedVector is (x: 5.0, y: 5.0)

Prefix and Postfix Operators

By default, operators like + are infix (between two values). You can also overload prefix operators (before a value, like -a) or postfix operators (after a value, like a!).

Type Keyword Example
Infix (Default) a + b
Prefix prefix -a
Postfix postfix a!
extension Vector2D {
    static prefix func - (vector: Vector2D) -> Vector2D {
        return Vector2D(x: -vector.x, y: -vector.y)
    }
}

let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive // (x: -3.0, y: -4.0)

Compound Assignment Operators

Compound assignment operators combine assignment (=) with another operation (like +=). When overloading these, you must mark the left-hand parameter as inout because its value will be modified directly.

extension Vector2D {
    static func += (left: inout Vector2D, right: Vector2D) {
        left = left + right
    }
}

Equivalence Operators

To allow your type to use == and !=, it should conform to the Equatable protocol. In many cases, Swift can synthesize this for you automatically if all properties of the type are also Equatable.

extension Vector2D: Equatable {
    static func == (lhs: Vector2D, rhs: Vector2D) -> Bool {
        return (lhs.x == rhs.x) && (lhs.y == rhs.y)
    }
}

Summary Checklist

  • Clarity: Only overload operators when the meaning is obvious (e.g., adding two vectors). Avoid using + for something non-mathematical, as it confuses other developers.
  • Static Methods: Operators must be defined as static methods on a type.
  • Precedence: Overloaded operators retain the same precedence and associativity as the original operator (e.g., * will still be calculated before +).
  • Protocol Synthesis: For Equatable and Comparable, try to let Swift synthesize the implementation before writing your own.

Custom Operators

If the standard set of Swift operators doesn't meet your needs, you can define your own custom operators. Custom operators allow you to use unique symbols (like *** or +-+) to represent specialized logic for your types.


Defining a Custom Operator

Defining a custom operator is a two-step process:

  1. Global Declaration: You must declare the operator at the global level using the operator keyword, specifying its position (prefix, infix, or postfix).
  2. Implementation: You implement the operator as a static method within your type.
// 1. Declare the operator globally
infix operator +-: AdditionPrecedence

extension Vector2D {
    // 2. Implement the logic
    static func +- (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y - right.y)
    }
}

let first = Vector2D(x: 10, y: 10)
let second = Vector2D(x: 5, y: 5)
let result = first +- second // Result: (x: 15, y: 5)

Precedence and Associativity

When you define a custom infix operator, you should assign it to a precedence group This tells Swift the order in which the operator should be evaluated relative to others (e.g., multiplication happens before addition).

Component Description
Precedence Higher precedence operators are evaluated first (like * over +).
Associativity Determines if operators of the same precedence group from left to right or right to left.
Assignment If true, the operator is treated like an assignment operator during optional chaining.

Defining a Custom Precedence Group

If the standard groups (like AdditionPrecedence or MultiplicativePrecedence) don't fit, you can create your own:

precedencegroup MyCustomPrecedence {
    higherThan: AdditionPrecedence
    associativity: left
}

infix operator ^^^: MyCustomPrecedence

Valid Characters

Custom operators can begin with characters such as /, =, -, +, !, *, %, <, >, &, |, ^, or ?. You can also use various mathematical Unicode characters (like ? or ?).

Note

You cannot define an operator consisting only of a single dot (.). While ... and ..< are reserved, you can create custom operators that begin with a dot if they contain at least one other dot.


Comparison: Standard vs. Custom Operators

Feature Standard Operators Custom Operators
Declaration Built into the language. Must be declared globally first.
Symbols Fixed (e.g., +, -, *). Flexible (e.g., >>>, <*>).
Precedence Pre-defined. Must be manually assigned.
Best Practice Use whenever applicable. Use sparingly for domain-specific logic.

Summary Checklist

  • Readability: Custom operators can make code look "magical" or confusing. Only use them when the symbol clearly communicates the intent to other developers.
  • Global Scope: Remember that the operator> declaration must be outside of any class or struct.
  • Symmetry: If you define a custom infix operator, consider if a corresponding compound assignment operator (like +-=) is also needed.

DocsAllOver

Where knowledge is just a click away ! DocsAllOver is a one-stop-shop for all your software programming needs, from beginner tutorials to advanced documentation

Get In Touch

We'd love to hear from you! Get in touch and let's collaborate on something great

Copyright copyright © Docsallover - Your One Shop Stop For Documentation