article thumbnail

Swift Crash Course

The Language Behind Every iPhone — and Now Much More

12 min read
#programming, #swift, #friday1

Every iPhone, iPad, Mac, Apple Watch, and Vision Pro app written in the last decade is powered by Swift. Apple unveiled it in 2014 to replace Objective-C — a language whose square-bracket syntax and manual memory dance scared off a generation of developers. Swift kept the power and threw out the pain: it's fast (compiled to native code via LLVM), safe (the compiler refuses to let nil blow up your app), and surprisingly pleasant to read. And it's no longer trapped in Apple's walled garden — Swift runs on Linux and Windows, powers server backends, and has been fully open source since 2015. Here's your crash course.

Why Swift Won

Swift was designed by Chris Lattner — who had already built the LLVM compiler infrastructure and the Clang C/C++ compiler — with a few non-negotiables:

The result reads almost like Python but runs like C, and the compiler catches an astonishing number of bugs before your code ever runs.

(Lattner left Apple in 2017. He now leads Modular, where he created Mojo — a Python-family language that borrows liberally from Swift's design.)

Hello, Swift

print("Hello, World!")

That's the entire program — no main, no boilerplate, no imports for basic I/O. You can run Swift as a script, in a REPL (swift), in an Xcode Playground, or compile it:

swift hello.swift        # run directly
swiftc hello.swift       # compile to a binary ./hello

Variables: let vs var

Swift makes you declare intent. let is a constant, var is mutable — and you're nudged toward let everywhere.

let name = "Ada"          // constant — cannot change
var age = 36              // variable — can change
age += 1

let pi: Double = 3.14159  // explicit type
let count = 0            // inferred as Int

The compiler warns you when a var never changes, quietly teaching you to prefer immutability.

Optionals: The Feature That Defines Swift

This is Swift's signature idea. A normal variable cannot be nil. If a value might be absent, its type must be marked optional with ?. The compiler then forces you to handle the empty case.

var username: String = "ada"    // can NEVER be nil
var nickname: String? = nil     // optional — may hold a String or nil

// You can't use an optional directly — you must unwrap it.

// 1. if-let: safely unwrap
if let nick = nickname {
    print("Hi \(nick)")
} else {
    print("No nickname set")
}

// 2. guard-let: unwrap or bail early (the idiomatic style)
func greet(_ name: String?) {
    guard let name = name else {
        print("Nobody to greet")
        return
    }
    print("Hello, \(name)")
}

// 3. nil-coalescing: provide a default
let display = nickname ?? "Anonymous"

// 4. optional chaining: call through safely
let length = nickname?.count   // Int? — nil if nickname is nil

Because the compiler enforces this, "unexpectedly found nil" crashes — the number-one cause of app crashes in the Objective-C era — largely disappear.

Strings and Interpolation

let name = "Ada"
let age = 36

let msg = "\(name) is \(age) years old"          // interpolation
let math = "Next year: \(age + 1)"               // expressions work too

// Multi-line strings
let poem = """
    Roses are red,
    Swift is too.
    """

name.uppercased()          // "ADA"
name.count                 // 3
name.hasPrefix("A")        // true

Collections

// Array
var nums = [1, 2, 3]
nums.append(4)
nums.count                 // 4

// Dictionary
var ages = ["Ada": 36, "Alan": 41]
ages["Grace"] = 45
let a = ages["Ada"]        // Int? — optional, because the key might not exist

// Set
let unique: Set = [1, 2, 2, 3]   // {1, 2, 3}

Swift's collections are strongly typed — [Int], [String: Int] — and come with a rich functional toolkit:

let doubled = nums.map { $0 * 2 }           // [2, 4, 6, 8]
let evens = nums.filter { $0 % 2 == 0 }     // [2, 4]
let total = nums.reduce(0, +)               // 10

$0 is shorthand for the first closure argument — Swift's compact closure syntax.

Control Flow and Pattern Matching

Swift's switch is far more powerful than C's — it matches ranges, tuples, and can bind values.

let score = 85

switch score {
case 90...100:
    print("A")
case 80..<90:
    print("B")
case let n where n < 60:
    print("Fail (\(n))")
default:
    print("C or below")
}

// Loops
for i in 1...5 { print(i) }          // 1 through 5 inclusive
for name in ["Ada", "Alan"] { print(name) }

Note: Swift's switch has no fall-through by default and must be exhaustive — the compiler checks you've covered every case.

Functions and Closures

Swift functions use argument labels, which make call sites read like sentences.

func greet(person name: String, from city: String) -> String {
    return "Hello \(name) from \(city)"
}

greet(person: "Ada", from: "London")

// Default values and multiple returns via tuples
func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
    guard !nums.isEmpty else { return nil }
    return (nums.min()!, nums.max()!)
}

