Basics
Variables & Constants
Use let by default for values that don't change; switch to var only when mutation is needed. Swift infers types at compile time, but explicit annotations improve readability for complex or ambiguous types.
var name = "Alice" // mutable
let age = 30 // immutable constant
let pi: Double = 3.14159 // explicit type
var count: Int = 0
count = 10
print(type(of: age)) // IntOptionals
Optionals represent the absence of a value. Use if-let for safe unwrapping, ?? for default values, and ! only when you're certain the value exists (risky—can crash).
var nickname: String? = nil
nickname = "Al"
if let n = nickname {
print("Hi, \(n)")
}
print(nickname ?? "Anonymous") // nil-coalescing
let force: String = nickname! // force unwrap (crashes if nil)Tuples
Tuples group multiple values into a single compound value. Useful for returning multiple values from functions. Named elements improve readability.
let person = (name: "Alice", age: 30)
print(person.name) // Alice
print(person.0) // Alice
let (n, a) = person
print("\(n), \(a)") // Alice, 30Type Conversion
Swift does not implicitly convert between types. Always use explicit conversion (e.g., Double(intVal)) to avoid ambiguity and prevent subtle bugs.
let intVal = 42
let doubleVal = Double(intVal)
let strVal = String(intVal)
let fromStr = Int("100")! // 100
let invalid = Int("abc") // nil
print(doubleVal, strVal)Assertions & Preconditions
Use assert for debugging checks (removed in release builds) and precondition for critical invariants checked in all builds. Both help catch logic errors early.
let age = -5
assert(age >= 0, "Age cannot be negative")
precondition(age >= 0, "Age must be non-negative")
// In debug builds, assert crashes if false
// precondition checks in release builds tooStrings & Characters
String Interpolation
String interpolation with \(expr) embeds any expression into a string. It's type-safe and evaluated at compile time, making it safer than format strings.
let name = "Alice"
let age = 30
let msg = "Name: \(name), Age: \(age)"
let calc = "5 + 3 = \(5 + 3)"
print(msg) // Name: Alice, Age: 30
print(calc) // 5 + 3 = 8Common String Methods
Swift strings are Unicode-correct, meaning count reflects actual characters (grapheme clusters), not bytes. Use these methods instead of manual indexing for safety.
let s = "Hello, World"
print(s.count) // 12
print(s.uppercased()) // HELLO, WORLD
print(s.lowercased()) // hello, world
print(s.hasPrefix("Hello")) // true
print(s.contains("World")) // true
print(s.reversed()) // "dlroW ,olleH"Substring & Indexing
String indices are not integers because characters can have different byte sizes. Use index(_:offsetBy:) for navigation. Substrings share memory with the original—convert to String for long-term storage.
let s = "Hello, World"
let idx = s.index(s.startIndex, offsetBy: 7)
print(s[idx]) // W
let sub = s[idx...] // "World"
let newStr = String(sub) // convert to String
print(s.prefix(5)) // "Hello"Multiline Strings
Triple-quoted strings preserve line breaks and indentation. The closing """ determines the baseline indentation. Ideal for HTML, JSON, or long text blocks.
let poem = """
Roses are red,
Violets are blue,
Swift is great,
And so are you.
"""
print(poem)
// Use \\(expr) for interpolation in multilineString Building & Splitting
Use joined(separator:) to concatenate with a delimiter and split(separator:) to tokenize. These are more efficient than manual loops with + concatenation.
let parts = ["apple", "banana", "cherry"]
let joined = parts.joined(separator: ", ")
print(joined) // apple, banana, cherry
let csv = "a,b,c"
let fields = csv.split(separator: ",")
print(fields) // ["a", "b", "c"]
let reversed = String(s.reversed())Data Structures
Array
Arrays are ordered, zero-indexed collections. Use append/insert for additions, filter/map/reduce for transformations. Prefer value types (Array is a struct) for thread safety.
var nums = [1, 2, 3]
nums.append(4)
nums.insert(0, at: 0)
nums[1] = 20
print(nums.count) // 5
print(nums.contains(20)) // true
let evens = nums.filter { $0 % 2 == 0 }
print(nums.first, nums.last)Dictionary
Dictionaries store key-value pairs with O(1) average lookup. Use default: to avoid nil when accessing missing keys. Keys must be Hashable.
var ages: [String: Int] = ["Alice": 30]
ages["Bob"] = 25
ages["Alice"] = 31
print(ages["Alice", default: 0]) // 31
for (name, age) in ages {
print("\(name): \(age)")
}
print(ages.keys.sorted())Set
Sets store unique values with O(1) membership testing. Ideal for deduplication and set operations (union, intersection, difference). Elements must be Hashable.
var a: Set<Int> = [1, 2, 3]
var b: Set<Int> = [3, 4, 5]
print(a.union(b)) // {1,2,3,4,5}
print(a.intersection(b)) // {3}
print(a.subtracting(b)) // {1,2}
print(a.isSubset(of: b)) // false
a.insert(6)Ranges
..< is a half-open range (excludes upper bound), ... is a closed range (includes both). Ranges are useful in loops, slicing, and pattern matching with the ~= operator.
for i in 0..<5 { print(i) } // 0,1,2,3,4
for i in 0...5 { print(i) } // 0,1,2,3,4,5
let nums = Array(1...5) // [1,2,3,4,5]
let prefix = nums.prefix(3) // [1,2,3]
if 5...10 ~= 7 { print("in range") }Higher-Order Functions
map transforms each element, filter selects matching elements, reduce combines all into one. These are the foundation of functional programming in Swift and enable concise, readable data pipelines.
let nums = [1, 2, 3, 4, 5]
let doubled = nums.map { $0 * 2 } // [2,4,6,8,10]
let evens = nums.filter { $0 % 2 == 0 } // [2,4]
let sum = nums.reduce(0, +) // 15
let strs = nums.map { String($0) } // ["1","2",...]
print(doubled, evens, sum)Control Flow
If / Else If / Else
Standard conditional branching. Conditions must be Boolean (Bool type). Swift requires braces even for single statements to prevent bugs like Apple's goto fail vulnerability.
let score = 85
if score >= 90 {
print("A")
} else if score >= 80 {
print("B")
} else if score >= 70 {
print("C")
} else {
print("F")
}Switch (Pattern Matching)
Swift switch is powerful: supports value binding, tuples, where guards, and ranges. It must be exhaustive (default covers remaining cases) and does not fall through by default.
let point = (2, 0)
switch point {
case (0, 0):
print("origin")
case (_, 0):
print("on x-axis")
case (0, _):
print("on y-axis")
case let (x, y) where x == y:
print("on diagonal")
default:
print("elsewhere: \(point.0),\(point.1)")
}For-In Loops
For-in iterates over ranges, arrays, dictionaries, and any Sequence. Use enumerated() when you need the index. Use _ to ignore loop variables you don't use.
for i in 0..<5 { print(i) }
let fruits = ["apple", "banana"]
for (i, fruit) in fruits.enumerated() {
print("\(i): \(fruit)")
}
let dict = ["a": 1, "b": 2]
for (k, v) in dict { print("\(k)=\(v)") }
for _ in 0..<3 { print("tick") }While & Repeat-While
while checks the condition before each iteration; repeat-while checks after (like do-while in C). Use repeat-while when the body must execute at least once.
var n = 5
while n > 0 {
print(n)
n -= 1
}
var x = 0
repeat {
x += 1
} while x < 3
print(x) // 3Guard (Early Exit)
guard provides early exit when prerequisites aren't met. Unwrapped optionals remain available in the rest of the scope, reducing nesting. Prefer guard over deep if-let nesting.
func greet(_ name: String?) {
guard let n = name, !n.isEmpty else {
print("No name provided")
return
}
print("Hello, \(n)")
// n is unwrapped and available here
}
greet("Alice")
greet(nil)Functions & Closures
Function Definition
Use _ to omit argument labels for readability. Default parameter values make parameters optional. Functions with a single expression have implicit return (Swift 5.9+).
func greet(_ name: String, greeting: String = "Hello") -> String {
return "\(greeting), \(name)!"
}
print(greet("Alice")) // Hello, Alice!
print(greet("Bob", greeting: "Hi")) // Hi, Bob!
func add(_ a: Int, _ b: Int) -> Int { a + b }
print(add(3, 4)) // 7Multiple Return Values
Tuples enable returning multiple named values. Returning an optional tuple signals possible failure. Access via r.min or r.0—named elements are clearer.
func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
guard let first = nums.first else { return nil }
var mn = first, mx = first
for n in nums { mn = min(mn, n); mx = max(mx, n) }
return (mn, mx)
}
if let r = minMax([3, 1, 4, 1, 5]) {
print("min=\(r.min), max=\(r.max)")
}Closures
Closures are self-contained blocks of functionality. Use $0, $1 for shorthand argument names. Trailing closure syntax (omitting the last argument label) is idiomatic for map/filter/reduce.
let square: (Int) -> Int = { x in x * x }
print(square(5)) // 25
let add: (Int, Int) -> Int = { $0 + $1 }
print(add(3, 4)) // 7
let nums = [1, 2, 3]
let doubled = nums.map { $0 * 2 }
print(doubled) // [2, 4, 6]Escaping & Auto-Closures
@escaping marks closures stored or called after the function returns (needed for async callbacks). @autoclosure wraps an expression in a closure, delaying its evaluation (used in assert).
var handlers: [() -> Void] = []
func saveHandler(_ fn: @escaping () -> Void) {
handlers.append(fn)
}
saveHandler { print("done") }
handlers.first?()
// @autoclosure delays evaluation
func logIfTrue(_ cond: @autoclosure () -> Bool) {
if cond() { print("true") }
}
logIfTrue(2 > 1)Inout Parameters
inout parameters allow functions to modify the caller's variable (pass by reference). Use sparingly—prefer returning new values for clarity. The & prefix marks the mutation site.
func swap(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 10, y = 20
swap(&x, &y)
print(x, y) // 20 10Classes & Structs
Struct (Value Type)
Structs are value types—copied on assignment. Use for simple data containers. Computed properties (distance) calculate on access. mutating methods modify self.
struct Point {
var x: Double
var y: Double
var distance: Double {
(x * x + y * y).squareRoot()
}
mutating func moveBy(dx: Double, dy: Double) {
x += dx; y += dy
}
}
var p = Point(x: 3, y: 4)
print(p.distance) // 5.0Class (Reference Type)
Classes are reference types—shared via references, support inheritance and deinit. Use when you need identity, shared mutable state, or Objective-C interoperability. Otherwise prefer structs.
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
deinit { print("\(name) deallocated") }
}
let p = Person(name: "Alice", age: 30)
print(p.name) // AliceInheritance & Override
Use override to redefine a superclass method. Swift uses dynamic dispatch so the subclass version is called. Mark methods final to prevent further overriding for performance.
class Animal {
func speak() { print("...") }
}
class Dog: Animal {
override func speak() { print("Woof!") }
}
class Cat: Animal {
override func speak() { print("Meow!") }
}
let pets: [Animal] = [Dog(), Cat()]
for pet in pets { pet.speak() }Properties (Computed & Lazy)
Computed properties have get/set blocks; set uses newValue by default. lazy defers initialization until first access—useful for expensive or rarely-needed properties. Must be var.
class Circle {
var radius: Double
var area: Double {
get { .pi * radius * radius }
set { radius = (newValue / .pi).squareRoot() }
}
lazy var heavy = loadExpensiveData()
init(radius: Double) { self.radius = radius }
}
func loadExpensiveData() -> String { "data" }Property Observers
willSet/didSet observe property changes. Use didSet to validate or react to changes (e.g., clamping values, triggering UI updates). Observers don't fire during init.
class Counter {
var count: Int = 0 {
willSet { print("about to set \(newValue)") }
didSet {
print("was \(oldValue), now \(count)")
if count > 10 { count = 10 }
}
}
}
let c = Counter()
c.count = 5
c.count = 20 // clamped to 10Protocols & Extensions
Protocol Definition
Protocols define a blueprint of methods and properties. Types conform by implementing them. Use for abstraction, polymorphism, and decoupling—similar to interfaces in Java/C#.
protocol Greetable {
var name: String { get }
func greet() -> String
}
struct User: Greetable {
let name: String
func greet() -> String { "Hi, \(name)" }
}
let u = User(name: "Alice")
print(u.greet()) // Hi, AliceProtocol Extensions (Default Impl)
Protocol extensions provide default implementations. This enables retroactive modeling and code reuse without a base class. A powerful feature for sharing behavior across unrelated types.
protocol Describable {
func describe() -> String
}
extension Describable {
func describe() -> String { "A \(type(of: self))" }
}
struct Box: Describable { }
let b = Box()
print(b.describe()) // A BoxExtensions
Extensions add functionality to existing types (even ones you don't own, like Int). Use to organize code, add computed properties, or conform to protocols. Cannot add stored properties.
extension Int {
var squared: Int { self * self }
func times(_ block: () -> Void) {
for _ in 0..<self { block() }
}
}
print(5.squared) // 25
3.times { print("hi") } // prints hi 3 timesGenerics
Generics write flexible, reusable code that works with any type while preserving type safety. Use <T> for type parameters. Constraints (where T: Equatable) restrict allowed types.
func stackOf<T>(_ items: T...) -> [T] {
var arr: [T] = []
for item in items { arr.append(item) }
return arr
}
struct Stack<Element> {
private var items: [Element] = []
mutating func push(_ e: Element) { items.append(e) }
}
let s = stackOf(1, 2, 3) // [Int]Protocol with Associated Type
Associated types let protocols use placeholder types, like generics for protocols. The conforming type specifies the actual type. Use typealias to make it explicit, or let Swift infer it.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int
private var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}Error Handling
Defining & Throwing Errors
Errors conform to the Error protocol (usually enums). throw signals an error. Associated values (coinsNeeded) carry context. Use throws to mark functions that can fail.
enum VendingError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
func vend(item: String, coins: Int) throws -> String {
guard item == "Candy" else { throw VendingError.invalidSelection }
guard coins >= 2 else { throw VendingError.insufficientFunds(coinsNeeded: 2) }
return "Dispensing \(item)"
}Do-Catch & Try
do-catch handles thrown errors. try marks throwing calls. Pattern-match on specific cases for targeted handling. A catch-all handles unexpected errors. Errors propagate up the call stack.
do {
let result = try vend(item: "Candy", coins: 1)
print(result)
} catch VendingError.insufficientFunds(let needed) {
print("Need \(needed) more coins")
} catch VendingError.outOfStock {
print("Sold out")
} catch {
print("Other error: \(error)")
}Try? / Try!
try? converts errors to nil (returns Optional). try! assumes success and crashes on error—use only when failure is truly impossible. Prefer try? with optional binding for graceful degradation.
let result1 = try? vend(item: "Candy", coins: 1)
print(result1) // nil (error converted to optional)
let result2 = try! vend(item: "Candy", coins: 5)
print(result2) // Dispensing Candy (crashes if error)
if let r = try? vend(item: "Candy", coins: 5) {
print(r)
}Result Type
Result encodes success or failure as a value, enabling async error handling without throw. Use for callbacks, async APIs, or when you want to store/chain outcomes. .get() throws on failure.
enum FetchError: Error { case network, parse }
func fetch(_ url: String) -> Result<String, FetchError> {
url.isEmpty ? .failure(.network) : .success("data from \(url)")
}
switch fetch("https://api.com") {
case .success(let data): print(data)
case .failure(let err): print("Error: \(err)")
}
let value = try? fetch("").get()Defer (Cleanup)
defer schedules cleanup code to run when the scope exits, regardless of how (normal return, throw, error). Use for resource release (files, locks). Multiple defers run in LIFO order.
func processFile(_ path: String) throws {
let handle = openFile(path)
defer { closeFile(handle) }
// ... work that might throw ...
let data = try read(handle)
// closeFile always runs, even if throw
print("processed \(data)")
}File I/O & Date/Time
Read & Write Files
Foundation provides simple file APIs. write(toFile:atomically:) writes safely (temp file + rename). Use String(contentsOfFile:) for text. For large files, use FileHandle for streaming.
import Foundation
let path = "test.txt"
let content = "Hello, File I/O!"
try content.write(toFile: path, atomically: true,
encoding: .utf8)
let read = try String(contentsOfFile: path, encoding: .utf8)
print(read) // Hello, File I/O!
let lines = read.split(separator: "\n")
print(lines.count)URL & FileManager
FileManager handles filesystem operations (create, delete, check existence). Use URLs (not paths) for modern APIs. .documentDirectory is the app's persistent storage on iOS/macOS.
import Foundation
let fm = FileManager.default
let url = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("data.json")
try "data".write(to: url, atomically: true, encoding: .utf8)
let exists = fm.fileExists(atPath: url.path)
print("Exists: \(exists)")
try fm.removeItem(at: url) // deleteDate & DateFormatter
Date represents a point in time (UTC internally). DateFormatter converts between Date and String—always set locale to en_US_POSIX for fixed-format parsing to avoid locale bugs.
import Foundation
let now = Date()
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
let str = fmt.string(from: now)
print(str) // e.g. 2024-01-15 14:30:00
let parsed = fmt.date(from: "2024-01-01 00:00:00")
print(parsed ?? "invalid")JSON Encoding/Decoding
Codable automates JSON serialization. JSONEncoder/JSONDecoder handle conversion. Conform your types to Codable—the compiler synthesizes the logic. Use CodingKeys to customize key names.
struct User: Codable {
let name: String
let age: Int
}
let user = User(name: "Alice", age: 30)
let json = try JSONEncoder().encode(user)
let str = String(data: json, encoding: .utf8)!
print(str) // {"name":"Alice","age":30}
let decoded = try JSONDecoder().decode(User.self, from: json)
print(decoded.name) // AliceDate Calculations
Calendar handles date arithmetic respecting time zones and DST. Use dateComponents to extract fields or compute differences. Never use raw seconds for date math—use Calendar APIs.
import Foundation
let cal = Calendar.current
let now = Date()
let tomorrow = cal.date(byAdding: .day, value: 1, to: now)!
let components = cal.dateComponents([.year, .month, .day], from: now)
print(components.year!, components.month!)
let diff = cal.dateComponents([.hour], from: now, to: tomorrow)
print(diff.hour!) // 24Concurrency & Async
Async / Await
async/await (Swift 5.5+) makes async code read like sync code. Task creates a new async context. async let runs concurrently and awaits all. Eliminates callback hell—prefer over completion handlers.
func fetchUser(_ id: Int) async -> String {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "User \(id)"
}
Task {
let user = await fetchUser(42)
print(user) // User 42
async let u1 = fetchUser(1)
async let u2 = fetchUser(2)
let users = await [u1, u2] // concurrent
print(users)
}Task & Cancellation
Task represents a unit of async work. Cooperative cancellation via Task.isCancelled—long-running tasks should check periodically. task.value awaits the result. Cancellation is cooperative, not forced.
let task = Task {
for i in 1...100 {
if Task.isCancelled { return }
try? await Task.sleep(nanoseconds: 100_000_000)
print(i)
}
}
Task {
try? await Task.sleep(nanoseconds: 500_000_000)
task.cancel() // request cancellation
}
await task.value // wait for completionActors (Thread Safety)
Actors (Swift 5.5+) protect mutable state from data races by serializing access. All access goes through await. Use instead of locks/queues for shared state. The compiler verifies safety.
actor Counter {
private var count = 0
func increment() { count += 1 }
func value() -> Int { count }
}
let counter = Counter()
Task {
await counter.increment()
print(await counter.value())
}
// Actors serialize access—no data racesGCD (Grand Central Dispatch)
GCD is the traditional concurrency API. DispatchQueue.global() for background work, .main for UI updates. QoS (.userInitiated, .background) prioritizes tasks. Still useful for non-async code.
import Dispatch
DispatchQueue.global(qos: .userInitiated).async {
let result = heavyComputation()
DispatchQueue.main.async {
print("UI update: \(result)")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("delayed by 2s")
}Async Sequence
AsyncSequence enables iterating over async values (like a stream). Use for await to consume. Ideal for paginated APIs, server-sent events, or any source producing values over time.
let urls = ["url1", "url2", "url3"]
func fetchAll() async {
for url in urls {
let data = await fetchData(url)
print(data)
}
}
func fetchData(_ url: String) async -> String {
"data from \(url)"
}
// AsyncSequence: for await item in stream { ... }Protocols Deep Dive
Protocol with Associated Types
Associated types (associatedtype) let protocols declare a placeholder type that conforming types specify. This is Swift's equivalent of generic type parameters for protocols. The type is inferred from the conforming type's methods. Use 'where' clauses to constrain associated types. PATs (Protocols with Associated Types) can't be used as existential types directly without type erasure or (Swift 5.7+) 'any Container'.
protocol Container {
associatedtype Item
var count: Int { get }
mutating func append(_ item: Item)
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
// associatedtype Item inferred as Int
private var items: [Int] = []
var count: Int { items.count }
mutating func append(_ item: Int) { items.append(item) }
subscript(i: Int) -> Int { items[i] }
}
// Generic constraint with associated type
func sum<C: Container>(_ c: C) -> Int where C.Item == Int {
var total = 0
for i in 0..<c.count { total += c[i] }
return total
}Protocol Extensions (Default Implementations)
Protocol extensions provide default implementations—conforming types get the method for free but can override it. This is Swift's way to add functionality to types retroactively. Constrained extensions (where Element: Numeric) add methods only to types meeting the constraint. This is how the standard library adds map/filter/reduce to all Collections.
protocol Describable {
var description: String { get }
}
// Default implementation via extension
extension Describable {
var description: String { "A \(type(of: self))" }
}
struct Point: Describable {
let x, y: Int
// Uses default description: "A Point"
}
struct Person: Describable {
let name: String
var description: String { "Person named (name)" } // override
}
// Extension with constraints
extension Collection where Element: Numeric {
var sum: Element { reduce(0, +) }
}
[1, 2, 3].sum // 6Protocol Composition and Existentials
Protocol composition (A & B) requires a value to conform to multiple protocols. Existential types (any Protocol) can hold any conforming type but have runtime dispatch overhead—a witness table lookup per call. Use 'some Protocol' (opaque return type) when you return one specific type but want to hide it. Prefer generics over existentials for performance; use existentials when you need heterogeneous collections.
protocol Named { var name: String { get } }
protocol Aged { var age: Int { get } }
struct Person: Named, Aged {
let name: String
let age: Int
}
// Protocol composition: requires both protocols
func wishHappyBirthday(to celebrant: Named & Aged) {
print("Happy birthday, (celebrant.name), you're (celebrant.age)!")
}
let p = Person(name: "Alice", age: 30)
wishHappyBirthday(to: p)
// Existential types (any) — type erasure overhead
let items: [any Named] = [p, Pet(name: "Rex")]
for item in items { print(item.name) }
// Swift 5.7: 'any' keyword required for existentials
// 'some' for opaque types (returns one concrete type)Protocol-Oriented Programming
Protocol-Oriented Programming is Swift's paradigm shift from OOP. Instead of class hierarchies, design around protocols with default implementations. Benefits: works with value types (structs/enums), supports retroactive conformance (extend types you don't own), enables multiple 'inheritance' (a type can conform to many protocols). Classes are still useful for reference semantics and Objective-C interop, but structs+protocols are preferred for most models.
// POP: design around protocols, not inheritance
protocol Drawable {
func draw(on canvas: Canvas)
}
extension Drawable {
// Default implementation + polymorphism
func drawTwice(on canvas: Canvas) {
draw(on: canvas)
draw(on: canvas)
}
}
struct Circle: Drawable { let radius: Double
func draw(on canvas: Canvas) { /* ... */ }
}
struct Square: Drawable { let side: Double
func draw(on canvas: Canvas) { /* ... */ }
}
// Value types + protocols = no inheritance needed
let shapes: [any Drawable] = [Circle(radius: 5), Square(side: 3)]
shapes.forEach { $0.draw(on: canvas) }
// Retroactive modeling (extend types you don't own)
extension Int: Drawable {
func draw(on canvas: Canvas) { /* draw the number */ }
}Custom Protocol Witnesses
Protocols as dependencies (Repository) enable swapping implementations (real vs mock) for testing and flexibility. Associated types make protocols generic. This is Swift's dependency injection—inject a protocol-conforming type. The 'protocol witness' is the conforming type that provides concrete behavior. This pattern (repository, data source) is common in Swift architecture (VIPER, Clean Architecture).
// Protocol with requirements
protocol Repository {
associatedtype Entity
func get(_ id: Int) -> Entity?
func save(_ entity: Entity)
}
// Concrete implementation
struct UserRepo: Repository {
typealias Entity = User
func get(_ id: Int) -> User? { /* fetch */ nil }
func save(_ user: User) { /* persist */ }
}
// Generic function using protocol
func display<R: Repository>(_ repo: R, id: Int) where R.Entity: CustomStringConvertible {
if let entity = repo.get(id) {
print(entity.description)
}
}
// Test double for testing
struct MockUserRepo: Repository {
typealias Entity = User
var stored: [Int: User] = [:]
func get(_ id: Int) -> User? { stored[id] }
func save(_ user: User) { /* no-op for tests */ }
}Generics Deep Dive
Generic Functions and Types
Generics write flexible, reusable code that works with any type while preserving type safety. T is a type parameter (placeholder). The compiler generates specialized versions for performance (no boxing). Generic types (Stack<T>) maintain their type parameter. Element is Array's generic type. Generics are resolved at compile time—no runtime overhead, unlike existentials.
// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
let temp = a; a = b; b = temp
}
var x = 1, y = 2
swap(&x, &y) // x=2, y=1
// Generic type
struct Stack<T> {
private var items: [T] = []
mutating func push(_ item: T) { items.append(item) }
mutating func pop() -> T? { items.popLast() }
}
var intStack = Stack<Int>()
intStack.push(42)
var strStack = Stack<String>()
strStack.push("hello")
// Generic method on non-generic type
extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}Type Constraints
Type constraints limit what types can be used: T: Equatable requires T to conform to a protocol, T: SomeClass requires a class hierarchy. The where clause adds more complex constraints (e.g., matching associated types). Constraints let you use the protocol's methods (== for Equatable, < for Comparable). Without constraints, you can only assign and pass T around—no operations.
// Constraint: T must conform to Equatable
func indexOf<T: Equatable>(_ item: T, in array: [T]) -> Int? {
for (i, elem) in array.enumerated() {
if elem == item { return i } // needs Equatable
}
return nil
}
// Multiple constraints
func max<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
// Constraint on associated type
protocol Repository {
associatedtype Entity: Identifiable
func find(_ id: Entity.ID) -> Entity?
}
// where clause for complex constraints
func merge<C1: Collection, C2: Collection>(
_ c1: C1, _ c2: C2
) -> [C1.Element] where C1.Element == C2.Element {
Array(c1) + Array(c2)
}Opaque Types (some)
Opaque types (some Protocol, Swift 5.1) return a specific concrete type that's hidden from the caller. Unlike 'any' (existential), the type is fixed and known to the compiler—no boxing, compile-time dispatch. This is the foundation of SwiftUI's 'some View'. Use 'some' when you want to hide the concrete type but maintain performance. The caller can use protocol methods but can't rely on the specific type.
// Opaque return type: returns ONE concrete type
// (hidden from caller, but fixed per call site)
func makeStack() -> some Collection<Int> {
return [1, 2, 3] // concrete type is [Int]
}
let s = makeStack() // type is 'some Collection<Int>'
print(s.count) // 3
// Useful for hiding implementation details
protocol Shape { func draw() }
struct Circle: Shape { func draw() { print("circle") } }
func makeShape() -> some Shape {
return Circle() // caller doesn't know it's Circle
}
// Difference from existential (any):
// - some: one concrete type, no boxing, compile-time dispatch
// - any: can be different types, runtime dispatch, boxing
// Opaque types in properties
struct View {
var body: some View { Text("Hello") } // SwiftUI
}Conditional Conformance
Conditional conformance makes a type conform to a protocol only when its type parameters meet certain conditions. Array<Int> is Equatable because Int is; Array<MyStruct> isn't unless MyStruct is. This propagates: [[Int]] is Equatable because [Int] is. The standard library uses this extensively—Array, Optional, Dictionary all conditionally conform to Equatable, Hashable, Codable.
// Array conforms to Equatable only if Element is Equatable
extension Array: Equatable where Element: Equatable {
static func == (lhs: [Element], rhs: [Element]) -> Bool {
return lhs.count == rhs.count && lhs.elementsEqual(rhs)
}
}
[1, 2, 3] == [1, 2, 3] // true (Int: Equatable)
// [SomeStruct] == [SomeStruct] // error if SomeStruct isn't Equatable
// Conditional conformance for Hashable
extension Stack: Equatable where T: Equatable {
static func == (lhs: Stack, rhs: Stack) -> Bool {
return lhs.items == rhs.items
}
}
// Enables: [Stack<Int>] == [Stack<Int>] (transitively)Result and Generic Error Handling
Result<Success, Failure> is a generic enum for operations that can fail, carrying either a value or an error. It's useful when you want to defer error handling or store results. map transforms the success value; flatMap chains operations that can also fail. Result { try ... } converts throwing functions. .get() converts back to throwing. With async/await, Result is less necessary but still useful for stored/passed errors.
// Result: built-in generic for success/failure
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
func fetchUser(_ id: Int) -> Result<User, FetchError> {
if id < 0 { return .failure(.invalidId) }
return .success(User(id: id))
}
switch fetchUser(42) {
case .success(let user): print(user)
case .failure(let err): print(err)
}
// map/flatMap for chaining
let result = fetchUser(1)
.map { $0.name }
.flatMap { name in fetchAvatar(name) }
// Throwing function → Result
let result = Result { try throwingFunction() }
// Result → throwing
let value = try result.get()ARC & Memory Management
Automatic Reference Counting (ARC)
ARC (Automatic Reference Counting) manages memory for class instances (reference types). Each strong reference increments the count; removing one decrements. When count hits 0, the object is freed immediately (deterministic, unlike garbage collection). Structs/enums (value types) don't use ARC—they're copied. ARC can't handle reference cycles—you must use weak/unowned to break them.
class Person {
let name: String
init(name: String) { self.name = name; print("\(name) initialized") }
deinit { print("\(name) deinitialized") }
}
var p1: Person? = Person(name: "Alice") // "Alice initialized"
var p2 = p1 // strong ref count = 2
p1 = nil // ref count = 1, not deinitialized
p2 = nil // ref count = 0, "Alice deinitialized"
// ARC tracks strong references:
// - +1 on assignment/copy
// - -1 when variable goes out of scope
// - Object freed when count hits 0
// Unlike GC, deterministic (freed immediately when count=0)Strong, Weak, and Unowned References
Strong references (default) keep objects alive. weak references don't keep the object alive and become nil when the object is freed (must be optional var). unowned references don't keep the object alive but are non-optional—use only when you're certain the referenced object outlives the reference (crash if accessed after dealloc). Use weak for delegate patterns; unowned for parent-child where parent always outlives child.
class Person {
var name: String
var apartment: Apartment? // strong
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class Apartment {
var unit: String
weak var tenant: Person? // weak breaks cycle
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) deinit") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice?.apartment = apt
apt?.tenant = alice // weak, doesn't keep alice alive
alice = nil // "Alice deinit" (no strong cycle)
apt = nil // "Apartment 4A deinit"
// unowned: non-optional, assumed to always have a value
// Use when the referenced object outlives the reference
class Customer {
var card: CreditCard? // strong
}
class CreditCard {
unowned let owner: Customer // non-nil, non-optional
init(owner: Customer) { self.owner = owner }
}Retain Cycles and Closures
Closures capture references strongly by default. If a class stores a closure that captures self, you have a retain cycle (both keep each other alive → memory leak). Fix with capture lists: [weak self] (self becomes optional, use guard let) or [unowned self] (non-optional, crashes if nil). Always use weak self in closures stored as properties or passed to long-lived objects (observers, async tasks).
class ViewModel {
var name = "VM"
var onUpdate: (() -> Void)?
func setup() {
// BAD: closure captures self strongly → cycle
onUpdate = {
print(self.name) // strong capture
}
// ViewModel holds closure, closure holds ViewModel → leak!
// GOOD: capture list with weak self
onUpdate = { [weak self] in
guard let self = self else { return }
print(self.name)
}
// GOOD: unowned if self outlives closure
onUpdate = { [unowned self] in
print(self.name)
}
}
deinit { print("VM deinit") }
}Escaping and Non-escaping Closures
Non-escaping closures (default) run within the function and are discarded—no retain cycle risk, no need for self. in the closure. @escaping closures outlive the function (stored in a property, dispatched async, etc.)—they can cause retain cycles, so you must use self. explicitly and consider [weak self]. The compiler enforces this. Most completion handlers are @escaping; map/filter/reduce are non-escaping.
class DataLoader {
var storedHandler: (() -> Void)?
// @escaping: closure outlives the function (stored or async)
func load(with handler: @escaping (Data) -> Void) {
storedHandler = { handler(cachedData) } // stored
DispatchQueue.global().async {
handler(self.fetch()) // escapes via async
}
}
// Non-escaping (default): closure runs during the function
func process(_ transform: (Int) -> Int) -> [Int] {
return [1, 2, 3].map(transform) // runs now, no storage
}
// No need for [weak self]—self is alive during the call
}
// @escaping requires explicit self. in closures
class View {
func load() {
DataLoader().load { [self] data in // self. required
print(data)
}
}
}Memory Safety and exclusivity
Swift enforces exclusivity—a variable can't be accessed (read+write or write+write) simultaneously from overlapping scopes. This prevents data races and undefined behavior. inout parameters get exclusive access for the call duration. Struct mutating methods hold exclusive access to self. Actors (Swift 5.5+) enforce exclusivity at the language level for shared mutable state, eliminating data races by design.
// Exclusivity: Swift prevents overlapping access to a variable
// This is a compile-time or runtime error:
var step = 1
func increment(_ n: inout Int) {
step += 1 // accessing global while n is inout to step
}
// increment(&step) // ERROR: overlapping access
// Fix with explicit copy
func safeIncrement(_ n: inout Int) {
var copy = step
copy += 1
step = copy
}
// Struct mutating methods have exclusive access
struct Counter {
var count = 0
mutating func increment(_ other: inout Counter) {
count += other.count // ok, different instances
}
}
// 'inout' parameters: exclusive access for duration
// 'isolation' (Swift 5.5+): actors enforce exclusivitySwiftUI Basics
Views and Modifiers
SwiftUI is Apple's declarative UI framework. Views are structs conforming to View with a 'body' property. Modifiers (.font, .padding) return new wrapped views—they don't mutate. Views are lightweight value types; SwiftUI diffs them to update the actual UI. The declarative style describes what the UI should look like for a given state, and SwiftUI handles the transitions.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Text("Hello, World!")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
Button(action: { print("tapped") }) {
HStack {
Image(systemName: "star.fill")
Text("Tap me")
}
}
.padding()
.background(Color.orange)
.cornerRadius(10)
}
.padding()
}
}
// Views are value types (structs), declarative
// Modifiers return new views (immutability)State and Binding
@State is for local, mutable view state—when it changes, SwiftUI re-renders the view. @Binding lets a child view read/write a parent's @State via the $ prefix (creates a binding). State should be the single source of truth; pass bindings down, not values to modify. For shared/complex state across views, use @StateObject/@ObservedObject/@EnvironmentObject with ObservableObject.
struct CounterView: View {
@State private var count = 0 // local, mutable state
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") { count += 1 }
}
}
}
// @Binding: child receives a reference to parent's state
struct StepperView: View {
@Binding var value: Int
var body: some View {
HStack {
Button("-") { value -= 1 }
Text("\(value)")
Button("+") { value += 1 }
}
}
}
// Parent passes binding with $
struct ParentView: View {
@State var score = 0
var body: some View {
StepperView(value: $score) // $ creates a Binding
}
}ObservableObject and EnvironmentObject
ObservableObject (class) is for shared state across multiple views. @Published properties trigger view updates when changed. @StateObject creates and owns the object (use at the top of the hierarchy). @EnvironmentObject injects the object into the view tree—any descendant can access it without passing through bindings. @ObservedObject receives an existing object (doesn't own it). Use @StateObject for ownership, @ObservedObject/@EnvironmentObject for receiving.
// Model: ObservableObject for shared state
class AppModel: ObservableObject {
@Published var username: String = "" // @Published triggers UI update
@Published var isLoggedIn: Bool = false
func login() {
// ... auth logic ...
isLoggedIn = true
}
}
// @StateObject: owns the model (created once)
struct RootView: View {
@StateObject var model = AppModel()
var body: some View {
// Inject into environment
ContentView().environmentObject(model)
}
}
// @EnvironmentObject: receives from environment
struct ProfileView: View {
@EnvironmentObject var model: AppModel
var body: some View {
Text("Hello, \(model.username)")
}
}
// @ObservedObject: receives from parent (doesn't own)
struct LoginView: View {
@ObservedObject var model: AppModel
}Lists and Navigation
List renders scrollable rows (like UITableView). Items must conform to Identifiable (or you provide id:). NavigationStack (iOS 16+) manages navigation; NavigationLink pushes destinations. LazyVStack/LazyHStack load content lazily for performance. ForEach is for repeating views inside other containers. Use List for tabular data, ScrollView+LazyVStack for custom layouts.
struct Item: Identifiable {
let id = UUID()
let name: String
}
struct ListView: View {
let items = [Item(name: "Apple"), Item(name: "Banana")]
var body: some View {
NavigationStack {
List(items) { item in
NavigationLink(item.name) {
DetailView(item: item)
}
}
.navigationTitle("Fruits")
}
}
}
struct DetailView: View {
let item: Item
var body: some View {
Text("Detail: \(item.name)")
.navigationTitle(item.name)
}
}
// ForEach for custom layouts
ScrollView {
LazyVStack {
ForEach(items) { item in
Text(item.name)
}
}
}Forms and Sheets
Form automatically styles controls for settings screens (grouped, platform-appropriate). Common controls: TextField, Toggle, Slider, Picker, Stepper. .sheet presents a modal; .fullScreenCover for full-screen. Bindings ($) connect controls to state. Form adapts to platform (iOS grouped list, macOS form layout). Use Form for data entry and settings; use VStack for custom layouts.
struct SettingsView: View {
@State private var name = ""
@State private var notifications = true
@State private var volume: Double = 0.5
@State private var theme = "Light"
@State private var showSheet = false
var body: some View {
Form {
Section("Profile") {
TextField("Name", text: $name)
Toggle("Notifications", isOn: $notifications)
}
Section("Audio") {
Slider(value: $volume, in: 0...1)
}
Section("Appearance") {
Picker("Theme", selection: $theme) {
Text("Light").tag("Light")
Text("Dark").tag("Dark")
}
}
Button("Show Modal") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
Text("Modal content")
}
}
}Closures Deep Dive
Closure Syntax Variations
Closures have many syntax forms. Full form: { (params) -> Type in body }. Type inference lets you omit types. Shorthand arguments ($0, $1) replace named params. Trailing closure syntax moves the closure outside () when it's the last arg. Multiple trailing closures (Swift 5.3+) name additional closures. Use the most concise form that's still readable—shorthand args are great for short closures like map.
// Full closure
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
// Type inference
let add2: (Int, Int) -> Int = { a, b in a + b }
// Shorthand argument names ($0, $1...)
let add3: (Int, Int) -> Int = { $0 + $1 }
// Trailing closure syntax
let result = [1, 2, 3].map { $0 * 2 } // [2, 4, 6]
// Multiple trailing closures (Swift 5.3+)
Button {
print("action") // action
} label: {
Text("Click") // label
}
// No return for Void closures
let log: (String) -> Void = { msg in print(msg) }Capturing Values
Closures capture variables from their enclosing scope. By default, they capture by reference (changes to the captured variable are visible). Capture lists [foo] capture by value (a snapshot at closure creation). For reference types (classes), [weak self] or [unowned self] break retain cycles. Value types (structs) captured by reference still see updates because the closure holds a box. Capture lists go before the parameter list.
// Closures capture surrounding state
func makeIncrementer(amount: Int) -> () -> Int {
var total = 0
return {
total += amount // captures total and amount
return total
}
}
let inc = makeIncrementer(amount: 5)
inc() // 5
inc() // 10
// total persists between calls (captured by reference)
// Capture list controls how
class Foo { var x = 0 }
let foo = Foo()
let closure = { [foo] in // captures foo by value (snapshot)
print(foo.x)
}
foo.x = 100
closure() // prints 0 (captured the old value)
// Without capture list, foo is captured by reference
let refClosure = { print(foo.x) }
foo.x = 200
refClosure() // prints 200Escaping and Autoclosure
@autoclosure wraps an expression in a zero-argument closure automatically, so callers write the expression directly (no braces). This enables lazy evaluation—assert() uses it to skip the condition in release builds. Combined with @escaping, you can defer evaluation. The && and || operators use @autoclosure for short-circuit evaluation. Use @autoclosure sparingly—it hides that code is deferred, which can confuse readers.
// @autoclosure: wraps an expression in a closure automatically
func log(_ condition: @autoclosure () -> Bool, _ message: String) {
if condition() { // evaluated here, not at call
print(message)
}
}
log(2 > 1, "two is greater") // no { } needed
// assert uses @autoclosure to avoid evaluating in release
assert(isValid, "Invalid state") // isValid not checked in release
// @autoclosure + @escaping
func deferred(_ block: @autoclosure @escaping () -> Int) {
DispatchQueue.main.async { print(block()) }
}
deferred(expensiveComputation()) // runs later, on main
// Short-circuit evaluation with @autoclosure
func &&(lhs: @autoclosure () -> Bool, rhs: @autoclosure () -> Bool) -> Bool {
return lhs() ? rhs() : false // rhs not evaluated if lhs is false
}Higher-Order Functions
Swift's higher-order functions (map, filter, reduce, flatMap, compactMap) enable functional programming on collections. map transforms, filter selects, reduce aggregates, flatMap flattens, compactMap removes nils. These return new collections (immutability). Chaining composes operations. For large datasets, use .lazy to avoid intermediate arrays. These are the foundation of functional Swift—prefer them over imperative for loops when transforming data.
let nums = [1, 2, 3, 4, 5, 6]
// map: transform each element
let doubled = nums.map { $0 * 2 } // [2, 4, 6, 8, 10, 12]
// filter: keep elements matching predicate
let evens = nums.filter { $0 % 2 == 0 } // [2, 4, 6]
// reduce: combine all into one
let sum = nums.reduce(0, +) // 21
let product = nums.reduce(1, *) // 720
// flatMap: transform + flatten
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 } // [1, 2, 3, 4]
// compactMap: transform + filter nils
let strings = ["1", "abc", "3"]
let ints = strings.compactMap { Int($0) } // [1, 3]
// Chaining (lazy evaluation)
let result = nums
.filter { $0 % 2 == 0 }
.map { $0 * $0 }
.reduce(0, +) // 4 + 16 + 36 = 56Closures as Completion Handlers
Completion handlers were the standard async pattern before async/await (Swift 5.5). They're @escaping (run later) and often use Result for error handling. The downside is nested callbacks ('pyramid of doom'). async/await makes this linear and readable. New code should use async/await; completion handlers remain for delegate APIs and Objective-C interop. You can wrap completion handlers with withCheckedContinuation to use them with async/await.
// Completion handler pattern (pre-async/await)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let user = try? JSONDecoder().decode(User.self, from: data) else {
completion(.failure(DecodeError()))
return
}
completion(.success(user))
}.resume()
}
// Usage (pyramid of doom)
fetchUser(id: 42) { result in
switch result {
case .success(let user):
fetchAvatar(user) { avatarResult in
// nested...
}
case .failure(let error):
print(error)
}
}
// Modern: async/await replaces this
func fetchUserAsync(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}Property Wrappers
Defining a Property Wrapper
Property wrappers (@propertyWrapper) encapsulate reusable property behavior. The wrappedValue property is what users interact with. The init receives the initial value and any custom arguments. SwiftUI uses property wrappers extensively: @State, @Binding, @Published, @AppStorage. Define your own for validation, caching, logging, or default values. The wrapper is a struct/class that manages the storage.
@propertyWrapper
struct Clamped<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
self.range = range
}
}
// Usage
struct Slider {
@Clamped(0...100) var progress: Int = 50
}
var s = Slider()
s.progress = 150 // clamped to 100
s.progress = -10 // clamped to 0
print(s.progress) // 0Projected Value
The projectedValue (accessed via $) is a secondary interface to the property. SwiftUI uses this heavily: @State's $ gives a Binding for child views, @Published's $ gives a Publisher. Define projectedValue when the wrapper should expose something beyond the wrapped value (a binding, a publisher, the wrapper itself). The $ prefix syntax makes this ergonomic. Not all wrappers need a projectedValue—it's optional.
@propertyWrapper
struct Observable<T> {
var value: T
var wrappedValue: T {
get { value }
set { value = newValue; onChange() }
}
var onChange: () -> Void = {}
// projectedValue: accessed via $ (like @State's binding)
var projectedValue: Observable<T> { self }
}
struct View {
@Observable var count = 0
// count: the wrapped value (Int)
// $count: the projected value (Observable<Int>)
}
// SwiftUI's @State projects a Binding
struct Counter: View {
@State var value = 0
var body: some View {
Stepper("Value: \(value)", value: $value) // Binding<Int>
}
}Built-in Property Wrappers
SwiftUI provides many property wrappers: @State (local state), @Binding (parent state), @ObservedObject/@StateObject (external model), @EnvironmentObject (injected model), @AppStorage (UserDefaults), @SceneStorage (scene restoration), @FocusState (keyboard focus), @ScaledMetric (Dynamic Type), @Namespace (animations). Each manages storage and lifecycle. Combine's @Published triggers UI updates. Knowing which to use when is key to SwiftUI.
import SwiftUI
import Combine
struct ContentView: View {
@State var count = 0 // local view state
@Binding var externalValue: Int // parent-owned
@EnvironmentObject var model: AppModel // injected
@AppStorage("username") var username = "" // UserDefaults
@SceneStorage("draft") var draft = "" // per-scene storage
@FocusState var isFocused: Bool // focus management
@ScaledMetric var size = 16 // Dynamic Type scaling
@Namespace var animationNamespace // matched geometry
// Combine
@Published var data = [Item]() // in ObservableObject
}
// @AppStorage auto-syncs with UserDefaults
// @SceneStorage restores state per scene (iPad multitasking)Custom Wrappers for Validation
Property wrappers shine for cross-cutting concerns. NonEmpty validates on set. Cached computes lazily and memoizes. Other ideas: Logged (logs changes), Trimmed (trims whitespace), Formatted (formats on get), Clamped (range limits), UserDefaults-backed. Wrappers reduce boilerplate—you write the logic once and apply it with @. They compose: @Logged @Clamped(0...100) var value. Keep wrappers focused on one concern.
@propertyWrapper
struct NonEmpty {
private var value: String = ""
var wrappedValue: String {
get { value }
set {
guard !newValue.isEmpty else {
print("Warning: cannot set to empty")
return
}
value = newValue
}
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
@propertyWrapper
struct Cached<T> {
private var cached: T?
private var generator: () -> T
var wrappedValue: T {
mutating get {
if cached == nil { cached = generator() }
return cached!
}
}
init(wrappedValue: @autoclosure @escaping () -> T) {
self.generator = wrappedValue
}
}
struct Config {
@NonEmpty var name: String = "default"
@Cached var expensiveValue: Int = computeExpensive()
}Property Wrappers in Protocols and Generics
Property wrappers can be generic and used in protocols. However, they have limitations: a struct with wrapped properties doesn't get memberwise initializers (you must write init manually), and copying can be tricky (the wrapper is copied, including its state). For classes, this is less of an issue. Despite limitations, wrappers are powerful for declarative, reusable property behavior. SwiftUI's design relies heavily on them.
// Apply to protocol requirements
protocol Configurable {
@NonEmpty var name: String { get set }
}
// Generic property wrapper
@propertyWrapper
struct Validated<T> {
private var value: T?
let validator: (T) -> Bool
var wrappedValue: T? {
get { value }
set {
if let v = newValue, validator(v) { value = v }
}
}
init(wrappedValue: T?, validator: @escaping (T) -> Bool) {
self.validator = validator
self.wrappedValue = wrappedValue
}
}
struct Form {
@Validated(validator: { $0.count >= 8 }) var password: String? = nil
}
// Limitation: property wrappers in structs can't be
// initialized from another instance easily (copy issues).
// They work best as stored properties, not computed.Combine Framework
Publishers and Subscribers
Combine is Apple's reactive framework (like RxSwift). Publishers emit values; Subscribers receive them. sink creates a subscriber with closures. assign binds output to a property. Operators (map, filter, reduce) transform publishers declaratively. Combine is declarative—describe the pipeline, and values flow through. It's used for async operations, UI bindings, and event handling. Modern Swift prefers async/await, but Combine remains for complex pipelines.
import Combine
// Publisher: emits values over time
let publisher = [1, 2, 3].publisher
// Subscriber: receives values
publisher.sink { value in
print(value) // 1, 2, 3
}
// With completion
publisher.sink(receiveCompletion: { completion in
print("done: \(completion)")
}, receiveValue: { value in
print(value)
})
// Assign to a property
class ViewModel {
var data: String = ""
}
let vm = ViewModel()
Just("hello").assign(to: &vm.data)
// Operators transform publishers
[1, 2, 3, 4].publisher
.map { $0 * 2 }
.filter { $0 > 4 }
.sink { print($0) } // 6, 8@Published and ObservableObject
@Published wraps a property and emits its new value to subscribers whenever it changes. The $ prefix accesses the publisher. This bridges Combine with SwiftUI—@Published in an ObservableObject triggers view updates. Subscriptions are cancelled when the AnyCancellable is deallocated, so store them (e.g., in a Set). Use Combine for debouncing, throttling, combining multiple async sources—things async/await handles less elegantly.
class UserStore: ObservableObject {
@Published var name: String = "" // publishes on change
@Published var age: Int = 0
// $name is a Publisher<String, Never>
func demo() {
$name.sink { print("name changed to \($0)") }
name = "Alice" // prints "name changed to Alice"
}
}
// Combine with SwiftUI
struct ProfileView: View {
@ObservedObject var store: UserStore
var body: some View {
Text(store.name) // updates when name changes
}
}
// Cancellable: store subscriptions to keep them alive
class Service {
var cancellables = Set<AnyCancellable>()
let store = UserStore()
init {
store.$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { name in saveToServer(name) }
.store(in: &cancellables) // keep alive
}
}Operators
Combine operators transform, filter, combine, and time-control publishers. map/scan transform; filter/selectors choose; merge/zip/combineLatest combine multiple streams; debounce/throttle control timing. Operators return new publishers (immutability, chaining). debounce waits for quiet period (search-as-you-type); throttle limits rate (button taps). collect gathers all values into an array. These enable declarative reactive pipelines.
let publisher = [1, 2, 3, 4, 5].publisher
// Transforming
publisher.map { $0 * 2 } // [2, 4, 6, 8, 10]
publisher.scan(0, +) // [1, 3, 6, 10, 15] (running sum)
// Filtering
publisher.filter { $0 % 2 == 0 } // [2, 4]
publisher.removeDuplicates() // consecutive dupes
publisher.first() // just 1
// Combining
let p1 = [1, 2].publisher
let p2 = [3, 4].publisher
p1.merge(with: p2) // interleaved: 1, 2, 3, 4 (or any order)
p1.zip(p2) // pairs: (1,3), (2,4)
p1.combineLatest(p2) // latest of each
// Timing
publisher.delay(for: 1, scheduler: RunLoop.main)
publisher.throttle(for: 1, scheduler: .main, latest: true)
publisher.debounce(for: 0.5, scheduler: .main)
publisher.collect() // gather all into [1,2,3,4,5]Subjects (Manual Publishing)
Subjects are mutable publishers you can send values to manually—bridging imperative and reactive code. PassthroughSubject broadcasts without storing (event stream). CurrentValueSubject stores the latest value (state). New subscribers to CurrentValueSubject immediately get the current value. Use Subjects to wrap delegates, notifications, or UI events into Combine pipelines. @Published is essentially a CurrentValueSubject integrated with ObservableObject.
import Combine
// PassthroughSubject: broadcasts to subscribers, no current value
let subject = PassthroughSubject<Int, Never>()
subject.sink { print($0) } // subscriber 1
subject.send(1) // prints 1
subject.send(2) // prints 2
// CurrentValueSubject: holds current value, new subs get it
let current = CurrentValueSubject<Int, Never>(0)
current.sink { print($0) } // prints 0 (current)
current.send(1) // prints 1
current.sink { print($0) } // prints 1 (current)
current.value // 1
// Use cases:
// - PassthroughSubject: events (button taps, notifications)
// - CurrentValueSubject: state (like @Published but more control)
// - Bridge imperative code to Combine
// Finish a subject
subject.send(completion: .finished)Error Handling
Combine publishers have a Failure type (Never for can't-fail). catch replaces an error with a fallback publisher. retry re-subscribes on failure (good for flaky networks). mapError transforms the error type. assertNoFailure crashes if an error occurs (use when you're certain). Publishers with Failure == Never can be used anywhere; those with errors need handling. This makes error handling explicit and composable in reactive pipelines.
enum APIError: Error { case network, parse }
// Publishers can fail with an error
let publisher = Fail<Int, APIError>(error: .network)
// catch: replace error with fallback publisher
publisher
.catch { _ in Just(0) } // fallback to 0
.sink { print($0) } // 0
// retry: retry on failure
URLSession.shared.dataTaskPublisher(for: url)
.retry(3) // retry up to 3 times
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
// mapError: transform error type
publisher
.mapError { _ in NSError(domain: "x", code: 1) }
// assertNoFailure: crash if error (when you're sure it won't)
Just(1).assertNoFailure().sink { print($0) }
// Result publisher
let resultPublisher = [1, 2, 3].publisher.setFailureType(to: Error.self)Access Control & Code Organization
Access Levels
Swift has five access levels. private (smallest scope, within declaration), fileprivate (within source file), internal (within module, default), public (anyone who imports), open (public + subclassable/overridable, classes only). Swift 5.9 adds package for SPM modules. Start restrictive and widen as needed. Use public for library APIs, internal for app code, private for implementation details. open is for framework classes designed for subclassing.
// public: accessible from any module that imports this
public class PublicAPI {
public func connect() {}
}
// internal (default): accessible within the module
class InternalClass {
func helper() {} // internal by default
}
// fileprivate: accessible within the source file
fileprivate func fileHelper() {}
// private: accessible within the enclosing declaration
class Counter {
private var count = 0 // only Counter can access
fileprivate func reset() { count = 0 } // same file
}
// Swift 5.9: package (for Swift Package Manager)
package struct PackageType {
package var value: Int
}
// Guidelines:
// - Start restrictive (private), open up as needed
// - public for framework API, internal for implementation
// - private for encapsulation within a typeExtensions and Organization
Extensions let you split a type's functionality across files (String+Validation.swift, String+Parsing.swift) and add conformance retroactively. This keeps files focused and manageable. Protocol conformance can be in a separate extension/file from the main type definition. Conditional conformance (where Element: ...) adds conformance conditionally. Use extensions to organize: keep the core type minimal, add functionality in focused extension files.
// Split a type across files with extensions
// String+Validation.swift
extension String {
var isValidEmail: Bool {
contains("@") && contains(".")
}
}
// String+Parsing.swift
extension String {
func toURL() -> URL? { URL(string: self) }
}
// Conform to protocols in extensions
extension String: Comparable { // already conforms, just showing
// protocol methods
}
// Conditional conformance
extension Array: MyProtocol where Element: MyProtocol {}
// Protocol conformance in a separate file
// (keeps the main type definition clean)
struct User { let id: Int; let name: String }
extension User: Codable {} // synthesized
extension User: Equatable {} // synthesized
extension User: CustomStringConvertible {
var description: String { "User(\(id), \(name))" }
}Modules and Packages
Swift Package Manager (SPM) defines modules via Package.swift. Each target is a module—internal access is within a target, public crosses modules. Dependencies are other packages (git URLs). Products are what your package exposes. SPM is the modern way to manage Swift code (replacing CocoaPods/Carthage). Xcode integrates SPM natively. Structure: Sources/MyLibrary/ for code, Tests/MyLibraryTests/ for tests. Use modules to separate concerns and control access.
// Package.swift (Swift Package Manager)
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyLibrary",
products: [
.library(name: "MyLibrary", targets: ["MyLibrary"]),
],
dependencies: [
.package(url: "https://github.com/.../Logging", from: "1.0.0"),
],
targets: [
.target(name: "MyLibrary", dependencies: ["Logging"]),
.testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
]
)
// Importing modules
import Foundation
import MyLibrary // your module
import Logging // dependency
// Each target is a module
// internal access is within a target/module
// public access crosses module boundariesInitializers and Designated/Convenience
Classes have designated initializers (fully init, must call super's designated) and convenience initializers (delegate to another init in the same class). This two-tier system ensures complete initialization. Structs get a memberwise initializer for free. Failable initializers (init?) return nil on failure. Required initializers (required init) must be implemented by subclasses. For most code, prefer structs (simpler initialization) over classes.
class Person {
let name: String
let age: Int
// Designated initializer: fully initializes
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class Employee: Person {
let employeeId: String
// Designated init must call super's designated init
init(name: String, age: Int, employeeId: String) {
self.employeeId = employeeId
super.init(name: name, age: age) // must call super
}
// Convenience init must call another init (same class)
convenience init(name: String) {
self.init(name: name, age: 0, employeeId: "TEMP")
}
}
// Structs: memberwise initializer (free)
struct Point { let x, y: Int }
let p = Point(x: 1, y: 2) // synthesized
// Failable initializers
struct Config {
let port: Int
init?(port: Int) {
guard port > 0 else { return nil }
self.port = port
}
}Optionals Deep Dive
Optionals are Swift's null-safety mechanism—an enum that's either .some(value) or .none (nil). You must unwrap to access the value. Optional chaining (?.) safely navigates; nil-coalescing (??) provides a default; if let/guard let safely unwrap. Avoid ! (force unwrap) unless you're certain—it crashes on nil. Optionals force you to handle absence explicitly, eliminating null-reference exceptions common in other languages.
// Optional: enum with .some(Value) and .none
let maybeInt: Int? = 42
let nilInt: Int? = nil
// Optional chaining (short-circuits on nil)
let name: String? = user?.profile?.name // nil if any link is nil
// Nil-coalescing (default value)
let value = maybeInt ?? 0 // 42
let value2 = nilInt ?? 0 // 0
// Forced unwrapping (crashes if nil)
let forced = maybeInt! // 42
// let crash = nilInt! // RUNTIME CRASH
// Optional binding
if let actual = maybeInt {
print(actual) // 42, actual is Int (not optional)
}
// Guard for early exit
func process(_ data: Data?) {
guard let data = data else { return }
// data is non-optional here
}
// Optional try
let result = try? parse(json) // nil on error
let result2 = try! parse(json) // crashes on error
// Multiple binding
if let a = optA, let b = optB, a < b { /* ... */ }Memory Management (ARC/weak)
ARC Basics
Automatic Reference Counting (ARC) tracks strong references. When count reaches zero, the object is deallocated. ARC is deterministic (unlike garbage collection): deinit runs immediately when the last reference is released. Most of the time, ARC just works. Issues arise with reference cycles: two objects strongly reference each other, preventing deallocation. Use weak or unowned to break cycles.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
reference1 = Person(name: "Alice")
// Prints: Alice is being initialized
reference2 = reference1 // Strong reference count: 2
reference1 = nil // Count: 1
reference2 = nil // Count: 0, deinit runs
// Prints: Alice is being deinitializedWeak References
weak references do not keep the referenced object alive. When the object is deallocated, weak references automatically become nil. weak must be optional (var tenant: Person?). Use weak when the referenced object has a shorter lifetime (tenant may leave the apartment). The classic use case is the delegate pattern: the delegate is weak to avoid cycles. weak has slight overhead (registered with the runtime for nil-setting).
class Person {
var name: String
var apartment: Apartment? // Strong (person owns apartment)
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class Apartment {
var unit: String
weak var tenant: Person? // Weak (breaks cycle)
init(unit: String) { self.unit = unit }
deinit { print("Apartment \(unit) deinit") }
}
var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice!.apartment = apt
apt!.tenant = alice // Weak reference, no cycle
alice = nil // Alice deinit (apartment's tenant becomes nil)
apt = nil // Apartment deinit
// weak must be optional (becomes nil when deallocated)Unowned References
unowned references, like weak, do not keep the object alive. Unlike weak, unowned is non-optional and does not become nil. Accessing an unowned reference after deallocation crashes. Use unowned when you can guarantee the referenced object outlives the reference (e.g., a credit card cannot exist without its customer). unowned has less overhead than weak. Choose: weak for uncertain lifetimes, unowned for guaranteed lifetimes.
class Customer {
let name: String
var card: CreditCard? // Strong
init(name: String) { self.name = name }
deinit { print("\(name) deinit") }
}
class CreditCard {
let number: UInt
unowned let customer: Customer // Non-optional, no cycle
init(number: UInt, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card \(number) deinit") }
}
var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234, customer: john!)
john = nil
// Customer deinit, then CreditCard deinit
// unowned: non-optional, assumed to always have a value
// CRASH if accessed after deallocation
// Use when the referenced object has same or longer lifetimeClosures & Capture Lists
Closures capture references strongly by default, causing cycles when the closure is stored on self. Capture lists ([weak self] or [unowned self]) break the cycle. weak self requires optional handling (guard let self = self). Capture specific values (let name = self.name) to avoid capturing self entirely. Closures passed to functions (not stored) do not cause cycles. Always use capture lists for stored closures that reference self.
class ViewController {
var name = "MyVC"
var onComplete: (() -> Void)?
// BAD: Strong reference cycle
func setupBad() {
onComplete = {
print("\(self.name) completed") // Captures self strongly
}
}
// GOOD: Capture list breaks cycle
func setupGood() {
onComplete = { [weak self] in
guard let self = self else { return }
print("\(self.name) completed")
}
}
// unowned for guaranteed lifetime
func setupUnowned() {
onComplete = { [unowned self] in
print("\(self.name) completed")
}
}
// Capture specific values
func setupCapture() {
let name = self.name // Capture value, not self
onComplete = {
print("\(name) completed")
}
}
deinit { print("VC deinit") }
}Detecting & Fixing Leaks
Common leak sources: delegates stored strong (use weak), Timer retaining target (invalidate in deinit), NotificationCenter observers (remove in deinit). The Xcode Memory Graph Debugger visualizes object graphs and highlights leaks. Run with MallocStackLogging to get allocation traces. Test deinit by setting references to nil and verifying deinit runs. Instruments (Leaks tool) detects leaks at runtime. Always pair setup with teardown.
// Xcode memory graph debugger: visualize reference cycles
// Product > Debug > View Memory Graph Hierarchy
// Common leak patterns:
// 1. Delegate stored as strong
class View {
weak var delegate: ViewDelegate? // MUST be weak
}
// 2. Timer retains target
class TimerHolder {
var timer: Timer?
func start() {
// BAD: timer retains self via #selector
timer = Timer.scheduledTimer(timeInterval: 1,
target: self, selector: #selector(fire),
userInfo: nil, repeats: true)
}
@objc func fire() {}
deinit { timer?.invalidate() } // MUST invalidate
}
// 3. NotificationCenter observer
class Observer {
init() {
NotificationCenter.default.addObserver(
self, selector: #selector(handle),
name: .someNotification, object: nil)
}
@objc func handle() {}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// Use Xcode's Debug Memory Graph to find leaksActors & async/await
async/await Basics
async/await (Swift 5.5+) makes asynchronous code look synchronous. async marks functions that can suspend; await marks suspension points. Tasks bridge sync to async contexts. async let starts a concurrent task; await collects the result. Use async let for parallel execution, regular await for sequential. Errors propagate with throws/try. The compiler enforces await at suspension points. async/await replaces completion handlers and Combine for many use cases.
// Async function
func fetchUser(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Calling async functions
func loadProfile() async {
do {
let user = try await fetchUser(id: 1)
print("Loaded: \(user.name)")
} catch {
print("Error: \(error)")
}
}
// Task: bridge async to sync context
Task {
await loadProfile()
}
// Concurrent execution
func loadMultiple() async {
async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
async let user3 = fetchUser(id: 3)
let users = try await [user1, user2, user3]
print("Loaded \(users.count) users")
}
// Sequenced vs concurrent
let sequential = try await fetchUser(id: 1) // Waits
let concurrent = try await fetchUser(id: 2) // Then waits againActors
Actors (Swift 5.5+) are reference types with serialized access, preventing data races. Only one method executes on an actor at a time—no manual locking needed. Actor methods must be called with await (they may suspend). Properties are isolated and cannot be accessed directly from outside. Actors are the safe default for shared mutable state in concurrent code. Use actors instead of classes with locks for most concurrent state.
// Actor: reference type with serialized access
actor BankAccount {
private var balance: Decimal
init(balance: Decimal) {
self.balance = balance
}
func deposit(_ amount: Decimal) {
balance += amount
}
func withdraw(_ amount: Decimal) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
func getBalance() -> Decimal { balance }
}
// Usage (must be async)
let account = BankAccount(balance: 100)
Task {
await account.deposit(50)
let balance = await account.getBalance()
print("Balance: \(balance)") // 150
}
// Actors prevent data races automatically
// Only one method runs at a time per actor instance
// No need for locks or queuesActor Isolation
Actor isolation protects state. nonisolated members can be accessed without await (for pure values or methods that do not touch isolated state). @MainActor is a global actor for main-thread isolation (UI updates). @globalActor creates custom global actors. Crossing actor boundaries requires await. The compiler verifies isolation at compile time, preventing data races. Use @MainActor for ViewModels and UI code; custom actors for domain-specific isolation.
actor Counter {
private var count = 0
// Isolated method (default)
func increment() { count += 1 }
// Nonisolated: can be called without await
nonisolated var description: String { "Counter" }
// nonisolated function
nonisolated func id() -> String { "counter-\(ObjectIdentifier(self))" }
}
// Global actor: @MainActor runs on main thread
@MainActor
class ViewModel: ObservableObject {
@Published var data: [Item] = []
func loadData() async {
// Always runs on main thread
data = try await fetchItems()
}
}
// Mark specific functions
class Service {
@MainActor
func updateUI(_ result: Result) {
// UI updates on main thread
}
}
// globalActor lets you apply to types, functions, properties
@globalActor
actor MyActor {
static let shared = MyActor()
}Structured Concurrency
Task groups enable dynamic parallel computation. addTask spawns child tasks; for await group collects results as they complete. Task groups enforce structured concurrency: all children complete before the parent continues. Cancellation propagates from parent to children automatically. Check Task.isCancelled in long-running tasks. withThrowingTaskGroup propagates errors, cancelling siblings on throw. Use task groups for fan-out/fan-in patterns.
// TaskGroup: dynamic parallel computation
func fetchAllUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await fetchUser(id: id) }
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
// Task tree: parent-child relationship
func process() async {
await withTaskGroup(of: Void.self) { group in
for i in 1...5 {
group.addTask {
await processItem(i)
}
}
// All tasks complete before group returns
}
print("All done")
}
// Cancellation
func longRunning() async {
await withTaskGroup(of: Void.self) { group in
group.addTask {
for i in 1...100 {
if Task.isCancelled { return }
await doWork(i)
}
}
}
}
// Cancel from outside
let task = Task { await longRunning() }
task.cancel()AsyncSequence & Streams
AsyncSequence is the async version of Sequence, enabling for await loops. AsyncStream bridges callback-based or delegate-based APIs to async/await. yield emits values, finish terminates. onTermination cleans up resources. AsyncThrowingStream supports errors. Use AsyncSequence for paginated APIs, real-time data, or any stream of values over time. The standard library provides .lines on URL for line-by-line file reading. AsyncSequence integrates with task cancellation.
// AsyncSequence: iterate asynchronously
struct Counter: AsyncSequence {
struct AsyncIterator: AsyncIteratorProtocol {
var current: Int
let max: Int
mutating func next() async -> Int? {
guard current <= max else { return nil }
defer { current += 1 }
try? await Task.sleep(nanoseconds: 100_000_000)
return current
}
}
let start: Int
let max: Int
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(current: start, max: max)
}
}
// Usage
for await num in Counter(start: 1, max: 5) {
print(num) // 1 2 3 4 5 (with delays)
}
// AsyncStream: bridge callbacks to async
func urlLines(_ url: URL) -> AsyncThrowingStream<String, Error> {
AsyncThrowingStream { continuation in
let task = Task {
do {
for try await line in url.lines {
continuation.yield(line)
}
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}Testing
XCTest Basics
XCTest is the standard testing framework. XCTestCase groups related tests. setUp/tearDown run before/after each test. Assertions: XCTAssertEqual, XCTAssertNil, XCTAssertThrowsError, XCTAssertTrue. @testable import accesses internal symbols. Tests run on the main thread by default. Use XCTAssertNotNil for optionals. Run tests with Cmd+U. Xcode shows test results inline. Write tests alongside code for fast feedback.
import XCTest
@testable import MyApp
class UserTests: XCTestCase {
var user: User!
override func setUp() {
super.setUp()
user = User(name: "Alice", age: 30)
}
override func tearDown() {
user = nil
super.tearDown()
}
func testName() {
XCTAssertEqual(user.name, "Alice")
}
func testAgeAfterBirthday() {
user.haveBirthday()
XCTAssertEqual(user.age, 31)
}
func testValidation() {
XCTAssertThrowsError(try User(name: "", age: -1)) { error in
guard let error = error as? UserError else {
XCTFail("Wrong error type")
return
}
XCTAssertEqual(error, .invalidName)
}
}
func testOptional() {
XCTAssertNotNil(user.email)
XCTAssertNil(user.address)
}
}
// Run: Cmd+U in XcodeAsync Testing
Swift 5.5+ supports async test methods directly (async throws func test...). Use try await inside tests. For older code, use XCTestExpectation: create expectation, fulfill() in callback, wait(for:timeout:). Set reasonable timeouts to catch hangs. For testing async sequences, use for await loops. Mock network calls with URLProtocol or dependency injection. Test both success and failure paths. Async tests run concurrently by default—use .serialized trait if order matters.
class APITests: XCTestCase {
func testFetchUser() async throws {
let user = try await fetchUser(id: 1)
XCTAssertEqual(user.name, "Alice")
}
func testFetchFailure() async {
do {
_ = try await fetchUser(id: -1)
XCTFail("Should have thrown")
} catch {
// Expected
}
}
// Old style with expectations (still works)
func testAsyncWithExpectation() {
let expectation = XCTestExpectation(description: "Fetch completes")
fetchUserAsync { result in
switch result {
case .success(let user):
XCTAssertEqual(user.name, "Alice")
case .failure:
XCTFail("Should succeed")
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
}
}Mocking & Stubbing
Dependency injection enables testing: define protocols for dependencies, inject mocks in tests. MockNetworkService returns predetermined results. Test success and failure cases by setting mock.fetchUserResult. This pattern decouples tests from the network, making them fast and deterministic. For complex mocks, use libraries like Cuckoo or Mockingbird. Test the public API of your types, not implementation details. Mocks should be simple and focused.
// Protocol for dependency injection
protocol NetworkService {
func fetchUser(id: Int) async throws -> User
}
// Production implementation
class APIService: NetworkService {
func fetchUser(id: Int) async throws -> User {
// Real network call
}
}
// Mock for testing
class MockNetworkService: NetworkService {
var fetchUserResult: Result<User, Error>?
func fetchUser(id: Int) async throws -> User {
switch fetchUserResult {
case .success(let user): return user
case .failure(let error): throw error
case .none: throw URLError(.badURL)
}
}
}
class ViewModelTests: XCTestCase {
func testLoadUser() async throws {
let mock = MockNetworkService()
mock.fetchUserResult = .success(User(name: "Alice", age: 30))
let vm = ViewModel(service: mock)
await vm.load()
XCTAssertEqual(vm.userName, "Alice")
}
}UI Testing
UI tests automate user interactions via accessibility identifiers. XCUIApplication launches the app. Find elements by accessibility identifier (set in code with .accessibilityIdentifier). Actions: tap, typeText, swipeLeft/Right. Assertions: waitForExistence, exists, hittable. UI tests are slower than unit tests but catch integration bugs. Set accessibility identifiers in SwiftUI with .accessibilityIdentifier("Email"). Run UI tests on multiple devices/simulators for coverage.
import XCTest
class AppUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
let app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
let app = XCUIApplication()
let emailField = app.textFields["Email"]
let passwordField = app.secureTextFields["Password"]
let loginButton = app.buttons["Log In"]
emailField.tap()
emailField.typeText("[email protected]")
passwordField.tap()
passwordField.typeText("password123")
loginButton.tap()
// Verify navigation
XCTAssertTrue(app.staticTexts["Welcome, Alice!"].waitForExistence(timeout: 5))
}
func testSwipeAndTap() {
let cell = app.cells["Item 5"]
cell.swipeLeft()
app.buttons["Delete"].tap()
XCTAssertFalse(cell.exists)
}
}Performance & Code Coverage
measure benchmarks code performance, comparing against baselines. Set baselines in Xcode; tests fail if regression exceeds threshold. measureMetrics tracks specific metrics (peak memory, CPU). Enable code coverage in the scheme to see untested code paths. Aim for 80%+ coverage on business logic. Snapshot testing (swift-snapshot-testing) captures UI for visual regression. Profile with Instruments (Time Profiler, Allocations) for deeper analysis. Performance tests catch regressions early.
class PerformanceTests: XCTestCase {
func testSortingPerformance() {
let data = (1...10000).shuffled()
measure {
_ = data.sorted()
}
}
func testMemoryUsage() {
measureMetrics([XCTPerformanceMetric.peakMemory]) {
_ = (0..<10000).map { _ in [Int](repeating: 0, count: 100) }
}
}
// Baseline comparison
func testWithBaselines() {
let options = XCTMeasureOptions()
options.defaultOptions.measurementUnits = .seconds
measure(options) {
// Code to benchmark
}
}
}
// Code coverage: enable in Edit Scheme > Test > Options
// View coverage: Cmd+Shift+Y (Report Navigator) > Coverage
// Snapshot testing (third-party)
// func testAppearance() {
// assertSnapshot(of: view, as: .image)
// }Swift Concurrency
async/await
async/await (Swift 5.5+) simplifies async code. async marks functions that can suspend. await marks suspension points. Task creates an async context. Much cleaner than completion handlers. Errors propagate with try.
func fetchData() async throws -> String {
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? ""
}
// Call from async context
Task {
let result = try await fetchData()
print(result)
}Actors
actor (Swift 5.5+) is a reference type with automatic mutual exclusion. Only one task accesses its state at a time. Methods are implicitly async when called from outside. Replaces locks and dispatch queues for shared mutable state. Thread-safe by design.
actor Counter {
private var count = 0
func increment() {
count += 1
}
func getValue() -> Int {
count
}
}
// Usage
Task {
let counter = Counter()
await counter.increment()
let value = await counter.getValue()
}Async Sequence
AsyncSequence is the async equivalent of Sequence. for await iterates asynchronously. Useful for streaming data (network, file lines). Implement AsyncIteratorProtocol to create custom async sequences. The compiler handles suspension and cancellation.
for try await item in asyncSequence {
print(item)
}
// Custom AsyncSequence
struct Counter: AsyncSequence {
struct AsyncIterator: AsyncIteratorProtocol {
var current = 0
mutating func next() async -> Int? {
current += 1
return current <= 5 ? current : nil
}
}
func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}Task Groups
Task groups run multiple tasks concurrently and collect results. addTask adds a child task. for await iterates results as they complete. All tasks must complete before the group returns. Structured concurrency: cancellation propagates to children. Results are collected safely.
let results = await withTaskGroup(of: Int.self) { group in
for i in 1...5 {
group.addTask { i * i }
}
var sum = 0
for await result in group {
sum += result
}
return sum
}Continuation
withCheckedContinuation bridges completion-handler APIs to async/await. resume(returning:) resumes the async function. Must be called exactly once. withCheckedThrowingContinuation supports errors. Useful for adopting async/await with existing APIs.
func fetchWithCompletion(_ completion: @escaping (String) -> Void) {
// Legacy completion handler API
completion("data")
}
// Wrap in async
func fetchAsync() async -> String {
await withCheckedContinuation { continuation in
fetchWithCompletion { result in
continuation.resume(returning: result)
}
}
}SwiftUI Deep
View Modifiers
Modifiers wrap views to change appearance or behavior. Order matters: later modifiers wrap earlier ones. .padding before .background puts padding inside the background. Modifiers return new View instances. Chain them for complex styling.
Text("Hello")
.font(.title)
.foregroundColor(.blue)
.padding()
.background(Color.gray)
.cornerRadius(8)
.shadow(radius: 4)Lists
List displays scrollable rows. ForEach generates rows from data. onDelete enables swipe-to-delete. Other modifiers: onMove, onInsert. Identifiable data (id property) is required for ForEach. List style: .plain, .insetGrouped, .sidebar.
List {
ForEach(items) { item in
HStack {
Text(item.name)
Spacer()
Text("\(item.price)")
}
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.listStyle(.insetGrouped)Navigation
NavigationStack manages a stack of views. NavigationLink pushes a destination. navigationTitle sets the title. toolbar adds navigation bar buttons. For iOS 16+. Use NavigationView for older versions. Sheets and alerts use .sheet and .alert modifiers.
NavigationStack {
List(items) { item in
NavigationLink(item.name) {
DetailView(item: item)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add") { addItem() }
}
}
}State Management
@State for local view state (value types). @Binding for passed-in state. @StateObject for owned observable objects (created once). @ObservedObject for external observable objects. @EnvironmentObject for app-wide state. @State changes trigger view updates.
struct CounterView: View {
@State private var count = 0
@Binding var selected: Int
@StateObject var viewModel = ViewModel()
var body: some View {
Button("\(count)") { count += 1 }
}
}Animations
withAnimation animates state changes. .spring, .easeInOut are animation curves. .animation(value:) animates when the value changes. .scaleEffect, .opacity, .offset are animatable. Implicit animations use .animation modifier. Explicit use withAnimation.
struct AnimatedView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Button("Tap") {
withAnimation(.spring(response: 0.3)) {
scale = scale == 1 ? 1.5 : 1
}
}
.scaleEffect(scale)
.animation(.easeInOut(duration: 0.3), value: scale)
}
}Combine Framework
Publishers & Subscribers
Combine is Apples reactive framework. Publishers emit values over time. Subscribers receive them. sink creates a subscriber. Just emits a single value. PassthroughSubject is a manual publisher. Cancellables must be retained or the subscription cancels. Similar to RxSwift.
import Combine
let publisher = Just(42)
let cancellable = publisher.sink { value in
print("Received: \(value)")
}
// Just emits one value then finishes
// PassthroughSubject: manual values
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)Operators
Operators transform publisher output. map transforms values. filter selects values. debounce delays until quiet. throttle limits rate. combineLatest merges streams. flatMap chains publishers. Operators are lazy: nothing happens until sink subscribes.
let publisher = (1...10).publisher
.map { $0 * 2 }
.filter { $0 > 10 }
.sink { print($0) } // 12, 14, 16, 18, 20
// Debounce
let debounced = subject
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { print($0) }@Published
@Published exposes a property as a Combine publisher. $prefix accesses the publisher. Changes emit new values. Works with ObservableObject for SwiftUI. The publisher emits the current value on subscription. Useful for reactive UI updates without manual notifications.
class ViewModel: ObservableObject {
@Published var count = 0
}
let vm = ViewModel()
let cancellable = vm.$count.sink { newCount in
print("Count changed: \(newCount)")
}
vm.count = 1 // Prints "Count changed: 1"
vm.count = 2 // Prints "Count changed: 2"Future
Future wraps a completion-handler API as a publisher. promise(.success) emits a value. promise(.failure) emits an error. Future emits exactly once. Useful for bridging callback APIs to Combine. The closure runs eagerly unless wrapped with .delay or deferred.
func fetchUser() -> Future<User, Error> {
Future { promise in
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { promise(.failure(error)) }
else { promise(.success(parse(data!))) }
}.resume()
}
}
// Usage
fetchUser().sink(receiveCompletion: { _ in }, receiveValue: { user in print(user) })Error Handling
Fail immediately emits an error. catch replaces a failed publisher with another. retry resubscribes on failure. assertNoFailure crashes on error (for debugging). The completion event signals no more values. Errors propagate downstream unless caught.
let publisher = Fail<Int, MyError>(error: .notFound)
.catch { error in
Just(0) // Fallback value
}
.sink { print($0) } // 0
// retry on failure
let retried = requestPublisher
.retry(3)
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })Memory Management
ARC Basics
ARC (Automatic Reference Counting) tracks references. When count hits zero, deinit runs and memory frees. Strong references increment the count. Reference cycles prevent deallocation. ARC is deterministic (unlike GC). deinit runs synchronously when the last reference drops.
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) is being deinitialized") }
}
var p1: Person? = Person(name: "Alice")
p1 = nil // Prints "Alice is being deinitialized"Weak References
weak references do not increment retain count. They auto-set to nil when the object deallocates. Must be optional var. Use for delegates and observers to break cycles. weak references are zeroing: safe to access after the object is gone. The most common fix for retain cycles.
class View {
weak var delegate: Delegate?
}
class Delegate {
var view: View? // strong
}
// weak: does not keep object alive, auto-set to nil
// Use for delegate patterns to avoid cyclesUnowned References
unowned references do not increment retain count. Unlike weak, they are non-optional and not zeroing. Accessing after dealloc crashes. Use when the referenced object outlives or dies with the referencer. Faster than weak (no nil check). Common in closures capturing self when self outlives the closure.
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer
init(customer: Customer) { self.customer = customer }
}
// unowned: does not keep alive, but not optional
// CRASHES if accessed after dealloc
// Use when the other object has same or shorter lifetimeRetain Cycles
Retain cycles occur when objects reference each other strongly. Neither deallocates, leaking memory. Fix with weak or unowned. Common in delegates, observers, and closures. Closures capture self strongly by default. [weak self] or [unowned self] breaks the cycle. Use Memory Graph Debugger to find cycles.
// BAD: retain cycle
class Node {
var next: Node?
var prev: Node? // strong -> cycle
}
// GOOD: break with weak
class Node {
var next: Node?
weak var prev: Node?
}
// Closures: use [weak self]
{ [weak self] in self?.update() }Closures & Capture
Closures capture variables by reference. Stored closures (escaping) create retain cycles if they capture self. [weak self] makes the capture optional and weak. @escaping marks closures that outlive the function call. Non-escaping closures (default) cannot cause cycles. The compiler warns about potential cycles.
class Timer {
var handler: (() -> Void)?
func start() {
handler = { [weak self] in
self?.tick() // weak to avoid cycle
}
}
func tick() { /* ... */ }
}
// Escaping closure: stored for later
// @escaping marks escaping closuresError Handling Deep
Custom Errors
Custom errors conform to Error. LocalizedError provides errorDescription. Associated values carry context. Enums are the idiomatic way to define errors. Each case represents a distinct failure. Switch exhaustiveness ensures all errors are handled. Conform to CustomStringConvertible for debug output.
enum APIError: Error, LocalizedError {
case invalidURL
case unauthorized
case serverError(Int)
case decodingFailed(Error)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .unauthorized: return "Unauthorized"
case .serverError(let code): return "Server error: \(code)"
case .decodingFailed(let err): return "Decode failed: \(err)"
}
}
}Result Type
Result is a typed success/failure enum. Useful for synchronous operations that can fail. map transforms the success value. flatMap chains operations. get() throws to convert to try/catch. Result is preferred over throwing functions for stored or passed-around results. Combines error handling with value semantics.
func fetchUser(id: Int) -> Result<User, APIError> {
guard id > 0 else { return .failure(.invalidURL) }
return .success(User(name: "Alice"))
}
// Usage
switch fetchUser(id: 1) {
case .success(let user): print(user)
case .failure(let error): print(error)
}
// map and flatMap
let result = fetchUser(id: 1).map { $0.name }Rethrows
rethrows propagates errors from a closure parameter. If the closure does not throw, the function does not throw either. This avoids forcing try on callers that pass non-throwing closures. Used by map, filter, and other higher-order functions. The function itself cannot throw independently of the closure.
func withLock<T>(_ lock: NSLock, _ body: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try body()
}
// Only throws if body throws
let x = withLock(lock) { 5 } // no try needed
let y = try withLock(lock) { try throwingFunc() }Error Propagation
try propagates errors to the caller. try? converts errors to nil (returns Optional). try! crashes on error (use when certain). Errors bubble up the call stack until caught. Functions that throw must be marked throws. The compiler enforces try markers, preventing unhandled errors. defer runs regardless of thrown errors.
func process() throws {
let data = try fetchData() // may throw
let user = try parse(data) // may throw
save(user) // does not throw
}
// try?: converts to optional, nil on error
let user = try? fetchUser()
// try!: asserts no error, crashes if thrown
let user = try! fetchUser()Cleanup with defer
defer schedules cleanup to run when the scope exits. Runs regardless of how the scope exits (return, throw, or fall-through). Multiple defers run in LIFO order. Useful for closing files, releasing locks, freeing resources. defer cannot throw or break/continue. The closure captures variables by reference.
func processFile() throws {
let file = open("file.txt")
defer { close(file) } // runs on scope exit
let data = try read(file)
// ... process ...
// close() runs here even if read throws
}
// Multiple defers run in reverse order (LIFO)
defer { print("1") }
defer { print("2") } // prints 2 then 1Related Swift snippets
Copy-paste ready code for common tasks.
Optionals
Use optional binding, nil-coalescing, chaining, and guard for safe unwrapping.
Closures
Define closure expressions, capture state, and pass escaping callbacks.
Protocols
Define contracts, conform with structs, and add default behavior via extensions.
Generics
Write type-parameterized functions and types with protocol constraints.
Structs and Classes
Compare value-type structs with reference-type classes and inheritance.
Error Handling
Throw and catch typed errors with do-catch, try?, try!, and rethrows.
Concurrency (async/await)
Run async functions, parallelize with async let, and fan out with task groups.
String Manipulation
Trim, split, join, replace, and index strings using Swift's Unicode API.
Was this helpful?