Swift is the programming language from Apple for iPhone, Mac and server apps. It
is strict in the useful way: values cannot be nil unless their type says so, number
types never convert behind your back, and Swift 6 checks for data races before your
code runs. The reference below is grouped by what you are trying to do, and the
filter box searches all of it at once. Type optional and every way to unwrap one
comes to you, or type 6.0 to see what arrived in that release.
Every snippet is checked against Swift 6.4, the current release, in Swift 6
language mode. Anything that needs a version newer than 5.7 says so in the notes
column. Names like user and nums are placeholders for your own. You do not need
a Mac to follow along: the official Docker image runs a file in one line,
docker run --rm -v "$PWD":/app -w /app swift:6.4 swift main.swift, and the
Docker cheat sheet has the rest. Coming from
another language? The Ruby and
PHP cheat sheets are grouped the same way, so they
read side by side.
Searches the task, the command and the third column. Press / from anywhere on the page.
248 commands
Running Swift
| Task | Command | Notes |
|---|---|---|
| Check which version you have | swift --version | |
| Run a script | swift main.swift | Runs in Swift 5 language mode. Add -swift-version 6 to match a package |
| Open an interactive shell | swift repl | |
| Compile one file to a binary | swiftc main.swift -o hello | |
| Start a command-line app | swift package init --type executable | Leave out --type for a library |
| Build a package | swift build | |
| Build and run | swift run | |
| Build an optimised release | swift build -c release | |
| Run the tests | swift test | |
| Run the tests matching a name | swift test --filter GreeterTests | |
| Add a dependency | swift package add-dependency https://github.com/apple/swift-argument-parser --from 1.5.0 | Then add-target-dependency to use it in a target |
| Update dependencies within their ranges | swift package update | |
| Format every file in place | swift format --in-place --recursive Sources | swift-format ships with the toolchain from 6.0 |
Xcode includes Swift on a Mac. On Linux and Windows, or to run a version other than the one Xcode ships, swiftly from swift.org installs and switches between toolchains.
Constants and variables
| Task | Code | Notes |
|---|---|---|
| Print a line | print("Hello") | |
| Print several values | print(a, b, separator: ", ") | |
| Constant | let maxUsers = 100 | Cannot be reassigned. Use let unless the value changes |
| Variable | var count = 0 | |
| Give the type explicitly | let price: Double = 9.99 | Usually inferred. 0.5 is a Double, 1 is an Int |
| Declare now, assign once later | let label: String | Assign it in every branch before reading it |
| Several on one line | var x = 0, y = 0 | |
| Sized integer types | Int Int64 UInt8 | Int matches the platform word size, 64-bit almost everywhere |
| Readable big numbers | 1_000_000 | |
| Whole-number division | 7 / 2 | Gives 3. Double(7) / 2 gives 3.5 |
| Remainder | 7 % 2 | |
| Convert between number types | Double(count) Int(3.9) | Int(3.9) is 3, it truncates |
| String to number | Int("42") | Returns Int?, which is nil for junk |
| Tuple with named parts | let point = (x: 3, y: 4) | Then point.x |
| Unpack a tuple | let (width, height) = size | |
| Swap two variables | swap(&a, &b) | |
| Give a type a second name | typealias UserID = Int | |
| What type is it | type(of: value) | |
| Is it a type | value is String | |
| Cast, or nil if it fails | value as? String | as! crashes instead of returning nil |
| Comment | // one line /* several */ |
Swift never converts between number types for you. count + price is a compile error when one is an Int and the other a Double, so wrap one side: Double(count) + price.
Optionals
An optional is a value that might be missing. String? holds either a String or nil, and Swift will not let you use it as a String until you unwrap it.
| Task | Code | Notes |
|---|---|---|
| Declare an optional | var nickname: String? = nil | |
| Use it only if it has a value | if let nickname { print(nickname) } | Short for if let nickname = nickname |
| Unwrap or leave early | guard let user else { return } | user is unwrapped for the rest of the scope |
| Unwrap several at once | if let first, let last { } | |
| Unwrap and test together | if let age, age >= 18 { } | |
| Default when nil | let name = nickname ?? "Anonymous" | |
| Reach through optionals | user?.address?.city | The whole chain is nil if any link is |
| Call a method only if not nil | delegate?.didFinish() | |
| Transform the value if there is one | nickname.map { $0.uppercased() } | Still optional afterwards |
| Transform with a step that can fail | input.flatMap { Int($0) } | Avoids an Int?? |
| Parse a list, keeping what worked | ["1", "x", "3"].compactMap { Int($0) } | Gives [1, 3] |
| Is it nil | nickname == nil | |
| Force unwrap | url! | Crashes on nil. Only where nil means a bug |
| Set up after init, used as if not optional | var connection: Connection! | Crashes if read while still nil |
| Match on a missing value | if case nil = cache[key] { } |
Strings
| Task | Code | Notes |
|---|---|---|
| Interpolate | "Hello, \(name)" | Any expression works inside the brackets |
| Several lines | """ ... """ | The closing quotes set the indentation to strip |
| Backslashes kept as typed | #"C:\path\new"# | Interpolate with \#(name) |
| Join strings | first + " " + last | |
| Append | s += "!" s.append("!") | |
| Length in characters | s.count | "café".count is 4 |
| Length in bytes | s.utf8.count | "café".utf8.count is 5 |
| Is it empty | s.isEmpty | Faster than s.count == 0 |
| Change case | s.uppercased() s.lowercased() | capitalized needs import Foundation |
| Trim whitespace | s.trimmingCharacters(in: .whitespacesAndNewlines) | Needs import Foundation |
| Does it contain | s.contains("cat") | |
| Does it start or end with | url.hasPrefix("https") file.hasSuffix(".swift") | |
| Replace every match | s.replacing("cat", with: "dog") | replacingOccurrences(of:with:) in Foundation does the same |
| Split into parts | csv.split(separator: ",") | Gives [Substring] and drops empty parts |
| Split, keeping empty parts | csv.split(separator: ",", omittingEmptySubsequences: false) | |
| Join an array into a string | names.joined(separator: ", ") | |
| First or last character | s.first s.last | Character?, nil on an empty string |
| First or last n characters | s.prefix(5) s.suffix(3) | Substrings. Wrap in String() to keep one |
| Character at a position | s[s.index(s.startIndex, offsetBy: 2)] | There is no s[2]. Characters vary in size |
| Loop over characters | for character in s { } | |
| Reverse | String(s.reversed()) | |
| Repeat | String(repeating: "-", count: 20) | |
| Number to string | String(42) "\(price)" | |
| Format a decimal | String(format: "%.2f", price) | Needs import Foundation |
| Does it match a pattern | s.contains(/\d+/) | Regex literals, Swift 5.7+. In Swift 5 mode write #/\d+/# |
| Capture part of a match | if let match = s.firstMatch(of: /(\d{4})-(\d{2})/) { match.1 } | match.1 is the first group |
| Compare ignoring case | a.caseInsensitiveCompare(b) == .orderedSame | Needs import Foundation |
Collections: arrays
| Task | Code | Notes |
|---|---|---|
| Create an array | var nums = [1, 2, 3] | |
| Empty array of a type | var names: [String] = [] | |
| Filled with one value | Array(repeating: 0, count: 5) | |
| Read by position | nums[0] | Crashes if out of range |
| First or last, safely | nums.first nums.last | Optional, nil when empty |
| Is a position in range | nums.indices.contains(i) | |
| Append | nums.append(4) nums += [5, 6] | |
| Insert at a position | nums.insert(0, at: 0) | |
| Remove at a position | nums.remove(at: 0) | Returns the removed item |
| Remove the last item | nums.popLast() | nil when empty. removeLast() crashes instead |
| Remove every match | nums.removeAll { $0 < 0 } | |
| How many, and is it empty | nums.count nums.isEmpty | |
| Does it contain | nums.contains(3) | |
| Position of a value | nums.firstIndex(of: 3) | nil when missing |
| A slice | nums[1...2] nums.prefix(2) nums.dropFirst() | ArraySlice. Array(...) to keep it |
| Sorted copy | nums.sorted() nums.sorted(by: >) | sort() sorts in place |
| Sort by a field | users.sorted { $0.age < $1.age } | |
| Sort by two fields | users.sorted { ($0.last, $0.first) < ($1.last, $1.first) } | Tuples compare left to right |
| Transform every item | nums.map { $0 * 2 } | |
| Read one property from each | users.map(\.name) | A key path works as a function |
| Keep items that pass a test | nums.filter { $0.isMultiple(of: 2) } | |
| Fold into one value | nums.reduce(0, +) | There is no built-in sum() |
| Smallest and largest | nums.min() nums.max() | |
| Item with the largest field | users.max { $0.score < $1.score } | |
| First item that passes a test | users.first { $0.isAdmin } | first(where:) with a trailing closure |
| Do all or any pass | nums.allSatisfy { $0 > 0 } nums.contains { $0 > 3 } | |
| Count items that pass a test | nums.count { $0 > 2 } | Swift 6.0+ |
| Loop with the position | for (i, name) in names.enumerated() { } | |
| Pair up two arrays | zip(names, scores) | |
| Flatten nested arrays | nested.flatMap { $0 } | |
| Reversed, shuffled, random item | nums.reversed() nums.shuffled() nums.randomElement() | |
| A range as an array | Array(1...5) | 1..<5 leaves out the 5 |
Collections: dictionaries and sets
Dictionaries and sets are unordered. Looping over the same dictionary twice can give a different order, so sort first whenever the order matters.
| Task | Code | Notes |
|---|---|---|
| Create a dictionary | var ages = ["Ada": 36, "Alan": 41] | |
| Empty dictionary of a type | var cache: [String: Int] = [:] | |
| Read a value | ages["Ada"] | Int?, nil when the key is missing |
| Read with a default | ages["Bob", default: 0] | |
| Set a value | ages["Bob"] = 30 | |
| Remove a key | ages["Bob"] = nil | removeValue(forKey:) returns the old value |
| Count how often each value appears | counts[word, default: 0] += 1 | |
| Does a key exist | ages["Ada"] != nil | |
| Just the keys or values | ages.keys ages.values | |
| Loop over pairs | for (name, age) in ages { } | |
| Loop in key order | for (name, age) in ages.sorted(by: { $0.key < $1.key }) { } | |
| Change every value | ages.mapValues { $0 + 1 } | |
| Keep pairs that pass a test | ages.filter { $0.value > 30 } | |
| Merge, right side wins | defaults.merge(overrides) { _, new in new } | merging(_:uniquingKeysWith:) returns a copy |
| Group a list by a field | Dictionary(grouping: words, by: \.count) | |
| Index a list by a field | Dictionary(uniqueKeysWithValues: users.map { ($0.id, $0) }) | Crashes on a duplicate key |
| Create a set | var tags: Set = ["swift", "ios"] | |
| Add and check | tags.insert("server") tags.contains("ios") | |
| Remove duplicates from an array | Array(Set(nums)) | Loses the original order |
| Union, intersection, difference | a.union(b) a.intersection(b) a.subtracting(b) | |
| Is one set inside another | a.isSubset(of: b) |
Arrays, dictionaries, sets and strings are all value types. Assigning one to a new variable gives you an independent copy, and Swift only duplicates the storage when one of the copies is changed.
Control flow
| Task | Code | Notes |
|---|---|---|
| If, else if, else | if a { } else if b { } else { } | No brackets round the condition. Braces always |
| Pick one of two values | let label = count == 1 ? "item" : "items" | |
| If as an expression | let label = if count == 1 { "item" } else { "items" } | Swift 5.9+ |
| Leave early unless a condition holds | guard age >= 18 else { return } | The else must leave the scope |
| Match a value to a result | switch code { case 200, 201: ok() case 404: missing() default: fail() } | Must cover every case. No fallthrough |
| Match a range | case 0..<13: "child" | |
| Match with a condition | case let t where t < 0: | |
| Match a tuple | switch (x, y) { case (0, 0): ... case (0, _): ... } | _ matches anything |
| Switch as an expression | let text = switch code { case 200: "OK" default: "Error" } | Swift 5.9+ |
| Loop over a range | for i in 0..<5 { } | 0...5 includes the 5 |
| Loop in steps | for i in stride(from: 0, to: 100, by: 10) { } | through: includes the end |
| Loop backwards | for i in (0..<5).reversed() { } | |
| Loop over matching items only | for n in nums where n > 0 { } | |
| Repeat n times | for _ in 1...3 { } | |
| While loop | while !queue.isEmpty { } | |
| Run the body at least once | repeat { } while tries < 3 | |
| Skip or stop | continue break | |
| Break out of an outer loop | outer: for row in grid { for cell in row { break outer } } | |
| Match one enum case | if case .failure(let error) = result { } | |
| Run code when the scope ends | defer { file.close() } | Runs however the scope is left |
| Is a value in a range | (1...10).contains(n) |
Functions and closures
| Task | Code | Notes |
|---|---|---|
| Define a function | func add(_ a: Int, _ b: Int) -> Int { a + b } | One-expression bodies return implicitly |
| Argument label | func greet(person name: String) | Call it as greet(person: "Ada") |
| No argument label | func square(_ x: Int) -> Int | The _ removes it |
| Default parameter | func greet(_ name: String = "world") | |
| Any number of arguments | func sum(_ numbers: Int...) -> Int | numbers is an [Int] inside |
| Change the caller's variable | func double(_ n: inout Int) | Call it as double(&count) |
| Return two values | func bounds(_ values: [Int]) -> (min: Int, max: Int) | |
| Return nothing useful, silently | @discardableResult func save() -> Bool | No unused-result warning |
| Store a function | let operation: (Int, Int) -> Int = add | |
| Closure | let square = { (x: Int) -> Int in x * x } | |
| Shorthand parameters | nums.map { $0 * 2 } | |
| Trailing closure | nums.sorted { $0 > $1 } | The last closure argument moves outside the brackets |
| Pass an operator as a function | nums.sorted(by: >) | |
| Closure kept to run later | func onDone(_ handler: @escaping () -> Void) | Required when the closure outlives the call |
| Avoid a retain cycle in a class | { [weak self] in self?.reload() } | |
| Closure that keeps state | var total = 0; return { total += 1; return total } | It captures total, not a copy |
| Generic function | func firstItem<T>(_ items: [T]) -> T? | |
| Generic with a constraint | func largest<T: Comparable>(_ items: [T]) -> T? | |
| Function that never returns | func fail() -> Never { fatalError() } |
Structs and classes
Structs are values: assigning or passing one copies it. Classes are references: two variables can point at one object, so a change through either shows through both. Start with a struct and switch to a class when you need shared identity, inheritance or deinit.
| Task | Code | Notes |
|---|---|---|
| Define a struct | struct Point { var x: Double; var y: Double } | Gets a memberwise init for free |
| Create one | var p = Point(x: 1, y: 2) | |
| Method that changes a struct | mutating func move(by dx: Double) { x += dx } | |
| Computed property | var area: Double { width * height } | |
| Run code when a property changes | var score = 0 { didSet { print("now \(score)") } } | willSet runs before |
| Property built on first use | lazy var parser = Parser() | |
| Shared by the type, not each value | static let origin = Point(x: 0, y: 0) | |
| Readable outside, writable only inside | private(set) var count = 0 | |
| Init that can fail | init?(_ value: Int) { guard value >= 0 else { return nil } } | |
| Define a class | class User { var name: String; init(name: String) { self.name = name } } | No memberwise init. You write one |
| Inherit | class Admin: User | |
| Replace a parent method | override func describe() -> String | |
| Call the parent's init | super.init(name: name) | After setting the subclass's own properties |
| Stop anyone subclassing | final class Cache | |
| Same object | a === b | Classes only. == compares values |
| Clean up when freed | deinit { } | Classes only |
| Reference that does not keep it alive | weak var delegate: (any TableDelegate)? | Always optional, becomes nil when freed |
| Access levels | private fileprivate internal public | internal is the default: visible in the same module |
Enums
| Task | Code | Notes |
|---|---|---|
| Define an enum | enum Direction { case north, south, east, west } | |
| Use a case | var heading = Direction.north | Then heading = .south, the type is known |
| Switch over it | switch heading { case .north: ... default: ... } | Cover every case and you need no default |
| String raw values | enum Role: String { case admin, member } | Role.admin.rawValue is "admin" |
| Int raw values | enum Planet: Int { case mercury = 1, venus, earth } | venus is 2, earth is 3 |
| Create from a raw value | Role(rawValue: "admin") | Optional, nil for an unknown value |
| Case that carries data | enum Payment { case card(last4: String), cash } | |
| Read the data out | case .card(let last4): | |
| Check one case | if case .card = payment { } | |
| List every case | enum Size: CaseIterable | Then Size.allCases |
| Property on an enum | var label: String { switch self { case .small: "S" case .large: "L" } } | |
| Compare cases | heading == .north | Automatic unless a case carries data |
| Order cases by declaration | enum Medal: Comparable { case bronze, silver, gold } | |
| Enum that contains itself | indirect enum Tree { case leaf(Int), node(Tree, Tree) } |
Protocols and extensions
| Task | Code | Notes |
|---|---|---|
| Define a protocol | protocol Shape { var area: Double { get }; func draw() -> String } | |
| Conform to it | struct Square: Shape { var side: Double; var area: Double { side * side } } | |
| Default implementation | extension Shape { func describe() -> String { "Area \(area)" } } | |
| Add to an existing type | extension Int { var isEven: Bool { self % 2 == 0 } } | |
| Conform in an extension | extension User: CustomStringConvertible { var description: String { name } } | Controls what print shows |
| Get == and hashing for free | struct Tag: Hashable { let name: String } | Works when every property is Hashable |
| Make a struct sortable | static func < (lhs: Version, rhs: Version) -> Bool | Declare Comparable. min, max and sorted then work |
| Encode and decode JSON | struct Repo: Codable | |
| Protocol only classes can adopt | protocol TableDelegate: AnyObject | |
| Protocol with a placeholder type | protocol Container { associatedtype Item } | |
| List of mixed conforming types | let shapes: [any Shape] = [Square(side: 1), Circle(radius: 1)] | |
| Accept any one conforming type | func render(_ shape: some Shape) | Faster than any Shape |
| Return a type without naming it | func makeShape() -> some Shape | |
| Check the concrete type | if let square = shape as? Square { } | |
| Combine protocols | typealias Model = Codable & Hashable |
Error handling
| Task | Code | Notes |
|---|---|---|
| Define errors | enum LoginError: Error { case badPassword, locked(minutes: Int) } | |
| Function that can throw | func login(_ password: String) throws -> User | |
| Throw | throw LoginError.badPassword | |
| Call and handle | do { let user = try login(pw) } catch { print(error) } | error is bound automatically |
| Catch one case | catch LoginError.locked(let minutes) { } | |
| Catch by type | catch let error as LoginError { } | |
| Nil instead of an error | let user = try? login(pw) | |
| Crash instead of an error | let config = try! loadBundledConfig() | Only where failure means a bug |
| Pass the error on to your caller | func signIn() throws { let user = try login(pw) } | |
| Throw only one error type | func parse(_ s: String) throws(ParseError) -> Int | Typed throws, Swift 6.0+ |
| Error as a value | let result = Result { try login(pw) } | Then result.get() to throw it again |
| Readable error text | extension LoginError: CustomStringConvertible | LocalizedError in Foundation for UI messages |
| Check an assumption | precondition(index >= 0, "negative index") | Also checked in release builds. assert is debug only |
| Code that should be unreachable | fatalError("unreachable") |
Every call that can throw is marked with try, so you can see where control might leave a function. Swift errors are ordinary values, not exceptions with a stack unwind, and they cost about as much as a normal return.
Async and await
| Task | Code | Notes |
|---|---|---|
| Async function | func fetchUser(id: Int) async throws -> User | |
| Call it | let user = try await fetchUser(id: 1) | try before await |
| Run two calls at once | async let a = fetchUser(id: 1) | Then let (x, y) = try await (a, b) |
| Run a list of calls at once | try await withThrowingTaskGroup(of: User.self) { group in } | group.addTask for each, then for try await |
| Start async work from sync code | Task { await refresh() } | |
| Get a task's result | let user = try await task.value | |
| Pause | try await Task.sleep(for: .seconds(1)) | |
| Cancel a task | task.cancel() | |
| Stop if cancelled | try Task.checkCancellation() | Or read Task.isCancelled |
| Protect shared mutable state | actor Counter { var value = 0; func increment() { value += 1 } } | One caller at a time inside |
| Call into an actor | await counter.increment() | |
| Run on the main thread | @MainActor func updateUI() | Where UI code lives |
| Run off the caller's actor | @concurrent func decode() async | Swift 6.2+. For heavy work |
| Safe to share between tasks | struct Message: Sendable | |
| Produce values over time | let (stream, continuation) = AsyncStream.makeStream(of: Int.self) | continuation.yield(1), then finish() |
| Consume values over time | for await value in stream { } | |
| Wrap a callback API | try await withCheckedThrowingContinuation { continuation in } | Resume it exactly once |
| Await in cleanup code | defer { await resource.close() } | Swift 6.4+ |
Swift 6 language mode checks for data races at compile time. New packages also turn on approachable concurrency, which keeps a nonisolated async function on its caller's actor unless it is marked @concurrent, so most code runs where you expect and only the slow parts move off.
A model, start to finish
Most of the reference above in one place: an enum with raw values and a computed
property, a struct with two initialisers, Comparable, CustomStringConvertible
and a method with typed throws.
enum Currency: String, CaseIterable {
case gbp, usd
var symbol: String {
switch self {
case .gbp: "£"
case .usd: "$"
}
}
}
enum MoneyError: Error {
case currencyMismatch(Currency, Currency)
}
struct Money: Comparable, CustomStringConvertible {
let pence: Int
let currency: Currency
init(pence: Int, currency: Currency = .gbp) {
self.pence = pence
self.currency = currency
}
init(pounds: Double, currency: Currency = .gbp) {
self.init(pence: Int((pounds * 100).rounded()), currency: currency)
}
var description: String {
let sign = pence < 0 ? "-" : ""
let whole = abs(pence) / 100
let fraction = abs(pence) % 100
let padded = fraction < 10 ? "0\(fraction)" : "\(fraction)"
return "\(sign)\(currency.symbol)\(whole).\(padded)"
}
func adding(_ other: Money) throws(MoneyError) -> Money {
guard other.currency == currency else {
throw .currencyMismatch(currency, other.currency)
}
return Money(pence: pence + other.pence, currency: currency)
}
static func < (lhs: Money, rhs: Money) -> Bool {
lhs.pence < rhs.pence
}
}
let price = Money(pence: 1999)
let total = try price.adding(Money(pounds: 5))
print(total) // £24.99
print([price, Money(pence: 500)].max()!) // £19.99
do {
_ = try price.adding(Money(pence: 100, currency: .usd))
} catch .currencyMismatch(let a, let b) {
print("Cannot add \(a) to \(b)") // Cannot add gbp to usd
}Defining < is enough for Comparable to give you >, <=, >=, min, max
and sorted, and == comes for free because every stored property is already
Equatable. Money is held in pence as an Int on purpose: a Double cannot hold
most decimal fractions exactly, so 0.1 + 0.2 == 0.3 is false in Swift, as it is in
almost every language. Because adding throws only MoneyError, the catch can
match .currencyMismatch directly and needs no catch-all.
Structs copy, classes share
The difference between value and reference types is the one to get into your fingers early, because the code looks identical until it behaves differently.
struct Settings {
var theme = "light"
}
final class Session {
var userName: String
init(userName: String) { self.userName = userName }
}
var original = Settings()
var copy = original
copy.theme = "dark"
print(original.theme) // light: the struct was copied
let session = Session(userName: "Ada")
let sameSession = session
sameSession.userName = "Grace"
print(session.userName) // Grace: both names point at one object
print(session === sameSession) // trueNotice that sameSession is a let and its property still changed. For a class,
let fixes which object the name refers to, not what is inside the object. For a
struct, let freezes the whole value.
Decoding JSON with Codable
Declare Codable and the compiler writes the encoding and decoding. A CodingKeys
enum maps Swift names to the keys in the JSON, and an optional property accepts
null or a missing key.
import Foundation
struct Repo: Codable {
let id: Int
let fullName: String
let stars: Int
let description: String?
enum CodingKeys: String, CodingKey {
case id
case fullName = "full_name"
case stars = "stargazers_count"
case description
}
}
let json = """
{ "id": 1, "full_name": "swiftlang/swift", "stargazers_count": 69000, "description": null }
"""
do {
let repo = try JSONDecoder().decode(Repo.self, from: Data(json.utf8))
print(repo.fullName, repo.stars) // swiftlang/swift 69000
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
let data = try encoder.encode(repo)
print(String(decoding: data, as: UTF8.self))
} catch {
print("Could not decode: \(error)")
}When every key is snake_case, decoder.keyDecodingStrategy = .convertFromSnakeCase
does the mapping for you and the CodingKeys enum can go. If decoding fails, print
the error rather than error.localizedDescription: the full DecodingError names
the key and the path to it.
Concurrent work with a task group and an actor
A task group runs one child task per item and collects the results as they finish. The actor keeps the cache safe while several of those tasks read and write it.
struct Profile: Sendable {
let id: Int
let name: String
}
enum FetchError: Error {
case notFound(id: Int)
}
actor ProfileCache {
private var profiles: [Int: Profile] = [:]
func profile(for id: Int) -> Profile? { profiles[id] }
func store(_ profile: Profile) { profiles[profile.id] = profile }
}
func fetchProfile(id: Int) async throws -> Profile {
try await Task.sleep(for: .milliseconds(100)) // stands in for a network call
guard id > 0 else { throw FetchError.notFound(id: id) }
return Profile(id: id, name: "User \(id)")
}
func loadProfiles(ids: [Int], cache: ProfileCache) async throws -> [Profile] {
try await withThrowingTaskGroup(of: Profile.self) { group in
for id in ids {
group.addTask {
if let cached = await cache.profile(for: id) { return cached }
let profile = try await fetchProfile(id: id)
await cache.store(profile)
return profile
}
}
var profiles: [Profile] = []
for try await profile in group {
profiles.append(profile)
}
return profiles.sorted { $0.id < $1.id }
}
}
let cache = ProfileCache()
let profiles = try await loadProfiles(ids: [1, 2, 3], cache: cache)
print(profiles.map(\.name)) // ["User 1", "User 2", "User 3"], in about 0.1s, not 0.3s
do {
_ = try await loadProfiles(ids: [1, -1], cache: cache)
} catch FetchError.notFound(let id) {
print("No profile \(id)") // No profile -1
}Results arrive in the order the tasks finish, not the order they started, which is
why the list is sorted before it is returned. When one child task throws, the group
cancels the rest and the error comes out of loadProfiles. Every call into the
actor is an await, because the caller may have to wait its turn.
A package, start to finish
swift package init --type executable writes a Package.swift like this one, and
swift package add-dependency fills in the dependencies. A library target holds the
logic, so the tests can import it, and a small executable target wraps it.
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "Greeter",
platforms: [.macOS(.v15)],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
],
targets: [
.target(name: "Greeter"),
.executableTarget(
name: "greet",
dependencies: [
"Greeter",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.testTarget(name: "GreeterTests", dependencies: ["Greeter"]),
]
)Tests use Swift Testing, which ships with the toolchain. swift test finds every
@Test function, and #expect shows both sides of a failed comparison.
// Tests/GreeterTests/GreeterTests.swift
import Testing
@testable import Greeter
@Test func greetsByName() {
#expect(greeting(for: "Ada") == "Hello, Ada!")
}
@Test(arguments: ["", "Grace"])
func neverEmpty(name: String) {
#expect(!greeting(for: name).isEmpty)
}The first line of Package.swift is not a comment you can delete: it sets the
oldest toolchain that can build the package and, from 6.0, turns on Swift 6
language mode. Commit Package.resolved for an app so every build gets the same
dependency versions, and the Git cheat sheet covers
the rest of that workflow.
Gotchas
The mistakes almost everyone makes in their first month of Swift.
| Looks right | What actually happens | Do this instead |
|---|---|---|
let total = count + price with an Int and a Double | Compile error: no implicit number conversion | Double(count) + price |
name[0] on a String | Compile error: strings are not indexed by Int | name.first, name.prefix(1), or Array(name)[0] |
nums[5] on a shorter array | Crashes at runtime | nums.indices.contains(5) first, or nums.first |
let user = users.first then user.name | Compile error: user is optional | if let user, guard let user, or user?.name |
print(nickname) on a String? | Prints Optional("kit") | Unwrap first, or nickname ?? "" |
for (k, v) in dict expecting insertion order | Order is arbitrary and can change between runs | dict.sorted(by: { $0.key < $1.key }) |
var copy = session on a class | Both names share one object | Use a struct, or write your own copy method |
s.contains(/\d+/) in swift file.swift | Syntax errors, because scripts default to Swift 5 mode | swift -swift-version 6 file.swift, or #/\d+/# |
self.onUpdate = { self.reload() } in a class | A retain cycle: neither object is ever freed | { [weak self] in self?.reload() } |
Int("3.5") | nil, not 3 | Double("3.5").map { Int($0) } |
Swift's sort and sorted are guaranteed to be stable: items that compare equal
keep their original order, so sorting by surname after sorting by first name gives
the order you expect. Under the hood it is an adaptive merge sort, the same family
as merge sort, unlike
quick sort, which makes no such
promise. Both are on the site as step-through visualisations.
Common questions
Which version of Swift does this cheat sheet cover?
Swift 6.4, the current release, which came out in September 2026, in Swift 6 language mode. Most of the page also works on Swift 5.9 and later, and anything newer than 5.7 says so in the notes column, for example count(where:) and typed throws need 6.0, @concurrent needs 6.2 and await inside defer needs 6.4. Run swift --version to see which version you have.
What is the difference between let and var in Swift?
let declares a constant, which cannot be reassigned after its first value. var declares a variable, which can. For a struct, let also freezes every property inside it, while for a class let only fixes which object the constant points at, and the object's var properties can still change. The compiler warns when a var is never changed, so start with let and switch when you need to.
Should I use a struct or a class?
Use a struct by default. Structs are copied when assigned or passed, so no other part of the program can change your copy, and they are cheaper to create. Reach for a class when you need one shared object that several parts of the program see change, when you need inheritance, or when you need deinit to run cleanup when the object goes away.
What does the question mark and exclamation mark mean in Swift?
A question mark after a type, as in String?, makes it optional: it may hold a value or nil. After a value, as in user?.name, it is optional chaining, which gives nil instead of crashing when user is nil. An exclamation mark force unwraps an optional, as in url!, and crashes the program if the value is nil, so keep it for values that can only be nil because of a bug.
What is the difference between if let and guard let?
Both unwrap an optional. if let makes the unwrapped value available only inside its braces, which suits doing something extra when a value is there. guard let makes it available for the rest of the function and requires its else branch to leave, with return, throw, break or continue, which suits checking requirements at the top of a function without nesting the rest of it.
What is the difference between any and some?
some Shape means one specific type that conforms to Shape, chosen by the caller or the function, and fixed for that call. The compiler knows the real type, so it is fast. any Shape is a box that can hold any conforming type, and different boxes can hold different types, which is what you need for a mixed array such as [any Shape]. Prefer some unless you need the mixing.
Do I need a Mac to learn Swift?
No. Swift is open source and runs on Linux and Windows as well as macOS. Every snippet on this page was checked with the official swift Docker image on Linux. You need a Mac and Xcode to build apps for iPhone, iPad and Mac, and some Apple frameworks such as SwiftUI and UIKit only exist there, but the language itself, Foundation and Swift packages work everywhere.
Why can't I index a Swift string with a number?
Because Swift counts characters as people see them, and one character can be several bytes, as with accented letters and emoji. Finding the tenth character means walking the string from the start, so Swift makes that cost visible with String.Index instead of pretending it is instant. Use prefix, suffix, first and last where you can, and convert with Array(s) if you really need random access.