if let result = minMax([3, 1, 4, 1, 5]) {
    print(result.min, result.max)   // 1 5
}

Closures are first-class and used everywhere:

let add = { (a: Int, b: Int) in a + b }
add(2, 3)   // 5

// Trailing closure syntax — the last closure argument moves outside the parens
[3, 1, 2].sorted { $0 < $1 }   // [1, 2, 3]

Structs, Classes, and Enums

Swift gives you three ways to model data, and it steers you toward structs.

// Struct — a value type (copied on assignment). The default choice.
struct Point {
    var x: Int
    var y: Int
    func distance() -> Double {
        Double(x*x + y*y).squareRoot()
    }
}

// Class — a reference type (shared). Use when you need identity or inheritance.
class Animal {
    var name: String
    init(name: String) { self.name = name }
    func speak() -> String { "..." }
}

class Dog: Animal {
    override func speak() -> String { "Woof" }
}

The struct-vs-class distinction (value vs reference semantics) is the concept to internalize in Swift. Structs are copied, so they're predictable and thread-friendly; classes are shared by reference.

Enums in Swift are unusually powerful — they can carry associated values:

enum Result {
    case success(data: String)
    case failure(error: String)
}

let outcome = Result.success(data: "OK")

switch outcome {
case .success(let data):
    print("Got \(data)")
case .failure(let error):
    print("Error: \(error)")
}

Protocols: Swift's Interfaces

Protocols define a contract of methods and properties. Types then conform to them — and unlike classes, structs and enums can conform too.

protocol Shape {
    var area: Double { get }
}

struct Circle: Shape {
    let radius: Double
    var area: Double { .pi * radius * radius }
}

struct Square: Shape {
    let side: Double
    var area: Double { side * side }
}

let shapes: [Shape] = [Circle(radius: 2), Square(side: 3)]
let totalArea = shapes.reduce(0) { $0 + $1.area }

Swift's design leans so heavily on protocols that the philosophy has a name: protocol-oriented programming — composing behavior from small protocols instead of deep inheritance trees. You can even extend protocols with default implementations and add methods to existing types:

extension String {
    var isValidEmail: Bool { contains("@") && contains(".") }
}

"me@example.com".isValidEmail   // true

Error Handling

Swift uses throws / try / catch — explicit but not as verbose as it looks.

enum FileError: Error {
    case notFound
}

func readConfig(_ path: String) throws -> String {
    guard path == "config.json" else { throw FileError.notFound }
    return "{...}"
}

do {
    let config = try readConfig("missing.json")
    print(config)
} catch FileError.notFound {
    print("File not found")
} catch {
    print("Other error: \(error)")
}

// try? turns a throwing call into an optional
let config = try? readConfig("config.json")   // String?

Modern Concurrency: async/await

Since Swift 5.5, concurrency is built into the language with async/await and actors that make data races a compile-time error.

func fetchUser(id: Int) async throws -> String {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(decoding: data, as: UTF8.self)
}

// Call it
Task {
    let user = try await fetchUser(id: 1)
    print(user)
}

// Run several tasks concurrently
async let a = fetchUser(id: 1)
async let b = fetchUser(id: 2)
let both = try await [a, b]

