Swift Cheat Sheet

Optionals, closures, protocols, generics, actors and SwiftUI in one Swift cheat sheet. Every snippet is copy ready, filterable and cross referenced.

314 snippets 25 sections Swift 6 ready
/ to focus
// no matchNothing matches that filter. Try a shorter query, or switch the level back to All.
01

Basics

Constants, variables, type inference and string interpolation. Swift is statically typed but you rarely write the type out.

Hello Swiftcore
Print to the console
print("Hello, world!")
Entry point for an executable target
@main struct App { static func main() async throws { print("running") } }
Comments and documentation comments
// line comment /* block comment, which can nest /* like this */ */ /// Documentation comment, shown in Quick Help /// - Parameter name: who to greet /// - Returns: the greeting
let and varcore
Constant with let, variable with var
let maximum = 100 // cannot change var counter = 0 // can change counter += 1
Explicit type annotation
let name: String = "Ada" var scores: [Int] = [] let ratio: Double = 0.75
Declare now, assign later
let result: Int if condition { result = 1 } else { result = 2 }
Several bindings on one line
let x = 0, y = 0, z = 0 var red, green, blue: Double
Type properties that never change
enum Config { static let apiVersion = "v2" static let timeout: TimeInterval = 30 } Config.apiVersion
String Interpolation & Printingstd
Embed any value in a string
let name = "Ada" let age = 36 print("\(name) is \(age)") print("next year: \(age + 1)")
Control the separator and terminator
print("a", "b", "c", separator: " | ") print("no newline", terminator: "")
Inspect a value while debugging
dump(user) // full structure debugPrint(user) // uses debugDescription
Format a number for display
let price = 1234.5 price.formatted(.currency(code: "EUR")) // "€1,234.50" (0.87).formatted(.percent) // "87%" String(format: "%.2f", price) // "1234.50"
02

Types & Operators

Swift never converts numeric types for you. That verbosity is deliberate: it removes a whole family of silent precision bugs.

Core Typescore
TypeHoldsNotes
Intwhole numbers64 bit on modern devices, the default
UInt8 ... UInt64unsigneduse only for bit work and byte buffers
Double64 bit floatthe default for decimals
Float32 bit floatgraphics and memory constrained data
Booltrue or falseno integer coercion at all
StringUnicode textvalue type, grapheme cluster based
Characterone graphemecan be several Unicode scalars
Array<T>ordered listwritten [T]
Dictionary<K,V>key to valuewritten [K: V]
Set<T>unique, unorderedT must be Hashable
Optional<T>value or nilwritten T?
Voidno valuethe empty tuple ()
Any / AnyObjectany / any classescape hatch, avoid in new code
Numbers & Conversioncore
Literal forms
let dec = 1_000_000 let hex = 0xFF let oct = 0o77 let bin = 0b1010_1010 let sci = 1.25e3 // 1250.0
Conversion is always explicit
let count = 3 let ratio = 0.75 let total = Double(count) * ratio // no implicit widening let rounded = Int(total) // truncates toward zero
Conversion that can fail
let n = Int("42") // Optional(42) let bad = Int("4x2") // nil let safe = Int(exactly: 3.0) // Optional(3) let lossy = Int(exactly: 3.5) // nil
Rounding and math
(2.7).rounded() // 3.0 (2.7).rounded(.down) // 2.0 abs(-4) // 4 max(1, 9), min(1, 9) (2.0).squareRoot() Int.random(in: 1...6)
Overflow behaviour
Int.max &+ 1 // wraps, no trap let (v, overflowed) = Int.max.addingReportingOverflow(1) Int8(clamping: 500) // 127
Never compare Doubles with equals
0.1 + 0.2 == 0.3 // false // compare against a tolerance scaled to the magnitude func isClose(_ a: Double, _ b: Double, tolerance: Double = 1e-9) -> Bool { abs(a - b) <= tolerance * max(abs(a), abs(b), 1) } // money belongs in Decimal, never in Double let amount = Decimal(string: "19.99")!
Operatorscore
OperatorMeaning
+ - * / %arithmetic, % is remainder and works on Int only
== != < > <= >=comparison, needs Equatable or Comparable
=== !==identity, do two references point at the same object
&& || !logical, short circuits
??nil coalescing, supply a default
?.optional chaining
!force unwrap, crashes on nil
... ..<closed range and half open range
&+ &- &*wrapping arithmetic, no overflow trap
~= pattern match, what switch case uses
& | ^ << >> ~bitwise
is as? as! astype check and cast
Tuples & Rangescore
Tuple, a lightweight grouping
let point = (x: 3, y: 7) print(point.x, point.1) let (status, message) = (404, "Not Found")
Ranges
1...5 // 1,2,3,4,5 1..<5 // 1,2,3,4 let letters = "a"..."f" array[2...] // from index 2 to the end array[..<3] // up to but not including 3
Type checks and casts
if value is String { } let text = value as? String // Optional let forced = value as! String // traps if wrong let any: Any = 42 // upcast, always safe
typealias for readable signatures
typealias JSON = [String: Any] typealias Completion = (Result<Data, Error>) -> Void
03

Optionals

Swift has no implicit null. A value that may be absent has type T?, and the compiler will not let you use it until you deal with the nil case.

The unwrapping decision, in order

