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.
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.
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.
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.
- When you "copy" a collection, both variables initially point to the same memory buffer.
- The actual byte-for-byte copy only happens if one of the instances is modified.
- 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.
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.
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:
- Declaration: Use the
inout keyword in the function
signature.
- 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:
- Asynchronous Operations: The closure is stored to be called once a
network request or timer finishes.
- 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. |
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:
- Before a specific case: If only some cases are recursive.
- 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.
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.
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 & 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:
- 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.
- 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
- A designated initializer must ensure that all properties introduced by its class are
initialized before it delegates up to a superclass initializer.
- A designated initializer must delegate up to a superclass initializer before assigning a
value to an inherited property.
- 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:
- Rule 1: A designated initializer must call a designated initializer
from its immediate superclass.
- Rule 2: A convenience initializer must call another initializer from
the same class.
- 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:
- Override
init? with init?: Standard behavior.
- 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).
- 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.
- The subclass deinitializer is called first.
- Once the subclass deinitializer finishes, the superclass deinitializer is called
automatically.
- 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.
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:
- Weak References (
weak)
- 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.
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
- 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.
- Short-Circuiting: If any part of the chain is
nil, the
rest of the chain is not even evaluated.
- 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:
- The Protocol: Defines what the delegate is capable of doing.
- The Delegator: Has a property (usually
weak) of the
protocol type.
- 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.
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:
C1 must conform to Container.
C2 must conform to Container.
C1.Item must be the same type as C2.Item.
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.
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:
- It is marked
final.
- It contains only immutable (
let) properties that are themselves
Sendable.
- 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.
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:
- The Declaration: This defines the macro's signature (how it looks to
the user).
- 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).
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:
- Global Declaration: You must declare the operator at the global level
using the
operator keyword, specifying its position (prefix, infix, or
postfix).
- 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.