actor types serialize access to their state, so you get thread-safe objects without manual locks — one of the safest concurrency models in any mainstream language.

Swift 6, released in September 2024, went further still: its opt-in language mode (swiftLanguageModes: [.v6] in Package.swift) promotes data races from runtime hazards to compile-time errors across an entire module. You migrate one module at a time, and once it builds, the concurrency is provably race-free. The current release is Swift 6.3 (March 2026).

Beyond the iPhone

Swift is no longer Apple-only:

swift package init --type executable
swift build
swift run
swift test

Since mid-2024 the compiler, standard library, and core packages live in a dedicated swiftlang GitHub organization, separate from Apple's — a nod to how far Swift has spread beyond Apple's platforms.

Working with Databases

Swift has solid database support everywhere it runs.

On Apple platforms, the modern choice is SwiftData (2023) — a declarative persistence framework that stores your model objects in SQLite behind the scenes:

import SwiftData

@Model
class Book {
    var title: String
    var rating: Int
    init(title: String, rating: Int) {
        self.title = title
        self.rating = rating
    }
}

context.insert(Book(title: "Dune", rating: 5))
let favorites = try context.fetch(
    FetchDescriptor<Book>(predicate: #Predicate { $0.rating >= 4 })
)

Its predecessor Core Data is still fully supported and widely used.

For direct SQL on any platform, GRDB.swift is the most popular SQLite toolkit, with SQLite.swift a lighter type-safe wrapper:

import GRDB

let dbQueue = try DatabaseQueue(path: "app.sqlite")

try dbQueue.write { db in
    try db.execute(sql: "INSERT INTO player (name, score) VALUES (?, ?)",
                   arguments: ["Ada", 100])
}

let top = try dbQueue.read { db in
    try Row.fetchAll(db, sql: "SELECT * FROM player ORDER BY score DESC")
}

On the server, the Swift Server workgroup maintains native async drivers — PostgresNIO, MySQLNIO, SQLiteNIO, MongoKitten — and Vapor's ORM Fluent sits on top with a records-as-Swift-types API:

let users = try await User.query(on: req.db)
    .filter(\.$name == "Ada")
    .all()

Every one of these speaks async/await, so database calls drop straight into Swift's concurrency model.

Quick Reference: Coming From Another Language

You know... In Swift it's...
null / None nil, but only on Optional (T?) types
final class value type struct (value semantics, the default)
interface / Protocol protocol
try/catch do { try ... } catch { }
async/await async/await (with actors for safety)
List / array [T]
Map / dict [K: V]
lambda / arrow fn closure { $0 * 2 }
extends : for both inheritance and protocol conformance
const / final let

Gotchas That Trip Up Newcomers

Where Swift Shines (and Where It Doesn't)

Great for: any Apple-platform app (this is still the main event), plus server backends, CLIs, and increasingly systems and embedded work. Its safety guarantees make it excellent for code that must not crash.

Less ideal for: cross-platform GUI apps outside Apple's ecosystem (tooling is still Apple-centric), quick throwaway data science (Python dominates), and Windows-first development, where support exists but is less mature.

Getting Started Checklist

✅ On a Mac? Install Xcode from the App Store — everything's included ✅ On Linux/Windows? Get the toolchain from swift.org ✅ Try the REPL: just type swift ✅ Experiment in an Xcode Playground for instant feedback ✅ Start a project with swift package init ✅ Work through The Swift Programming Language — the free, excellent official book ✅ Prefer let over var, and guard let over force-unwrapping

Conclusion: Swift proves that safe and pleasant aren't opposites. Optionals make nil crashes a compile-time conversation instead of a 2 a.m. pager alert; structs and value semantics make code easier to reason about; and async/await with actors tames concurrency that would be treacherous elsewhere. Whether you're building the next great iPhone app or a Linux web service, Swift gives you C-class speed with guardrails that actually help. Install the toolchain, open a Playground, and write something small — the compiler will teach you the rest.

Enjoyed this article? Share it with someone who'd love it too.

Most covered topics