Need it for the rest of the function? Use guard let and exit early.
Need it only inside a short branch? Use if let.
Have a sensible fallback? Use ?? and move on.
Only reading one property? Use optional chaining with ?.
Certain it cannot be nil? Prefer restructuring, and reach for ! only when a nil genuinely means a programming error.
Declaring & Unwrappingcore
An optional is a real type
var middleName: String? // starts as nil var age: Int? = 36 age = nil // allowed, it is Optional<Int>
if let, the everyday unwrap
if let name = middleName { print("middle name is \(name)") } else { print("no middle name") }
Shorthand when the names match
if let middleName { print(middleName) // Swift 5.7 and later }
guard let, unwrap and keep going
func greet(_ name: String?) { guard let name else { print("nobody to greet") return } // name is a non optional String from here on print("Hello, \(name)") }
Unwrap several at once
guard let url = URL(string: raw), let host = url.host, !host.isEmpty else { return }
Nil coalescing supplies a default
let display = middleName ?? "none" let port = config.port ?? 8080 let first = list.first ?? "empty"
Optional chaining stops at the first nil
let count = user?.profile?.followers?.count // count is Int?, nil if any link is nil user?.profile?.refresh() // simply does nothing if nil
Working With Optionalsstd
Transform without unwrapping
let length = middleName?.count // Int? let upper = middleName.map { $0.uppercased() } // String? let parsed = raw.flatMap { Int($0) } // Int?, no double optional
Drop the nils from a collection
let raw = ["1", "two", "3"] let numbers = raw.compactMap { Int($0) } // [1, 3]
Switch over an optional
switch age { case .some(let value) where value >= 18: print("adult") case .some(let value): print("minor, \(value)") case .none: print("unknown") }
Assign only when the optional has a value
user.nickname = incoming ?? user.nickname if let incoming { user.nickname = incoming }
Force unwrap, and how to make it safe to read
// avoid let name = middleName! // if it truly cannot fail, say why let url = URL(string: "https://example.com")! // literal, verified // better still, fail loudly with a reason guard let url else { preconditionFailure("base URL must be valid") }
Implicitly unwrapped optionals
@IBOutlet var label: UILabel! var delegate: Handler! // set immediately after init
04

Strings

Swift strings are Unicode correct by design. That is why you index them with String.Index instead of Int, and why count is not a constant time lookup.

Creating & Combiningstd
Literals, empty and repeated
let greeting = "Hello" var empty = "" let line = String(repeating: "-", count: 20) let fromChar = String(Character("A"))
Multiline string
let query = """ SELECT * FROM users WHERE active = 1 """
Raw string, no escapes processed
let path = #"C:\Users\new\table"# let regex = #"\d{3}-\d{4}"# let mixed = #"total is \#(amount)"# // interpolate inside raw
Append and join
var s = "Hello" s += ", world" s.append("!") let csv = ["a", "b", "c"].joined(separator: ",")
Escape sequences
"\n \t \\ \" \' \0" "\u{1F600}" // emoji by scalar
Inspecting & Searchingstd
Length and emptiness
text.count // grapheme clusters, not bytes text.isEmpty "cafe\u{301}".count // 4, the accent combines
Contains, prefix, suffix
text.contains("swift") text.hasPrefix("https://") text.hasSuffix(".json") text.lowercased(), text.uppercased() text.capitalized
Trim whitespace
import Foundation let clean = raw.trimmingCharacters(in: .whitespacesAndNewlines)
Split and replace
let parts = "a,b,c".split(separator: ",") // [Substring] let words = sentence.split(whereSeparator: \.isWhitespace) let fixed = text.replacingOccurrences(of: "-", with: "_")
Iterate characters and scalars
for ch in "abc" { print(ch) } for (i, ch) in text.enumerated() { } Array(text) // [Character] text.unicodeScalars.map(\.value) Array(text.utf8) // [UInt8]
Indices & Slicingcareful
Strings are not indexed by Int
let first = text[text.startIndex] let idx = text.index(text.startIndex, offsetBy: 3) let slice = text[idx...] // Substring let sub = String(text.prefix(5)) let tail = String(text.suffix(3))
Find a range and use it
if let r = text.range(of: "swift") { let before = text[..<r.lowerBound] let match = text[r] }
Take and drop
text.prefix(10) text.dropFirst() text.dropLast(3) text.prefix(while: { $0.isLetter })
Regex literals, Swift 5.7 and later
let re = /(\d{4})-(\d{2})-(\d{2})/ if let m = "due 2026-08-20".firstMatch(of: re) { print(m.1, m.2, m.3) } let all = text.matches(of: /\w+@\w+\.\w+/) let cleaned = text.replacing(/\s+/, with: " ")
05

Collections

Array, Dictionary and Set are all value types with copy on write, so passing one around is cheap until somebody mutates it.

Arrayscore
Create
var names = ["Ada", "Grace"] var empty: [Int] = [] var zeros = [Int](repeating: 0, count: 5) var reserved = [Int](); reserved.reserveCapacity(100)
Add and remove
names.append("Alan") names += ["Barbara"] names.insert("Edsger", at: 0) names.remove(at: 1) names.removeFirst(); names.removeLast() names.removeAll()
Read safely
names[0] // traps if out of range names.first, names.last // Optional names.isEmpty, names.count names.indices names.contains("Ada")
A safe subscript worth adding
extension Collection { subscript(safe index: Index) -> Element? { indices.contains(index) ? self[index] : nil } } names[safe: 99] // nil instead of a crash
Sort and reverse
names.sort() let sorted = names.sorted() let byLength = names.sorted { $0.count < $1.count } users.sort(using: KeyPathComparator(\.age)) names.reverse() names.shuffle()
Transformingcore
map, filter, reduce
let lengths = names.map(\.count) let long = names.filter { $0.count > 4 } let total = numbers.reduce(0, +) let joined = names.reduce("") { $0 + $1 }
compactMap and flatMap
let ints = ["1", "x", "3"].compactMap { Int($0) } // [1, 3] let flat = [[1, 2], [3]].flatMap { $0 } // [1, 2, 3]
Search and test
names.first { $0.hasPrefix("A") } names.firstIndex(of: "Ada") names.allSatisfy { !$0.isEmpty } names.contains { $0.count > 6 } numbers.min(), numbers.max()
Slicing and chunking
names.prefix(2) names.dropFirst(2) zip(names, ages) // pairs, stops at the shorter names.enumerated() // (offset, element) Array(names.reversed())
Group into a dictionary
let byInitial = Dictionary(grouping: names) { $0.first! } // ["A": ["Ada", "Alan"], "G": ["Grace"]]
Lazy chains avoid intermediate arrays
let firstBig = numbers.lazy .map { $0 * $0 } .first { $0 > 1_000 }
Dictionariesstd
Create and read
var ages = ["Ada": 36, "Grace": 45] var empty: [String: Int] = [:] ages["Ada"] // Optional(36) ages["Nobody", default: 0] // 0, no optional
Insert, update, remove
ages["Alan"] = 41 ages["Alan"] = nil // removes the key ages.removeValue(forKey: "Ada") // returns the old value ages.updateValue(37, forKey: "Ada")
Counting idiom
var counts: [Character: Int] = [:] for ch in text { counts[ch, default: 0] += 1 }
Iterate, keys, values
for (name, age) in ages { } ages.keys.sorted() ages.values.reduce(0, +) ages.mapValues { $0 + 1 } ages.filter { $0.value > 40 }
Build from pairs, handling duplicates
Dictionary(uniqueKeysWithValues: zip(names, ages)) Dictionary(pairs, uniquingKeysWith: { first, _ in first })
Setsstd
Unique, unordered, fast membership
var tags: Set<String> = ["swift", "ios"] tags.insert("xcode") // (inserted: Bool, memberAfterInsert) tags.contains("swift") // O(1) tags.remove("ios")
Set algebra
a.union(b) a.intersection(b) a.subtracting(b) a.symmetricDifference(b) a.isSubset(of: b) a.isDisjoint(with: b)
Deduplicate while keeping order
var seen = Set<String>() let unique = names.filter { seen.insert($0).inserted }
06

Control Flow

Switch is exhaustive and pattern matching runs deep. Most of the branching you would write with if elsewhere becomes a switch here.

Conditionalscore
if without parentheses
if score >= 90 { grade = "A" } else if score >= 80 { grade = "B" } else { grade = "C" }
Ternary
let label = isActive ? "on" : "off"
if and switch as expressions, Swift 5.9
let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" } let color = switch state { case .idle: "gray" case .active: "green" case .failed: "red" }
guard for preconditions
func send(_ message: String) throws { guard !message.isEmpty else { throw SendError.empty } guard isConnected else { throw SendError.offline } // happy path, unindented }
switch & Pattern Matchingcore
Exhaustive, and no implicit fallthrough
switch statusCode { case 200: print("ok") case 400..<500: print("client error") case 500...: print("server error") default: print("unknown") }
Several values in one case
switch ch { case "a", "e", "i", "o", "u": print("vowel") case "a"..."z": print("consonant") default: print("other") }
Bind values and add a where clause
switch point { case (0, 0): print("origin") case (let x, 0): print("on x axis at \(x)") case (let x, let y) where x == y: print("diagonal") case (-2...2, -2...2): print("near origin") default: print("elsewhere") }
Match on type
switch value { case let n as Int: print("int \(n)") case let s as String: print("string \(s)") case is [Any]: print("some array") default: break }
Match enums with associated values
switch result { case .success(let data) where data.isEmpty: print("empty") case .success(let data): print("\(data.count) bytes") case .failure(let error): print(error) }
Pattern match outside a switch
if case .success(let data) = result { } for case let .success(data) in results { } if 1...9 ~= digit { }
Loopscore
for in over ranges and collections
for i in 1...5 { } for i in stride(from: 0, to: 10, by: 2) { } for name in names { } for (key, value) in dict { } for _ in 1...3 { } // ignore the value
Index and element together
for (index, name) in names.enumerated() { print("\(index): \(name)") }
Filter inside the loop header
for name in names where name.count > 3 { print(name) }
while and repeat while
while !queue.isEmpty { process(queue.removeFirst()) } repeat { attempt += 1 } while attempt < 3
Labels for breaking out of nesting
outer: for row in grid { for cell in row { if cell == target { break outer } } }
defer runs on the way out
func read() throws -> Data { let handle = try FileHandle(forReadingFrom: url) defer { try? handle.close() } return try handle.readToEnd() ?? Data() }
07

Functions & Closures

Argument labels make call sites read like sentences. Closures are first class, and trailing closure syntax is why SwiftUI looks the way it does.

Functionscore
Parameters and return type
func add(_ a: Int, _ b: Int) -> Int { a + b // single expression, return is implicit } func greet(name: String) -> String { return "Hello, \(name)" }
External and internal parameter names
func move(from start: Point, to end: Point) { } move(from: a, to: b) func log(_ message: String) { } // _ removes the label log("no label needed")
Default values and variadics
func connect(host: String, port: Int = 443, retries: Int = 3) { } connect(host: "example.com") func sum(_ numbers: Int...) -> Int { numbers.reduce(0, +) } sum(1, 2, 3, 4)
inout modifies the caller's value
func double(_ value: inout Int) { value *= 2 } var score = 21 double(&score) // score is 42
Return several values with a tuple
func bounds(of values: [Int]) -> (min: Int, max: Int)? { guard let lo = values.min(), let hi = values.max() else { return nil } return (lo, hi) } if let b = bounds(of: scores) { print(b.min, b.max) }
Functions that never return
func crash(_ reason: String) -> Never { fatalError(reason) }
Closurescore
Full form, then everything Swift lets you drop
names.sorted(by: { (a: String, b: String) -> Bool in return a < b }) names.sorted(by: { a, b in a < b }) // types inferred names.sorted(by: { $0 < $1 }) // positional names names.sorted { $0 < $1 } // trailing closure names.sorted(by: <) // operator as function
Store a closure in a variable
let square: (Int) -> Int = { $0 * $0 } var onFinish: (() -> Void)? onFinish?()
Trailing closures, including several
UIView.animate(withDuration: 0.3) { view.alpha = 0 } completion: { _ in view.removeFromSuperview() }
Escaping closures outlive the call
func fetch(_ completion: @escaping (Data) -> Void) { queue.async { completion(data) } }
Capture lists control what is captured and how
// strong by default, this is the retain cycle button.onTap = { self.reload() } // break it button.onTap = { [weak self] in self?.reload() } // capture a snapshot by value let handler = { [count] in print(count) }
Unwrap self once at the top
load { [weak self] data in guard let self else { return } self.items = data self.reload() }
autoclosure delays evaluation
func assert(_ condition: Bool, _ message: @autoclosure () -> String) { if !condition { print(message()) } } assert(ok, "expensive \(buildReport())") // only built on failure
Key Paths & Function Valuesstd
Key path as a shorthand closure
let names = users.map(\.name) let adults = users.filter(\.isAdult) users.sorted(using: KeyPathComparator(\.age))
Key paths as values
let path = \User.profile.city let city = user[keyPath: path] let writable = \User.nickname user[keyPath: writable] = "Ada"
Methods are functions you can pass
let printer = print // (Any...) -> Void names.forEach(printer) let trim = String.trimmingCharacters(in:)
08

Structs & Classes

The choice between value and reference semantics is the most consequential design decision in a Swift codebase. Start with a struct.

Struct vs Classcore
Aspectstructclass
semanticsvalue, copied on assignmentreference, shared
memberwise initgenerated for freeyou write it
inheritanceno, compose with protocolsyes, single superclass
mutationmethods need mutatingfree, even on a let
deinitnot availableavailable
identitynone, compare by value=== compares identity
thread safetyeasy, copies are independentneeds care, shared state
storageusually inline or in the parentalways heap, ARC counted
reach for it whenmodels, values, most typesidentity, shared state, ObjC interop
Structscore
Definition and the free initialiser
struct User { let id: UUID var name: String var age: Int = 0 } let u = User(id: UUID(), name: "Ada", age: 36) let v = User(id: UUID(), name: "Grace") // age defaults
Copies are independent
var a = User(id: UUID(), name: "Ada") var b = a b.name = "Grace" // a.name is still "Ada"
Mutating methods
struct Counter { private(set) var value = 0 mutating func increment() { value += 1 } mutating func reset() { self = Counter() } }
Custom initialiser in an extension
extension User { init(name: String) { self.init(id: UUID(), name: name, age: 0) } }
Classesstd
Definition, init and deinit
class Session { let id: String var isActive = false init(id: String) { self.id = id } deinit { print("session \(id) released") } }
Reference semantics in one example
let s1 = Session(id: "a") let s2 = s1 s2.isActive = true // s1.isActive is now true as well, and s1 is a let s1 === s2 // true, same instance
Inheritance and overriding
class Animal { func speak() -> String { "..." } } class Dog: Animal { override func speak() -> String { "Woof" } } final class Puppy: Dog { } // final blocks further subclassing
Designated, convenience and required inits
class Vehicle { var wheels: Int init(wheels: Int) { self.wheels = wheels } convenience init() { self.init(wheels: 4) } } class Truck: Vehicle { var payload: Double init(payload: Double) { self.payload = payload // own properties first super.init(wheels: 6) // then super } }
Failable initialiser
struct Port { let number: UInt16 init?(_ value: Int) { guard (1...65535).contains(value) else { return nil } number = UInt16(value) } } Port(0) // nil
Copy on Writepattern
Why passing arrays around is cheap
let big = Array(0..<1_000_000) var copy = big // no allocation yet, storage is shared copy.append(1) // now, and only now, it is duplicated
Implement it for your own type
struct Buffer { private var storage: Storage mutating func write(_ byte: UInt8) { if !isKnownUniquelyReferenced(&storage) { storage = storage.copy() } storage.append(byte) } }
09

Enums

Swift enums carry data, conform to protocols and have methods. Used well they make illegal states impossible to represent.

Basic Enumscore
Cases and dot shorthand
enum Direction { case north, south, east, west } let heading: Direction = .north if heading == .north { }
Raw values
enum Status: Int { case ok = 200, notFound = 404, error = 500 } Status.notFound.rawValue // 404 Status(rawValue: 200) // Optional(.ok)
Iterate every case
enum Weekday: String, CaseIterable { case mon, tue, wed, thu, fri } Weekday.allCases.count // 5 Weekday.allCases.map(\.rawValue)
Methods and computed properties
enum Direction: CaseIterable { case north, south, east, west var opposite: Direction { switch self { case .north: .south case .south: .north case .east: .west case .west: .east } } func degrees() -> Double { ... } }
Associated Valuescore
Each case can carry its own payload
enum Loading { case idle case inFlight(progress: Double) case loaded(items: [Item]) case failed(Error) }
Read the payload back out
switch state { case .idle: showPlaceholder() case .inFlight(let progress): showBar(progress) case .loaded(let items): show(items) case .failed(let error): showError(error) }
Convenience accessors
extension Loading { var items: [Item] { if case .loaded(let items) = self { items } else { [] } } var isFailed: Bool { if case .failed = self { true } else { false } } }
Recursive enums need indirect
indirect enum Expr { case value(Int) case add(Expr, Expr) case multiply(Expr, Expr) } func eval(_ e: Expr) -> Int { switch e { case .value(let n): n case .add(let a, let b): eval(a) + eval(b) case .multiply(let a, let b): eval(a) * eval(b) } }
Enums with generics
enum Result<Success, Failure: Error> { case success(Success) case failure(Failure) }
OptionSet & RawRepresentablestd
OptionSet for bit flag combinations
struct Permissions: OptionSet { let rawValue: Int static let read = Permissions(rawValue: 1 << 0) static let write = Permissions(rawValue: 1 << 1) static let execute = Permissions(rawValue: 1 << 2) static let all: Permissions = [.read, .write, .execute] } var p: Permissions = [.read, .write] p.contains(.write) // true p.insert(.execute) p.remove(.read)
RawRepresentable on your own type
struct CountryCode: RawRepresentable, Hashable { let rawValue: String init?(rawValue: String) { guard rawValue.count == 2 else { return nil } self.rawValue = rawValue.uppercased() } }
Decode an unknown enum case safely
enum Status: String, Codable { case active, paused, unknown init(from decoder: Decoder) throws { let raw = try decoder.singleValueContainer().decode(String.self) self = Status(rawValue: raw) ?? .unknown } }
10

Protocols

Protocols describe capability rather than ancestry. With protocol extensions they also carry default behaviour, which is why Swift leans on composition instead of inheritance.

Defining & Conformingcore
A protocol is a contract
protocol Identifiable { var id: String { get } func refresh() throws } struct User: Identifiable { let id: String func refresh() throws { } }
Conform in an extension to keep things tidy
struct User { let id: String } extension User: Identifiable { func refresh() throws { } }
Compose several protocols
struct Article: Identifiable, Codable, Hashable, Sendable { } func render(_ item: some Identifiable & Codable) { }
Protocol inheritance and class-only protocols
protocol Reloadable: Identifiable { func reload() async } protocol Delegate: AnyObject { // classes only, so it can be weak func didFinish() }
Protocol Extensionspattern
Default implementations
protocol Greeter { var name: String { get } func greet() -> String } extension Greeter { func greet() -> String { "Hello, \(name)" } } struct English: Greeter { let name: String } // greet comes free
Constrain the extension
extension Collection where Element: Numeric { var total: Element { reduce(.zero, +) } } [1, 2, 3].total // 6
Static dispatch gotcha
protocol P { } extension P { func hello() { print("protocol") } } struct S: P { func hello() { print("struct") } } let s: P = S() s.hello() // "protocol", because hello is not a requirement
some, any and Existentialsstd
some means one specific type, chosen by the callee
func makeGreeter() -> some Greeter { English(name: "Ada") } func render(_ item: some Identifiable) { } // shorthand generic
any means a box that could hold any conformer
var greeters: [any Greeter] = [English(name: "Ada"), French(name: "Ana")] func handle(_ item: any Identifiable) { }
Associated types
protocol Repository { associatedtype Item func fetch(id: String) async throws -> Item } struct UserRepo: Repository { func fetch(id: String) async throws -> User { ... } }
Primary associated types, Swift 5.7
protocol Repository<Item> { associatedtype Item func fetch(id: String) async throws -> Item } func load(from repo: any Repository<User>) async { }
Self requirements
protocol Copyable { func copy() -> Self } extension Equatable { func isSame(as other: Self) -> Bool { self == other } }
Protocols Worth Knowingstd
ProtocolGives you
Equatable== and !=, synthesised for simple types
Hashableuse as a Dictionary key or in a Set
Comparable< > and sorted()
Codableencode to and decode from JSON and more
Identifiablea stable id, required by SwiftUI lists
CustomStringConvertibleyour own description in print
Sequence / Collectionfor in, map, filter and the rest
IteratorProtocoldrive a custom Sequence
ExpressibleByStringLiteralinit your type from a literal
RawRepresentableraw value conversion, usually via enums
Errorthrowable, usually an enum
Sendablesafe to cross a concurrency boundary
OptionSetbit flag style sets
AsyncSequencefor await iteration
Operatorsstd
Implement an operator for your type
struct Vector { var x, y: Double static func + (a: Vector, b: Vector) -> Vector { Vector(x: a.x + b.x, y: a.y + b.y) } static func += (a: inout Vector, b: Vector) { a = a + b } static prefix func - (v: Vector) -> Vector { Vector(x: -v.x, y: -v.y) } }
Declare a brand new operator
infix operator ** : MultiplicationPrecedence func ** (base: Double, power: Double) -> Double { pow(base, power) }
Custom Equatable and Comparable
struct Version: Comparable { let major, minor, patch: Int static func < (a: Version, b: Version) -> Bool { (a.major, a.minor, a.patch) < (b.major, b.minor, b.patch) } }
11

Generics

Write once, work with every type that satisfies the constraint, with no boxing and no loss of type information.

Generic Functions & Typescore
A generic function
func swapValues<T>(_ a: inout T, _ b: inout T) { (a, b) = (b, a) }
Constrain the placeholder
func largest<T: Comparable>(_ items: [T]) -> T? { items.max() } func describe<T: CustomStringConvertible & Hashable>(_ v: T) { }
Generic type
struct Stack<Element> { private var items: [Element] = [] mutating func push(_ item: Element) { items.append(item) } mutating func pop() -> Element? { items.popLast() } var isEmpty: Bool { items.isEmpty } } var s = Stack<Int>() var inferred = Stack<String>()
where clauses for the harder constraints
func merge<C1: Collection, C2: Collection>(_ a: C1, _ b: C2) -> [C1.Element] where C1.Element == C2.Element, C1.Element: Hashable { Array(Set(a).union(b)) }
Generic type aliases and opaque returns
func evens(_ xs: [Int]) -> some Sequence<Int> { xs.lazy.filter { $0 % 2 == 0 } }
Parameter packs, Swift 5.9
func describeAll<each T: CustomStringConvertible>( _ values: repeat each T ) -> [String] { var out: [String] = [] repeat out.append((each values).description) return out } describeAll(1, "two", 3.0) // ["1", "two", "3.0"]
Generic Patternspattern
Phantom types for compile time safety
struct Tagged<Tag, Value> { let value: Value } enum UserIDTag { } enum OrderIDTag { } typealias UserID = Tagged<UserIDTag, String> typealias OrderID = Tagged<OrderIDTag, String> // now a UserID cannot be passed where an OrderID is expected
Type erasure when you truly need it
struct AnyRepository<Item>: Repository { private let _fetch: (String) async throws -> Item init<R: Repository>(_ repo: R) where R.Item == Item { _fetch = repo.fetch } func fetch(id: String) async throws -> Item { try await _fetch(id) } }
Generics in Practicepattern
A generic network client
func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T { let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(T.self, from: data) } let user = try await fetch(User.self, from: url)
Constrain an extension to one element type
extension Array where Element == String { var longest: String? { self.max(by: { $0.count < $1.count }) } } extension Optional where Wrapped: Collection { var isNilOrEmpty: Bool { self?.isEmpty ?? true } }
Generic caching with a keyed store
final class Cache<Key: Hashable, Value> { private var storage: [Key: Value] = [:] subscript(key: Key) -> Value? { get { storage[key] } set { storage[key] = newValue } } }
Conditional conformance
struct Pair<T> { let a: T; let b: T } extension Pair: Equatable where T: Equatable { } extension Pair: Codable where T: Codable { }
12

Error Handling

Swift errors are values, not exceptions. A function that can fail says so in its signature, and the compiler makes callers acknowledge it.

throw, do, catchcore
Define errors as an enum
enum LoadError: Error { case notFound(path: String) case malformed case tooLarge(bytes: Int) }
Throw and call
func load(_ path: String) throws -> Data { guard exists(path) else { throw LoadError.notFound(path: path) } return try read(path) } do { let data = try load("config.json") } catch LoadError.notFound(let path) { print("missing \(path)") } catch { print("other failure: \(error)") }
try, try? and try!
let data = try load(path) // propagate, needs throws or do let maybe = try? load(path) // Optional, error discarded let forced = try! load(path) // crash if it throws
Catch several cases at once
do { try load(path) } catch LoadError.notFound, LoadError.malformed { useDefaults() } catch let error as LoadError { report(error) } catch { throw error }
defer for cleanup that always runs
func process() throws { let lock = acquire() defer { release(lock) } try risky() // lock is released either way }
Result & Friendly Messagesstd
Result when you need to store the outcome
let outcome = Result { try load(path) } switch outcome { case .success(let data): use(data) case .failure(let error): report(error) } let value = try outcome.get()
Readable error messages
extension LoadError: LocalizedError { var errorDescription: String? { switch self { case .notFound(let path): "No file at \(path)" case .malformed: "The file could not be parsed" case .tooLarge(let n): "File is too large (\(n) bytes)" } } }
rethrows passes the caller's error through
func mapAll<T, U>(_ items: [T], _ f: (T) throws -> U) rethrows -> [U] { try items.map(f) } // only throws when the closure you passed throws
Typed throws, Swift 6
func load(_ path: String) throws(LoadError) -> Data { ... } do { try load(path) } catch { // error is LoadError, no casting needed }
Assertions and traps
assert(count > 0, "count must be positive") // debug only precondition(index < size, "index out of range") // release too fatalError("unreachable")
13

Properties & Access Control

Computed properties, observers and wrappers cover most of what you would write boilerplate for elsewhere.

Property Kindscore
Stored and computed
struct Rect { var width = 0.0 var height = 0.0 var area: Double { width * height } // read only var diagonal: Double { get { (width * width + height * height).squareRoot() } set { width = newValue / 2; height = newValue / 2 } } }
Property observers
var progress: Double = 0 { willSet { print("about to become \(newValue)") } didSet { if progress != oldValue { redraw() } } }
lazy defers expensive work
class Importer { lazy var parser = ExpensiveParser() // built on first access }
Type properties and methods
struct Theme { static let shared = Theme() static var count = 0 static func reset() { count = 0 } } class Base { class func describe() -> String { "base" } // overridable }
Subscripts
struct Matrix { private var grid: [Double] let columns: Int subscript(row: Int, col: Int) -> Double { get { grid[row * columns + col] } set { grid[row * columns + col] = newValue } } }
Access Controlstd
LevelVisible to
privatethe enclosing declaration and its extensions in the same file
fileprivateeverything in the same file
internalthe whole module, this is the default
packageevery module in the same package, Swift 5.9
publicother modules, but not subclassable outside
openother modules, and subclassable or overridable there
Access & Wrappersstd
Readable outside, writable inside
public struct Cart { public private(set) var items: [Item] = [] public mutating func add(_ item: Item) { items.append(item) } }
Write your own property wrapper
@propertyWrapper struct Clamped { private var value: Int private let range: ClosedRange<Int> init(wrappedValue: Int, _ range: ClosedRange<Int>) { self.range = range self.value = min(max(wrappedValue, range.lowerBound), range.upperBound) } var wrappedValue: Int { get { value } set { value = min(max(newValue, range.lowerBound), range.upperBound) } } } struct Player { @Clamped(0...100) var health = 100 }
Wrappers you already use
@State @Binding @Environment // SwiftUI @Published // Combine @AppStorage @SceneStorage // persisted UI state @MainActor // an attribute, not a wrapper
Globals, Statics & Lifetimecareful
Globals and statics are lazy and thread safe
let shared = Service() // created on first use, exactly once enum Env { static let apiKey = ProcessInfo.processInfo.environment["API_KEY"] ?? "" }
The singleton, done properly
final class Analytics { static let shared = Analytics() private init() { } // stops anyone constructing a second one }
Mutable global state needs isolation under Swift 6
// error under strict concurrency var counter = 0 // fix: isolate it @MainActor var counter = 0 // or actor Counter { private var value = 0 }
Computed static for configuration
extension URL { static var api: URL { URL(string: "https://api.example.com")! } } // used as .api at any call site expecting a URL
14

Memory & ARC

Automatic Reference Counting frees objects the moment the last strong reference disappears. The only thing it cannot solve for you is a cycle.

The three reference kinds

strong is the default. It keeps the object alive.
weak does not keep it alive and becomes nil when it goes. Always optional, always a var.
unowned does not keep it alive and is not optional. Faster, but a crash if you are wrong about the lifetime.
Retain Cyclescareful
Two objects holding each other
class Parent { var child: Child? } class Child { var parent: Parent? } // leak // fix: one direction must be weak class Child { weak var parent: Parent? }
The closure cycle, by far the most common
class ViewModel { var onUpdate: (() -> Void)? func setup() { onUpdate = { self.reload() } // self holds closure holds self } } // fix onUpdate = { [weak self] in self?.reload() }
Delegates should be weak
protocol LoaderDelegate: AnyObject { func loaderDidFinish(_ loader: Loader) } class Loader { weak var delegate: LoaderDelegate? }
unowned when the lifetime is guaranteed
class Customer { var card: Card? } class Card { unowned let owner: Customer // a card never outlives its owner init(owner: Customer) { self.owner = owner } }
Find leaks
deinit { print("\(Self.self) deinit") } // should print when expected // Xcode: Debug Memory Graph, or Instruments Leaks // swift build -Xswiftc -warn-long-expression-type-checking
Value Types Sidestep Most of Thispattern
Structs cannot form reference cycles
struct Node { var value: Int var children: [Node] // fine, no ARC, no cycle possible }
Exclusive access to memory
var total = 0 func addTo(_ value: inout Int) { value += total } addTo(&total) // error: overlapping access to total
Capture List Recipespattern
When you need weak, and when you do not
// stored, so it can outlive the call: weak self.onDone = { [weak self] in self?.finish() } // non escaping, finishes before the call returns: no capture list items.map { self.transform($0) } // Task inside a class that may be cancelled: weak task = Task { [weak self] in await self?.load() }
Capture a value, not a reference
var count = 0 let printer = { [count] in print(count) } // captures 0 count = 5 printer() // still prints 0
Capture something other than self
button.action = { [weak viewModel, unowned router] in viewModel?.submit() router.dismiss() }
weak self in an async context
Task { [weak self] in guard let self else { return } let data = await self.load() self.apply(data) // still alive here, self is strong now }
15

Concurrency

Structured concurrency replaced callback pyramids. Under Swift 6 strict checking, data races become compile errors instead of intermittent crashes.

async and awaitcore
Declare and call
func loadUser(id: String) async throws -> User { let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data) } let user = try await loadUser(id: "42")
Enter async from synchronous code
Task { let user = try await loadUser(id: "42") await MainActor.run { self.user = user } } // SwiftUI .task { await load() }
Run independent work in parallel
async let profile = loadProfile() async let posts = loadPosts() let (p, ps) = try await (profile, posts) // both run at once
A dynamic number of children
let images = try await withThrowingTaskGroup(of: Image.self) { group in for url in urls { group.addTask { try await download(url) } } var result: [Image] = [] for try await image in group { result.append(image) } return result }
Cancellation is cooperative
let task = Task { try await longJob() } task.cancel() // inside the job try Task.checkCancellation() if Task.isCancelled { return } try await Task.sleep(for: .seconds(1)) // throws on cancel
Actors & Isolationstd
An actor serialises access to its state
actor Counter { private var value = 0 func increment() { value += 1 } func current() -> Int { value } } let counter = Counter() await counter.increment() let n = await counter.current()
MainActor for anything touching UI
@MainActor final class ViewModel: ObservableObject { @Published var items: [Item] = [] func load() async { let fetched = await service.fetch() // hops off and back items = fetched // already on the main actor } } @MainActor func updateLabel() { }
Sendable, the crossing guard
struct Payload: Sendable { let id: String } // value types: automatic final class Config: Sendable { let name: String // immutable, so it is safe init(name: String) { self.name = name } } @unchecked Sendable // "trust me", you handle the locking
nonisolated escapes actor isolation
actor Store { let id: String nonisolated var describedBy: String { "Store(\(id))" } }
Async Sequences & Bridgingstd
for await
for try await line in url.lines { print(line) } for await note in NotificationCenter.default.notifications(named: .didUpdate) { }
Make your own stream
let stream = AsyncStream<Int> { continuation in let timer = startTimer { continuation.yield($0) } continuation.onTermination = { _ in timer.stop() } } for await tick in stream { print(tick) }
Wrap a callback API
func load() async throws -> Data { try await withCheckedThrowingContinuation { continuation in legacyLoad { result in continuation.resume(with: result) } } }
Detached work and priorities
Task.detached(priority: .background) { await reindex() } await Task.yield() try await Task.sleep(for: .milliseconds(250))
Combinelegacy
Publisher, operators, subscriber
import Combine var bag = Set<AnyCancellable>() searchField.textPublisher .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() .filter { $0.count > 2 } .sink { [weak self] term in self?.search(term) } .store(in: &bag)
Common publishers
Just(5) Future { promise in promise(.success(1)) } URLSession.shared.dataTaskPublisher(for: url) NotificationCenter.default.publisher(for: .didUpdate) Timer.publish(every: 1, on: .main, in: .common).autoconnect() $searchTerm // from @Published
Bridge Combine into async await
let value = try await publisher.values.first { _ in true } for await item in publisher.values { handle(item) }
16

SwiftUI

Views are values that describe the interface for the current state. You never mutate a view, you change state and let the framework rebuild.

Views & Layoutui
A view is a struct with a body
struct ContentView: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Hello") .font(.title) .foregroundStyle(.primary) Text("World").font(.subheadline) } .padding() } }
Stacks and spacing
VStack { } // vertical HStack { } // horizontal ZStack { } // layered Spacer() // pushes content apart Divider() Grid { GridRow { } }
Modifier order matters
Text("Hi").padding().background(.blue) // blue includes the padding Text("Hi").background(.blue).padding() // padding outside the blue
Common modifiers
.frame(width: 100, height: 44) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) .background(.thinMaterial, in: .rect(cornerRadius: 12)) .opacity(0.8) .disabled(isBusy) .accessibilityLabel("Save")
State & Data Flowui
State owned by this view
struct Counter: View { @State private var count = 0 var body: some View { Button("Tapped \(count)") { count += 1 } } }
Binding shares write access with a child
struct Parent: View { @State private var text = "" var body: some View { Child(text: $text) } } struct Child: View { @Binding var text: String var body: some View { TextField("Name", text: $text) } }
Observation, Swift 5.9 and later
import Observation @Observable final class Model { var items: [Item] = [] var isLoading = false } struct ListView: View { @State private var model = Model() var body: some View { List(model.items) { Text($0.title) } } }
The older ObservableObject route
final class Model: ObservableObject { @Published var items: [Item] = [] } @StateObject private var model = Model() // owner @ObservedObject var model: Model // passed in @EnvironmentObject var session: Session // injected
Environment values
@Environment(\.colorScheme) private var scheme @Environment(\.dismiss) private var dismiss @AppStorage("username") private var username = ""
Lists, Navigation & Presentationui
List over Identifiable data
List(items) { item in Text(item.title) } ForEach(items) { item in Row(item) } ForEach(0..<5, id: \.self) { i in Text("\(i)") }
Navigation stack with a typed path
NavigationStack(path: $path) { List(items) { item in NavigationLink(item.title, value: item) } .navigationTitle("Items") .navigationDestination(for: Item.self) { DetailView(item: $0) } }
Sheets, alerts and confirmations
.sheet(isPresented: $showing) { EditView() } .sheet(item: $selected) { DetailView(item: $0) } .alert("Delete?", isPresented: $confirming) { Button("Delete", role: .destructive) { delete() } Button("Cancel", role: .cancel) { } }
Async loading and refresh
List(model.items) { Row($0) } .task { await model.load() } .refreshable { await model.reload() } .overlay { if model.isLoading { ProgressView() } }
Animation
withAnimation(.spring(duration: 0.3)) { isExpanded.toggle() } Circle() .scaleEffect(isExpanded ? 1.4 : 1) .animation(.easeInOut, value: isExpanded) .transition(.opacity.combined(with: .scale))
Previews
#Preview("Loaded") { ContentView() .environment(Model.sample) }
Accessibility & Localizationui
Describe the interface to VoiceOver
Image(systemName: "trash") .accessibilityLabel("Delete") .accessibilityHint("Removes this item permanently") .accessibilityAddTraits(.isButton) HStack { Text(name); Text(role) } .accessibilityElement(children: .combine) decorativeStripe.accessibilityHidden(true)
Respect the user's settings
@Environment(\.dynamicTypeSize) private var typeSize @Environment(\.accessibilityReduceMotion) private var reduceMotion Text("Title").font(.title) // scales with Dynamic Type withAnimation(reduceMotion ? nil : .spring()) { expand() }
Localised strings
Text("welcome_title") // looked up automatically Text("Hello \(name)") // String Catalog picks it up String(localized: "cart.items", defaultValue: "\(count) items", comment: "Cart count")
17

UIKit

Most shipping iOS code is still UIKit, and mixed UIKit and SwiftUI apps are the norm. Here is the imperative half of the platform.

View Controller Lifecycleui
The callbacks, in the order they fire
final class DetailViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // once, after the view is created } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // every time it is about to show } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() } }
Present and dismiss
let vc = DetailViewController() vc.modalPresentationStyle = .pageSheet present(vc, animated: true) dismiss(animated: true) navigationController?.pushViewController(vc, animated: true) navigationController?.popViewController(animated: true)
Alerts and action sheets
let alert = UIAlertController(title: "Delete?", message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in self.delete() }) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) present(alert, animated: true)
Auto Layoutui
Constraints in code
let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false view.addSubview(label) NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), label.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -20) ])
Priorities and hugging
let c = label.widthAnchor.constraint(equalToConstant: 200) c.priority = .defaultHigh // 750 c.isActive = true label.setContentHuggingPriority(.required, for: .horizontal) label.setContentCompressionResistancePriority(.required, for: .vertical)
Stack views do most of the work
let stack = UIStackView(arrangedSubviews: [title, subtitle]) stack.axis = .vertical stack.spacing = 8 stack.alignment = .leading stack.distribution = .fill
Table & Collection Viewsui
Diffable data source, the modern way
enum Section { case main } let dataSource = UITableViewDiffableDataSource<Section, Item.ID>( tableView: tableView ) { table, indexPath, id in let cell = table.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.contentConfiguration = self.configuration(for: id) return cell } var snapshot = NSDiffableDataSourceSnapshot<Section, Item.ID>() snapshot.appendSections([.main]) snapshot.appendItems(items.map(\.id)) await dataSource.apply(snapshot, animatingDifferences: true)
Classic data source methods
func tableView(_ t: UITableView, numberOfRowsInSection s: Int) -> Int { items.count } func tableView(_ t: UITableView, cellForRowAt i: IndexPath) -> UITableViewCell { let cell = t.dequeueReusableCell(withIdentifier: "Cell", for: i) return cell } func tableView(_ t: UITableView, didSelectRowAt i: IndexPath) { t.deselectRow(at: i, animated: true) }
Compositional layout for collection views
let item = NSCollectionLayoutItem(layoutSize: .init( widthDimension: .fractionalWidth(1), heightDimension: .absolute(60))) let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init( widthDimension: .fractionalWidth(1), heightDimension: .absolute(60)), subitems: [item]) let layout = UICollectionViewCompositionalLayout(section: .init(group: group))
Targets, Gestures & Interopui
Wire up a control
button.addTarget(self, action: #selector(tapped), for: .touchUpInside) @objc private func tapped() { } // or with a closure, iOS 14 and later button.addAction(UIAction { _ in self.reload() }, for: .touchUpInside)
Gesture recognisers
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap)) view.addGestureRecognizer(tap) view.isUserInteractionEnabled = true
Put SwiftUI inside UIKit
let host = UIHostingController(rootView: ProfileView()) addChild(host) view.addSubview(host.view) host.didMove(toParent: self)
Put UIKit inside SwiftUI
struct MapView: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { MKMapView() } func updateUIView(_ view: MKMapView, context: Context) { } func makeCoordinator() -> Coordinator { Coordinator() } }
18

Persistence

SwiftData is the modern declarative store. Core Data still runs underneath it and is what most existing apps use.

SwiftDatamodern
Define a model
import SwiftData @Model final class Task { var title: String var isDone: Bool var created: Date @Relationship(deleteRule: .cascade) var subtasks: [Task] = [] init(title: String, isDone: Bool = false) { self.title = title self.isDone = isDone self.created = .now } }
Wire the container into the app
@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Task.self) } }
Query and mutate from a view
struct TaskList: View { @Environment(\.modelContext) private var context @Query(sort: \Task.created, order: .reverse) private var tasks: [Task] var body: some View { List(tasks) { task in Text(task.title) } Button("Add") { context.insert(Task(title: "New")) } } }
Filter with a predicate
@Query(filter: #Predicate<Task> { !$0.isDone }, sort: \Task.created) private var open: [Task] let descriptor = FetchDescriptor<Task>( predicate: #Predicate { $0.title.contains("swift") }) let found = try context.fetch(descriptor)
Delete and save
context.delete(task) try context.save() // usually automatic, explicit when you need it
Core Datastd
Load the stack
let container = NSPersistentContainer(name: "Model") container.loadPersistentStores { _, error in if let error { fatalError("store failed: \(error)") } } let context = container.viewContext
Fetch with a predicate and sort
let request: NSFetchRequest<TaskEntity> = TaskEntity.fetchRequest() request.predicate = NSPredicate(format: "isDone == %@", NSNumber(value: false)) request.sortDescriptors = [NSSortDescriptor(key: "created", ascending: false)] request.fetchLimit = 50 let results = try context.fetch(request)
Insert, delete, save
let task = TaskEntity(context: context) task.title = "Write tests" context.delete(other) if context.hasChanges { try context.save() }
Background work
container.performBackgroundTask { bg in // bg is a private queue context, never touch it from the main thread let item = TaskEntity(context: bg) item.title = "Imported" try? bg.save() }
Files, Defaults & Keychaincareful
Save a Codable value to disk
let url = URL.documentsDirectory.appending(path: "state.json") try JSONEncoder().encode(state).write(to: url, options: .atomic) let restored = try JSONDecoder().decode(State.self, from: Data(contentsOf: url))
Secrets belong in the Keychain
let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: "authToken", kSecValueData as String: Data(token.utf8) ] SecItemDelete(query as CFDictionary) let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecSuccess else { throw KeychainError.save(status) }
Pick the right store
// UserDefaults small preferences, plist backed // File on disk documents the user owns, exportable // Keychain credentials and tokens, encrypted // SwiftData structured, queryable, relational // Core Data the same, with a longer history and more control
19

Foundation & Codable

JSON, dates, files and networking. Codable in particular removes most of the parsing code you would otherwise hand write.

Codablestd
Encode and decode for free
struct User: Codable { let id: Int let name: String let email: String? } let data = try JSONEncoder().encode(user) let decoded = try JSONDecoder().decode(User.self, from: data) let list = try JSONDecoder().decode([User].self, from: data)
Map different JSON key names
struct User: Codable { let id: Int let fullName: String enum CodingKeys: String, CodingKey { case id case fullName = "full_name" } }
Or convert every key at once
let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .iso8601 let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
Custom decoding for awkward payloads
init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) name = try c.decodeIfPresent(String.self, forKey: .name) ?? "Unknown" }
Read a decoding failure properly
do { _ = try JSONDecoder().decode(User.self, from: data) } catch let DecodingError.keyNotFound(key, context) { print("missing \(key.stringValue): \(context.debugDescription)") } catch let DecodingError.typeMismatch(type, context) { print("expected \(type) at \(context.codingPath)") }
Networkingstd
GET with async await
let url = URL(string: "https://api.example.com/users")! let (data, response) = try await URLSession.shared.data(from: url) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw APIError.badStatus } let users = try JSONDecoder().decode([User].self, from: data)
POST JSON
var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode(payload) let (data, _) = try await URLSession.shared.data(for: request)
Build URLs with query items
var components = URLComponents(string: "https://api.example.com/search")! components.queryItems = [ URLQueryItem(name: "q", value: term), URLQueryItem(name: "page", value: "1") ] let url = components.url!
Dates, Files & Defaultsstd
Dates and formatting
let now = Date() now.formatted(date: .abbreviated, time: .shortened) now.formatted(.relative(presentation: .named)) // "2 hours ago" let iso = ISO8601DateFormatter().string(from: now) let later = now.addingTimeInterval(3600) Calendar.current.date(byAdding: .day, value: 7, to: now)
Files
let docs = URL.documentsDirectory let file = docs.appending(path: "notes.json") try data.write(to: file) let loaded = try Data(contentsOf: file) FileManager.default.fileExists(atPath: file.path()) try FileManager.default.removeItem(at: file)
UserDefaults for small settings
UserDefaults.standard.set(true, forKey: "hasLaunched") UserDefaults.standard.bool(forKey: "hasLaunched") UserDefaults.standard.string(forKey: "username")
20

Testing

Swift Testing is the modern framework built around macros and plain expressions. XCTest is still everywhere, and the two run side by side.

Swift Testingmodern
A test is a function with an attribute
import Testing @Test func addsTwoNumbers() { #expect(add(2, 2) == 4) } @Test("Rejects an empty name") func rejectsEmpty() throws { #expect(throws: ValidationError.self) { try validate(name: "") } }
Run one test over many inputs
@Test(arguments: [1, 2, 3, 100]) func isPositive(_ value: Int) { #expect(value > 0) } @Test(arguments: zip(inputs, expected)) func converts(_ input: String, _ want: Int) throws { #expect(try convert(input) == want) }
Group, tag and control tests
@Suite("Parser") struct ParserTests { @Test(.tags(.slow)) func handlesLargeFile() async throws { } @Test(.disabled("flaky on CI")) func unstable() { } @Test(.timeLimit(.minutes(1))) func bounded() async { } }
Stop the test on a failed requirement
@Test func decodes() throws { let user = try #require(try? decode(sample)) // unwraps or fails now #expect(user.name == "Ada") }
Setup and teardown are just init and deinit
struct DatabaseTests { let db: Database init() async throws { db = try await Database.temporary() } // each test gets a fresh instance, no shared state }
XCTeststd
The classic shape
import XCTest @testable import MyApp final class MathTests: XCTestCase { override func setUp() { } override func tearDown() { } func testAdd() { XCTAssertEqual(add(2, 2), 4) } }
The assertions you will use
XCTAssertEqual(a, b) XCTAssertTrue(flag) XCTAssertNil(value) XCTAssertThrowsError(try risky()) XCTAssertNoThrow(try safe()) XCTUnwrap(optional) // throws instead of crashing
Async tests and expectations
func testLoad() async throws { let user = try await service.load(id: "1") XCTAssertEqual(user.name, "Ada") } func testCallback() { let exp = expectation(description: "finished") service.load { _ in exp.fulfill() } wait(for: [exp], timeout: 2) }
Measure performance
func testSortPerformance() { measure { _ = largeArray.sorted() } }
Making Code Testablepattern
Inject the dependency instead of reaching for a singleton
protocol UserLoading { func load(id: String) async throws -> User } struct ViewModel { let loader: UserLoading // not URLSession.shared init(loader: UserLoading = APIClient()) { self.loader = loader } }
A stub that records what happened
final class LoaderSpy: UserLoading { private(set) var requested: [String] = [] var result: Result<User, Error> = .success(.sample) func load(id: String) async throws -> User { requested.append(id) return try result.get() } }
Sample data as a static
extension User { static let sample = User(id: "1", name: "Ada", email: nil) } // usable from tests and from #Preview alike
Stub the network layer itself
final class StubProtocol: URLProtocol { static var handler: ((URLRequest) -> (HTTPURLResponse, Data))? override class func canInit(with: URLRequest) -> Bool { true } override func startLoading() { } override func stopLoading() { } } let config = URLSessionConfiguration.ephemeral config.protocolClasses = [StubProtocol.self]
21

Debugging & LLDB

The debugger console is faster than another print and rebuild cycle. These are the commands worth knowing by heart.

LLDB Commandsworkflow
CommandWhat it does
po valueprint the object description
p valueprint with type information
v valueprint a variable without running code
e expressionevaluate, and it can mutate state
e -l swift -- codeforce the Swift language context
btbacktrace for the current thread
bt allbacktrace for every thread
frame variablelist everything in the current frame
frame select 2move up the stack
thread step-over (n)step over one line
thread step-in (s)step into the call
continue (c)resume execution
b MyType.methodset a symbolic breakpoint
br list / br deletelist or remove breakpoints
watchpoint set variable xbreak when x changes
image lookup -a addressresolve a crash address to a symbol
expr -O -- objectdescription of a complex object
Breakpoints Worth Settingworkflow
Catch the exact moment something goes wrong
// Xcode breakpoint navigator, then the plus button: // All Exceptions stops where an ObjC exception is raised // Swift Error stops on any throw // Symbolic break on a symbol you do not have source for // Constraint Error stops on an Auto Layout conflict
Conditional and counted breakpoints
// right click a breakpoint, Edit Breakpoint: // Condition index == 42 // Ignore skip the first N hits // Action Debugger Command, or Log Message // Options Automatically continue after evaluating
Assertions that only fire in debug
assert(index < count, "index out of range") // debug only assertionFailure("should not reach here") // debug only precondition(index < count) // debug and release fatalError("unrecoverable") // always, returns Never
Logging & Inspectionstd
Unified logging, not print
import OSLog let log = Logger(subsystem: "com.example.app", category: "network") log.debug("requesting \(url, privacy: .public)") log.info("loaded \(items.count) items") log.error("failed: \(error.localizedDescription)")
Where am I
print(#file, #line, #function) dump(model) // full structure, nested debugPrint(value) // uses debugDescription
Measure a block
let start = ContinuousClock.now work() print("took \(ContinuousClock.now - start)") let signposter = OSSignposter(subsystem: "app", category: "perf") let state = signposter.beginInterval("import") signposter.endInterval("import", state)
Runtime diagnostics worth enabling
// Scheme, Diagnostics tab: // Address Sanitizer memory errors // Thread Sanitizer data races // Main Thread Checker UIKit called off the main thread // Malloc Scribble / Guard heap corruption // Zombie Objects messages to deallocated instances
Find retain cycles
deinit { print("\(Self.self) deinit") } // should fire when you expect // Xcode: Debug Memory Graph button, filter to your type, // then look at the inbound arrows that keep it alive.
Reading a Crashcareful
What the common crash types mean
// EXC_BAD_ACCESS touched deallocated or invalid memory // EXC_BREAKPOINT a Swift runtime trap: force unwrap of nil, // array index out of range, integer overflow // SIGABRT an assertion, precondition, or ObjC exception // Watchdog 0x8badf00d the main thread was blocked too long
Symbolicate a report
atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x100000000 0x1000123ab // keep the dSYM for every release build, it is the only way // to turn addresses back into file and line numbers
22

Package Manager & Tooling

Swift Package Manager ships with the toolchain and is the default way to build, test and depend on code, inside Xcode and on the command line alike.

Commandsstd
CommandWhat it does
swift package initscaffold a new package
swift buildcompile in debug
swift build -c releasecompile optimised
swift runbuild and run the executable
swift testrun the test targets
swift test --filter Parserrun a subset
swift package resolvefetch and pin dependencies
swift package updatemove within the allowed ranges
swift package show-dependenciesprint the dependency tree
swift package cleandelete the build folder
swift-format -i -r Sourcesformat in place
xcrun swift-format lintlint without rewriting
xcodebuild -scheme App testrun tests in CI
Package.swiftstd
A realistic manifest
// swift-tools-version: 6.0 import PackageDescription let package = Package( name: "MyLibrary", platforms: [.iOS(.v17), .macOS(.v14)], products: [ .library(name: "MyLibrary", targets: ["MyLibrary"]) ], dependencies: [ .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") ], targets: [ .target(name: "MyLibrary", dependencies: [ .product(name: "Algorithms", package: "swift-algorithms") ]), .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]) ] )
Version requirements
.package(url: url, from: "1.2.0") // 1.2.0 up to 2.0.0 .package(url: url, .upToNextMinor(from: "1.2.0")) .package(url: url, exact: "1.2.3") .package(url: url, branch: "main") .package(path: "../LocalPackage")
Turn on strict concurrency while migrating
.target( name: "MyLibrary", swiftSettings: [ .enableUpcomingFeature("StrictConcurrency"), .swiftLanguageMode(.v6) // swift-tools-version 6.0 and later ] )
Bundle resources
.target(name: "MyLibrary", resources: [ .process("Assets.xcassets"), .copy("seed.json") ]) let url = Bundle.module.url(forResource: "seed", withExtension: "json")
Xcode Shortcutsworkflow
KeysAction
Cmd Rrun
Cmd Urun tests
Cmd Bbuild
Cmd Shift Kclean build folder
Cmd Shift Oopen quickly, jump to any symbol
Ctrl Cmd Jjump to definition
Ctrl Cmd Leftgo back
Cmd Shift Ffind in project
Ctrl Ire-indent selection
Cmd /toggle comment
Cmd Ctrl Erename in scope
Cmd Shift Aquick actions menu
Cmd Opt Presume the preview canvas
Cmd Shift Ytoggle the debug area
Targets & Productsstd
An executable alongside the library
products: [ .library(name: "Core", targets: ["Core"]), .executable(name: "cli", targets: ["CLI"]) ], targets: [ .target(name: "Core"), .executableTarget(name: "CLI", dependencies: ["Core"]), .testTarget(name: "CoreTests", dependencies: ["Core"]) ]
A command line interface with ArgumentParser
import ArgumentParser @main struct Tool: ParsableCommand { @Argument(help: "File to read") var input: String @Option(name: .shortAndLong) var output: String = "out.txt" @Flag(name: .shortAndLong) var verbose = false func run() throws { } }
Binary and system library targets
.binaryTarget(name: "Analytics", url: "https://…/Analytics.xcframework.zip", checksum: "abc123…") .systemLibrary(name: "CSQLite", pkgConfig: "sqlite3")
Keep dependencies honest
swift package show-dependencies --format tree swift package diagnose-api-breaking-changes 1.2.0 swift package resolve # commit Package.resolved for apps // libraries usually do not commit Package.resolved
23

Macros & Attributes

Attributes steer the compiler. Macros, added in Swift 5.9, generate real source code at build time and are fully type checked.

Attributes You Will Meetstd
AttributeMeaning
@mainmarks the program entry point
@escapingthe closure outlives the call
@autoclosurewrap the argument in a closure automatically
@discardableResultno warning if the return value is ignored
@availablegate on OS version or mark deprecated
@inlinableexpose the body for cross module inlining
@MainActorisolate to the main actor
@Sendablethe closure is safe to send across tasks
@objc / @objcMembersexpose to the Objective-C runtime
@dynamicMemberLookupresolve arbitrary members at compile time
@resultBuilderbuild values from a statement list, as SwiftUI does
@frozenpromise the layout will not change, ABI stability
@testableimport a module with internal access
Availability & Deprecationcore
Gate on OS version
@available(iOS 17, macOS 14, *) func modernOnly() { } if #available(iOS 17, *) { modernOnly() } else { fallback() }
Deprecate with a migration path
@available(*, deprecated, renamed: "connect(to:)") func connect(url: URL) { } @available(*, unavailable, message: "Use the async version") func loadSync() { } @discardableResult func save() -> Bool { true }
Macrosmodern
Macros you already use
#Preview { ContentView() } // SwiftUI preview @Observable final class Model { } // observation @Test func works() { } // Swift Testing #expect(a == b) #warning("finish this") #error("do not ship")
Freestanding macros for source locations
print(#file, #line, #function) print(#filePath) let literal = #"raw \n stays raw"#
Declare your own
@freestanding(expression) public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacros", type: "StringifyMacro") let (result, code) = #stringify(2 + 3) // (5, "2 + 3")
Attached macro roles
@attached(member) // adds properties or methods @attached(peer) // adds declarations alongside @attached(accessor) // turns a stored property into computed @attached(extension, conformances: Codable)
Result Builderspattern
What makes SwiftUI's syntax possible
@resultBuilder struct StringBuilder { static func buildBlock(_ parts: String...) -> String { parts.joined(separator: "\n") } static func buildOptional(_ part: String?) -> String { part ?? "" } static func buildEither(first: String) -> String { first } static func buildEither(second: String) -> String { second } static func buildArray(_ parts: [String]) -> String { parts.joined(separator: "\n") } }
Use it
@StringBuilder func report(showDetail: Bool) -> String { "Header" if showDetail { "Detail line" } for item in items { item.title } "Footer" }
Builders you already use
@ViewBuilder // SwiftUI view bodies @SceneBuilder // App scenes @ToolbarContentBuilder @RegexComponentBuilder // RegexBuilder DSL
24

Compiler Error Index

The messages that actually stop people, each with the smallest reproduction and the fix that resolves it.

Optionals & Typescommon
Value of optional type must be unwrapped
let name: String? = "Ada" print(name.count) // error // fix print(name?.count ?? 0) if let name { print(name.count) }
Cannot convert value of type Int to expected Double
let total = 3 * 0.5 // fine, both are literals let count = 3 let bad = count * 0.5 // error // fix let good = Double(count) * 0.5
Cannot assign to property, self is immutable
struct Counter { var value = 0 func bump() { value += 1 } // error } // fix: mutating func bump() { value += 1 }
Type of expression is ambiguous without more context
let value = [] // error let coder = JSONDecoder().decode(from: data) // error // fix: say what you want let value: [Int] = [] let user = try JSONDecoder().decode(User.self, from: data)
Missing argument label
func move(from a: Int, to b: Int) { } move(1, 2) // error // fix move(from: 1, to: 2)
Initialisation & Protocolscommon
Return from initializer without initializing all stored properties
struct User { let id: String let name: String init(id: String) { self.id = id } // error, name unset } // fix: set name, give it a default, or make it optional
Type does not conform to protocol
struct User: Equatable { let handler: () -> Void // closures are not Equatable } // fix: remove the closure from the type, or write == by hand
Escaping closure captures mutating self
struct Loader { var items: [Item] = [] mutating func load() { service.fetch { self.items = $0 } // error in a struct } } // fix: make Loader a class, or an actor, or hand the result back
Protocol can only be used as a generic constraint
// older Swift var repo: Repository = UserRepo() // error if it has associatedtype // fix, Swift 5.7 and later var repo: any Repository<User> = UserRepo() func use(_ repo: some Repository) { }
Concurrency & SwiftUIcommon
Expression is async but is not marked with await
let user = loadUser() // error // fix: let user = await loadUser(), inside an async context or a Task
Main actor isolated property cannot be referenced
Task.detached { self.label.text = "done" // error, UI off the main actor } // fix Task { @MainActor in self.label.text = "done" }
Type does not conform to Sendable
final class Cache { var items: [String] = [] } Task { cache.items.append("x") } // error under Swift 6 // fix: make it an actor, or make the state immutable actor Cache { var items: [String] = [] }
The compiler is unable to type check this expression in reasonable time
// a long chain of + and ternaries in one expression // fix: split it into several lets and annotate the types let a: Double = base * factor let b: Double = a + offset
25

Coming From Another Language

Direct translations for what you already know how to do. Find the habit on the left, learn its Swift spelling on the right.

Objective-C to Swifttranslation
Objective-CSwift
NSString *String
NSArray / NSMutableArraylet [T] / var [T]
NSDictionary[K: V]
NSNumberInt, Double, Bool
nil check on every callOptional plus if let
idAny or AnyObject
@property (nonatomic, strong)var
@property (weak)weak var
@interface / @implementationone struct or class, no header
@protocolprotocol
categoriesextension
blocksclosures
NSError **throws
#importimport, no header files
Kotlin to Swifttranslation
KotlinSwift
val / varlet / var
String?String?
?: elvis?? nil coalescing
?.let { }if let
data classstruct with Equatable and Hashable
sealed classenum with associated values
whenswitch
interfaceprotocol
companion objectstatic members
extension functionsextension
suspend funasync func
coroutineScopewithTaskGroup
FlowAsyncSequence or AsyncStream
listOf / mutableListOflet [T] / var [T]
TypeScript to Swifttranslation
TypeScriptSwift
const / letlet / var
string | undefinedString?
?? and ?.?? and ?.
interface / typeprotocol / struct
union type A | Benum with associated values
Array<T> / T[][T]
Record<K, V>[K: V]
map / filter / reducemap / filter / reduce
async / awaitasync / await, plus try
Promise.allasync let, or a task group
try / catchdo / catch with throws
JSON.parseJSONDecoder().decode
npm installadd to Package.swift
generics <T extends X><T: X>
Python to Swifttranslation
PythonSwift
x = 5let x = 5
list / dict / set[T] / [K: V] / Set<T>
Nonenil, inside an Optional
len(x)x.count
f"{name}""\(name)"
[f(x) for x in xs]xs.map(f)
[x for x in xs if p(x)]xs.filter(p)
for i, v in enumerate(xs)for (i, v) in xs.enumerated()
class / @dataclassclass / struct
try / exceptdo / catch
raise ValueErrorthrow MyError.invalid
with open(p) as fdefer for cleanup
json.loadsJSONDecoder().decode
pytestswift test
Habits to Unlearnmindset
Stop reaching for a class by default
// most models want to be values struct User: Codable, Hashable { let id: UUID; var name: String } // use a class when you need identity or shared mutable state final class SessionStore { }
Stop using a sentinel for absence
// not -1, not "", not a magic zero func index(of item: Item) -> Int? func load() throws -> Config
Stop modelling state with loose booleans
// three booleans allow eight states, only four are real var isLoading = false; var error: Error?; var items: [Item] = [] // one enum allows exactly the states that exist enum State { case idle, loading, loaded([Item]), failed(Error) }
Let extensions organise, not inheritance
extension User: Codable { } extension User: CustomStringConvertible { var description: String { name } } // each conformance in its own extension, easy to read and to move
Objective-C Interopinterop
Expose Swift to Objective-C
@objc final class Bridge: NSObject { @objc func refresh() { } @objc var title: String = "" } @objcMembers final class Everything: NSObject { } // exposes all members
Use Objective-C from Swift
// App target: add the header to MyApp-Bridging-Header.h #import "LegacyManager.h" // Then use it directly let manager = LegacyManager() manager.start()
Nullability decides what Swift sees
// Objective-C header - (nullable NSString *)nameFor:(NSInteger)id; // becomes String? - (nonnull NSString *)title; // becomes String // unannotated becomes String!, an implicitly unwrapped optional
Rename across the boundary
@objc(TMSUserManager) final class UserManager: NSObject { @objc(refreshWithForce:) func refresh(force: Bool) { } }

What Swift Is

Swift is a compiled, statically typed language built by Apple to replace Objective-C, and designed so that the safe way to write something is also the short way. Optionals remove null crashes, value types remove aliasing bugs, and since Swift 6 the compiler catches data races too.

Chris Lattner started it in 2010, Apple announced it at WWDC in 2014, and it went open source in December 2015. Governance now sits with the Swift project, and language changes go through a public proposal process on GitHub rather than being handed down.

It is the default for every Apple platform, but it is not limited to them. Swift compiles on Linux and Windows, powers server frameworks such as Vapor, and runs on microcontrollers through Embedded Swift.

The trade compared with a dynamic language is straightforward. You spend more time being precise about types up front, and in exchange a large class of runtime failures becomes a build error you fix in seconds.


Swift Syntax Basics

Swift reads like a modern curly-brace language. Types come after the name, semicolons are optional and almost never used, and type inference means you rarely write the type at all.

Constants and variables

Use let for anything that does not change, which in practice is most things. The compiler warns when a var is never mutated.

let maximum = 100
var counter = 0
counter += 1

let name: String = "Ada"   // explicit type, usually unnecessary

Functions and argument labels

Swift separates the name the caller uses from the name the body uses. Well chosen labels are why idiomatic Swift reads almost like prose at the call site.

func move(from start: Point, to end: Point) { }
move(from: a, to: b)

func log(_ message: String) { }   // underscore removes the label
log("no label needed")

Control flow

Switch is exhaustive, so leaving out a case is a compile error rather than a silent fallthrough. Since Swift 5.9 both if and switch also work as expressions.

let grade = switch score {
    case 90...: "A"
    case 80..<90: "B"
    default: "C"
}

Optionals, the Idea That Defines Swift

There is no implicit null. A value that might be absent has the type String?, which is a genuinely different type from String, and the compiler will not let you use one where the other is expected.

var middleName: String?          // nil until set

if let middleName {
    print(middleName)            // non optional inside this branch
}

let display = middleName ?? "none"
let length = user?.profile?.bio?.count   // nil if any link is nil

The everyday choice is between if let and guard let. Use guard let when the value is required for the rest of the function, because it exits early and keeps the unwrapped binding in scope below, which flattens nesting considerably.

func greet(_ name: String?) {
    guard let name else { return }
    print("Hello, \(name)")      // name is a plain String here
}

Force unwrapping with ! exists, and it crashes when the value is nil. It is occasionally the honest choice, for a resource you ship inside the app bundle, but every one is a promise about data you may not control.


Value Types and Reference Types

This is the design decision that shapes a Swift codebase most. A struct is a value type: assigning it makes an independent copy. A class is a reference type: assigning it shares one instance.

Questionstructclass
copied on assignmentyesno, shared
free memberwise inityesno
inheritancenoyes
can have deinitnoyes
identity with ===noyes
safe across threadsusuallyneeds care

The guidance from Apple, and from most production codebases, is to start with a struct and move to a class only when you need identity, inheritance, a deinitialiser, or genuinely shared mutable state. Passing large structs around is cheap because Array, String, Dictionary and Set all use copy on write: the underlying buffer is shared until something actually mutates it.


Protocols Instead of Inheritance

Swift is often described as protocol oriented. A protocol declares capability, any type can adopt as many as it likes, and protocol extensions supply default implementations so adoption costs almost nothing.

protocol Greeter {
    var name: String { get }
    func greet() -> String
}

extension Greeter {
    func greet() -> String { "Hello, \(name)" }
}

struct English: Greeter { let name: String }   // greet comes for free

One rule catches everyone once: only methods declared in the protocol body are dynamically dispatched. A method that exists solely in the extension is selected by the static type of the variable, so overriding it in a conforming type will not take effect through a protocol-typed reference.

Since Swift 5.7 the keywords some and any make the cost explicit. Prefer some for parameters and return types, because it stays generic and free. Reach for any only when you need to store different concrete types in one collection.


Concurrency Without Callback Pyramids

Structured concurrency landed in Swift 5.5 and changed how asynchronous code is written. Instead of nesting completion handlers, you write straight-line code with ordinary try and catch.

func loadDashboard() async throws -> Dashboard {
    async let profile = loadProfile()
    async let posts = loadPosts()
    return try await Dashboard(profile: profile, posts: posts)
}

Because the two loads start immediately and the await collects them, this costs the slower of the two rather than the sum. Actors then protect shared mutable state by serialising access, and @MainActor guarantees that anything touching the interface runs on the main thread.

Swift 6 turned strict concurrency checking on by default. Passing a non-Sendable value across an actor boundary is now a compile error, which is uncomfortable during migration and genuinely prevents the intermittent crashes that used to survive code review.


SwiftUI in One Paragraph

A SwiftUI view is a struct describing what the interface should look like for the current state. You never mutate a view. You change state, and the framework works out the smallest redraw.

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

    var body: some View {
        VStack(spacing: 12) {
            Text("Count: \(count)")
                .font(.title)
            Button("Increment") { count += 1 }
        }
        .padding()
    }
}

The @Observable macro introduced in Swift 5.9 replaced most uses of ObservableObject and @Published. It tracks which properties a view actually reads, so changing an unrelated property no longer triggers a redraw.


When Swift Is the Right Choice

  • Any Apple platform app: iOS, macOS, watchOS, tvOS and visionOS. There is no serious alternative if you want full platform capability and first day support for new OS features.
  • Cross platform mobile with shared logic: Swift for the iOS layer alongside Kotlin on Android is still the highest quality path, and the two languages are close enough that porting logic is quick.
  • Server side Swift: worth it when your team already writes Swift and you want one language across client and backend, with real performance and low memory use.
  • Command line tools: a single compiled binary with no runtime to install, and Swift Argument Parser makes the interface pleasant.
  • Performance sensitive libraries: value semantics and no tracing garbage collector give predictable latency, which matters for audio, graphics and real time work. If that is the draw rather than the Apple platforms, Rust solves the same problem with a stricter compiler and no runtime at all.

It is a weaker fit for quick data exploration, where Python remains far quicker to reach an answer, for teams with no Apple hardware, and for web frontends, where JavaScript and TypeScript remain the sensible defaults.


Learning Path That Actually Works

The order matters. Skipping ahead to SwiftUI before optionals and value semantics are comfortable is the most common way people get stuck.

  • Week one: syntax, optionals, collections and control flow. Write small command line programs, not apps.
  • Week two: structs, classes, enums with associated values, and error handling. Model something real and notice how enums remove impossible states.
  • Week three: protocols, extensions, generics and closures. This is where the language starts feeling expressive rather than strict.
  • Week four: SwiftUI and data flow, then async await once you need to fetch something.
  • Ongoing: ARC and capture lists, then strict concurrency. Both make far more sense once you have hit the bugs they prevent.

The canonical free resources are The Swift Programming Language, Apple's SwiftUI tutorials, and the API Design Guidelines, which explain why the standard library is named the way it is. Version control matters from day one, so keep a Git reference nearby, and if your app talks to a database the SQL you write against it is unchanged by the client language. Server side Swift deploys as a single binary, so a scratch Docker image is usually all the runtime it needs.


Common Mistakes

  • Force unwrapping to silence the compiler: Xcode offers ! as a fix-it, and taking it converts a build error into a crash on a user's device.
  • Retain cycles in stored closures: any closure you keep that mentions self needs [weak self]. A closure passed to map does not.
  • Reaching for a class out of habit: developers arriving from Java or Objective-C often make everything a class and lose the safety that value semantics provide for free.
  • Modelling state with several booleans: three flags describe eight states when only four exist. An enum with associated values makes the other four unrepresentable.
  • Ignoring the HTTP status code: URLSession does not treat a 500 as an error, so an unchecked response surfaces later as a confusing decoding failure.
  • Creating a DateFormatter inside a loop: it is genuinely expensive to construct. Reuse one, or use the newer formatted API.

Frequently Asked Questions

What is the difference between a struct and a class in Swift?

A struct is a value type, so it is copied on assignment and each copy is independent. A class is a reference type, so assignment shares one instance and changes are visible everywhere. Swift favours structs for models and data, and classes when you need identity, inheritance, deinit, or reference semantics such as a shared controller.

When should I use guard let instead of if let in Swift?

Use guard let when the unwrapped value is required for the rest of the function, because guard exits early and keeps the bound value in scope afterwards, which flattens nesting. Use if let when the value is optional to the logic and you only need it inside a short branch.

What does weak self do in a Swift closure?

Closures capture the values they reference strongly by default, so a closure stored on an object that also references that object creates a retain cycle and neither is deallocated. Writing [weak self] in the capture list makes the reference weak and optional, breaking the cycle. Use unowned only when self is guaranteed to outlive the closure.

Is Swift only for iOS development?

No. Swift builds apps for iOS, macOS, watchOS, tvOS and visionOS, but it is also open source and runs on Linux and Windows. Server frameworks such as Vapor and Hummingbird use it for backends, and Swift is used for command line tools, systems programming, and embedded targets through Embedded Swift.

What is the difference between async await and completion handlers?

Completion handlers pass a closure that runs later, which nests badly and makes error paths easy to miss. Async await lets you write the same asynchronous work as straight-line code with ordinary try and catch, and the compiler checks that every path returns or throws. Structured concurrency also cancels child tasks automatically when the parent is cancelled.

What does the Sendable protocol mean in Swift concurrency?

Sendable marks a type as safe to pass across concurrency boundaries such as between actors or into a Task. Value types made of Sendable parts conform automatically, while classes generally do not unless they are immutable or protect their own state. Under Swift 6 strict concurrency the compiler enforces this, turning a whole class of data races into build errors.