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.
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:
null mistake.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.)
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
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.
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.
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
// 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.
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.
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]
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 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
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?
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).
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.
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.
| 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 |
struct is copied on assignment; a class is shared. Mixing these up causes surprising bugs.! ("just make the optional go away") reintroduces the crashes optionals prevent. Use if let / guard let instead — reserve ! for cases you can prove are safe.switch must be exhaustive. Handle every case or add default.String.Index, not s[0]. This trips up everyone once.self in closures can create retain cycles in classes — learn [weak self] when you get to app development.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.
✅ 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.