</>
CodeLearn Pro
Start Learning Free โ†’

Swift โ€” Complete Beginner Tutorial

6 modules ยท 17 examples ยท Click โ–ถ Run on any example to see the output

โœฆ Beginner
โ† Overview

๐Ÿ’ก How to use this page

Read the explanation, study the code, then hit โ–ถ Run to see the output.

๐Ÿ“ฆ
Module 01

Variables & Types

Swift is a statically typed language โ€” every value has a specific type. Use let to declare a constant (value never changes) and var for a variable (value can change). Swift's type inference means you usually do not have to write the type โ€” Swift figures it out from the value you assign.

let and var โ€” Constants and Variables

let creates a constant โ€” once set, it cannot be changed. var creates a variable that can be reassigned. In Swift, prefer let everywhere by default and only use var when you know the value will change. Swift will warn you if you use var when let would work.

main.swiftSwift
import Foundation

// let โ€” constant, cannot be changed
let name = "Alice"           // String (inferred)
let age  = 25                // Int (inferred)
let pi   = 3.14159           // Double (inferred)
let isStudent = true         // Bool (inferred)

// var โ€” variable, CAN be changed
var score = 80
var level = 1

print("Name: \(name)")
print("Age: \(age)")
print("Score: \(score)")

// Change the variable
score = 95
level += 1
print("Updated score: \(score)")
print("Level: \(level)")

// Explicit type annotation (optional but clear)
let city: String  = "Cape Town"
var lives: Int    = 3
var health: Float = 100.0

print("\(name) lives in \(city)")
print("Lives: \(lives), Health: \(health)")
โ–ถ Swift Playground
๐Ÿ’ก

Swift strongly encourages using let over var. If you try to change a let constant, Xcode will immediately show a red error. This is intentional โ€” constants make your code safer and easier to reason about.

Strings & String Interpolation

Swift strings are powerful, Unicode-safe, and use backslash-parenthesis \() to embed any expression directly inside a string โ€” called string interpolation. No need for + to join strings (though that works too).

main.swiftSwift
import Foundation

let firstName = "Alice"
let lastName  = "Smith"
let score     = 92
let grade     = "A"

// String interpolation โ€” embed values with \()
let fullName = "\(firstName) \(lastName)"
print(fullName)
print("Score: \(score)% โ€” Grade: \(grade)")
print("Next year Alice will be \(25 + 1) years old")

// You can put any expression inside \()
let passing = score >= 50
print("Is passing: \(passing)")
print("Double score: \(score * 2)")

// Multiline string (triple quotes)
let message = """
    Hello, \(firstName)!
    You scored \(score)% on the test.
    That is a grade \(grade).
    Well done!
    """
print(message)

// Useful String methods
let greeting = "  Hello, Swift!  "
print(greeting.trimmingCharacters(in: .whitespaces))
print(greeting.lowercased())
print(greeting.uppercased())
print("Contains Swift: \(greeting.contains("Swift"))")
print("Character count: \(greeting.count)")
โ–ถ Swift Playground
๐Ÿ’ก

String interpolation \() can contain any Swift expression โ€” not just variable names. \(score * 2), \(name.uppercased()), or even \(score > 80 ? "passed" : "failed") all work perfectly.

Type Conversion & Basic Maths

Swift never automatically converts between types โ€” you must do it explicitly. You cannot add an Int and a Double directly. Convert with Int(), Double(), String() etc. This strictness prevents an entire class of bugs that plague other languages.

main.swiftSwift
import Foundation

let intNumber   = 42
let doubleNum   = 3.14
let numberText  = "100"

// Swift DOES NOT auto-convert โ€” you must be explicit
// let sum = intNumber + doubleNum  โ† COMPILE ERROR!

// Correct: convert manually
let sum = Double(intNumber) + doubleNum
print("Sum: \(sum)")

// String to Int โ€” returns optional (might fail!)
if let parsed = Int(numberText) {
    print("Parsed: \(parsed)")
    print("Doubled: \(parsed * 2)")
}

// Int to String
let asText = String(intNumber)
print("As text: \(asText)")
print("Type check: \(type(of: asText))")

// Maths operations
let a = 17
let b = 5
print("\(a) + \(b) = \(a + b)")
print("\(a) - \(b) = \(a - b)")
print("\(a) * \(b) = \(a * b)")
print("\(a) / \(b) = \(a / b)")  // Integer division!
print("\(a) % \(b) = \(a % b)")  // Remainder

// For decimal division, use Double
let result = Double(a) / Double(b)
print("\(a) / \(b) = \(result)")
โ–ถ Swift Playground
๐Ÿ’ก

17 / 5 = 3 in Swift โ€” integer division drops the decimal. This surprises many beginners. To get 3.4, at least one operand must be a Double: Double(17) / Double(5). The % operator gives the remainder: 17 % 5 = 2.

โ“
Module 02

Optionals

Optionals are one of Swift's most important features and what makes it so much safer than languages like C or Objective-C. An optional is a variable that might have a value, or might have nothing (nil). The ? after a type makes it optional. You must explicitly unwrap it before using the value โ€” Swift will not let you accidentally use nil.

What is an Optional?

Think of an optional like a gift box โ€” it either contains something, or it is empty. String? means "a String or nil". You cannot use the value directly โ€” you must open the box first. The safest way is if let, which only runs if the box has something inside.

main.swiftSwift
import Foundation

// Regular variable โ€” MUST have a value
let name: String = "Alice"

// Optional variable โ€” might be nil (no value)
var nickname: String? = "Ali"
var middleName: String? = nil   // explicitly nothing

print("Name: \(name)")
print("Nickname: \(nickname)")   // prints Optional("Ali")
print("Middle: \(middleName)")   // prints nil

// Safe unwrap with if let
if let nick = nickname {
    // Inside here, nick is a plain String (not optional)
    print("Nickname is: \(nick)")
    print("Uppercased: \(nick.uppercased())")
} else {
    print("No nickname")
}

// Check nil directly
if middleName == nil {
    print("No middle name")
}

// Nil coalescing ?? โ€” provide a default if nil
let displayNick = nickname ?? "No nickname"
let displayMiddle = middleName ?? "N/A"
print("Nick display: \(displayNick)")
print("Middle display: \(displayMiddle)")

// Optional chaining โ€” safely call methods on optional
let shout = nickname?.uppercased()
print("Shout: \(shout)")  // Optional("ALI")
โ–ถ Swift Playground
๐Ÿ’ก

The ?? (nil coalescing) operator is incredibly useful โ€” it says "use this value if it exists, otherwise use this default". let display = name ?? "Unknown" is cleaner than writing a full if/else every time.

guard let โ€” Early Exit Pattern

guard let is the professional Swift way to handle optionals. Instead of nesting your code deeper and deeper with if let, guard let exits early if the value is nil. The unwrapped value is then available for the rest of the function โ€” much cleaner for real code.

main.swiftSwift
import Foundation

// Simulating a database lookup
func findUser(id: Int) -> String? {
    let users = [1: "Alice", 2: "Bob", 3: "Charlie"]
    return users[id]  // Returns String? (might not exist)
}

func processUser(id: Int) {
    // guard let exits early if nil โ€” keeps the happy path unindented
    guard let user = findUser(id: id) else {
        print("User \(id) not found โ€” stopping")
        return  // must exit the function
    }

    // user is now a plain String โ€” guaranteed to exist
    print("Processing: \(user)")
    print("Name length: \(user.count)")
    print("Upper: \(user.uppercased())")
}

processUser(id: 1)
print("---")
processUser(id: 99)  // not found
print("---")
processUser(id: 2)

// Multiple optional unwrapping
func describe(name: String?, age: Int?, city: String?) -> String {
    guard let name = name,
          let age  = age,
          let city = city else {
        return "Incomplete profile"
    }
    return "\(name), age \(age), from \(city)"
}

print("---")
print(describe(name: "Alice", age: 25, city: "Cape Town"))
print(describe(name: "Bob",   age: nil, city: "Durban"))
โ–ถ Swift Playground
๐Ÿ’ก

Professional Swift code uses guard let far more than if let for validation. The rule of thumb: use if let when you want to DO something with the value. Use guard let when the value MUST exist for the function to continue.

โš–๏ธ
Module 03

Control Flow

Swift's control flow is similar to other languages but with some powerful extras. The switch statement in Swift is far more powerful than in C or Java โ€” it supports ranges, tuples, and pattern matching. The for-in loop works cleanly over any collection.

if / else and switch

Swift if statements work like most languages. The switch statement in Swift must be exhaustive โ€” every possible value must be covered, which prevents bugs. No break keyword needed โ€” cases do not fall through by default. Use ranges and where clauses for powerful matching.

main.swiftSwift
import Foundation

let score = 78

// if / else if / else
if score >= 90 {
    print("Grade: A โ€” Outstanding!")
} else if score >= 80 {
    print("Grade: B โ€” Great work!")
} else if score >= 70 {
    print("Grade: C โ€” Good effort")
} else if score >= 60 {
    print("Grade: D โ€” Needs improvement")
} else {
    print("Grade: F โ€” Please retry")
}

print("---")

// switch โ€” much more powerful than other languages
switch score {
case 90...100:          // Range matching!
    print("A")
case 80..<90:           // Half-open range
    print("B")
case 70..<80:
    print("C โ€” this matches")
case 60..<70:
    print("D")
case let s where s < 0: // where clause for extra conditions
    print("Invalid score: \(s)")
default:
    print("F")
}

print("---")

// switch on strings โ€” multiple values per case
let day = "Saturday"
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    print("\(day) is a weekday")
case "Saturday", "Sunday":
    print("\(day) โ€” enjoy the weekend!")
default:
    print("Unknown day")
}
โ–ถ Swift Playground
๐Ÿ’ก

Swift ranges: 1...5 includes both 1 and 5 (closed range). 1..<5 includes 1 but NOT 5 (half-open range). Ranges work in switch cases, for loops, and array slicing. They are one of Swift's most elegant features.

for-in, while, and repeat-while

Swift's for-in loop iterates over any sequence โ€” arrays, ranges, strings, dictionaries. Use while when you do not know how many times to loop upfront. repeat-while (like do-while in other languages) always runs at least once.

main.swiftSwift
import Foundation

// for-in over a range
for i in 1...5 {
    print("Count: \(i)")
}

print("---")

// for-in with stride (step) โ€” like Python's range with step
for i in stride(from: 0, to: 20, by: 5) {
    print(i, terminator: " ")
}
print()

print("---")

// for-in over an array
let fruits = ["apple", "banana", "cherry"]
for fruit in fruits {
    print("I like \(fruit)")
}

print("---")

// for-in with enumerated โ€” index AND value
for (index, fruit) in fruits.enumerated() {
    print("\(index): \(fruit)")
}

print("---")

// while loop
var power = 1
while power < 100 {
    power *= 2
}
print("First power of 2 over 100: \(power)")

// repeat-while โ€” always runs at least once (like do-while)
var count = 5
repeat {
    print("Countdown: \(count)")
    count -= 1
} while count > 0
print("Blast off! ๐Ÿš€")
โ–ถ Swift Playground
๐Ÿ’ก

stride(from: 0, to: 20, by: 5) goes from 0 up to (but not including) 20, stepping by 5. Use stride(from:through:by:) if you want to include the end value. The terminator parameter on print() replaces the default newline โ€” print(x, terminator: " ") prints on the same line.

โš™๏ธ
Module 04

Functions & Closures

Swift functions have a unique feature: argument labels. Every parameter has an external label (used by the caller) and an internal name (used inside the function). This makes function calls read like natural English sentences. Closures are self-contained blocks of code โ€” Swift's version of lambdas or anonymous functions.

Functions with Argument Labels

A Swift function can have two names for each parameter: one for the caller and one for inside the function. This is unique to Swift and makes APIs incredibly readable. Use _ to omit the external label if it would be redundant.

main.swiftSwift
import Foundation

// func name(externalLabel internalName: Type) -> ReturnType
func greet(person name: String) -> String {
    return "Hello, \(name)!"
}

// Call uses the external label
print(greet(person: "Alice"))

// _ suppresses the external label
func double(_ number: Int) -> Int {
    return number * 2
}
print(double(5))   // No label needed

// Multiple parameters with labels
func describe(student name: String,
              scoring score: Int,
              in city: String) -> String {
    return "\(name) from \(city) scored \(score)%"
}

// Reads like a sentence!
print(describe(student: "Alice", scoring: 95, in: "Cape Town"))

// Default parameter values
func createProfile(name: String,
                   age: Int,
                   role: String = "Student") -> String {
    return "\(name), \(age), \(role)"
}

print(createProfile(name: "Alice", age: 20))
print(createProfile(name: "Bob", age: 35, role: "Teacher"))

// Multiple return values with a tuple
func minMax(of numbers: [Int]) -> (min: Int, max: Int) {
    return (numbers.min()!, numbers.max()!)
}

let result = minMax(of: [3, 1, 7, 2, 9, 4])
print("Min: \(result.min), Max: \(result.max)")
โ–ถ Swift Playground
๐Ÿ’ก

Argument labels are what makes Swift APIs read like English: tableView.deleteRows(at: indexPaths, with: .fade) โ€” this reads almost like a sentence. When you design your own functions, choose labels that make the call site clear.

Closures โ€” Swift Lambdas

A closure is an unnamed block of code โ€” like a function you can pass around. They are used constantly with collection methods like map, filter and sorted. Swift lets you write them in increasingly shorter forms โ€” the full form, then shorthand argument names, then trailing closure syntax.

main.swiftSwift
import Foundation

let numbers = [5, 3, 8, 1, 9, 2, 7, 4, 6]

// Full closure syntax
let doubled = numbers.map({ (n: Int) -> Int in
    return n * 2
})
print("Doubled:", doubled)

// Shorter: Swift infers types
let tripled = numbers.map({ n in n * 3 })
print("Tripled:", tripled)

// Shortest: shorthand argument names $0, $1...
let squared = numbers.map { $0 * $0 }
print("Squared:", squared)

// filter โ€” keep only matching items
let evens = numbers.filter { $0 % 2 == 0 }
print("Evens:", evens)

let overFive = numbers.filter { $0 > 5 }
print("Over 5:", overFive)

// sorted โ€” custom sort order
let ascending  = numbers.sorted { $0 < $1 }
let descending = numbers.sorted { $0 > $1 }
print("Ascending:", ascending)
print("Descending:", descending)

// reduce โ€” collapse to one value
let sum = numbers.reduce(0) { $0 + $1 }
let product = numbers.reduce(1) { $0 * $1 }
print("Sum:", sum)
print("Product:", product)
โ–ถ Swift Playground
๐Ÿ’ก

When a closure is the last argument, you can write it outside the parentheses โ€” this is called "trailing closure syntax". numbers.sorted() { $0 < $1 } or numbers.sorted { $0 < $1 }. Swift code uses this constantly.

๐Ÿ—๏ธ
Module 05

Structs & Classes

Swift has two ways to create custom types: structs and classes. The key difference is how they are copied. Structs are value types โ€” when you assign a struct to another variable, you get an independent copy. Classes are reference types โ€” multiple variables can point to the same object. In Swift, prefer structs unless you specifically need reference semantics.

Structs โ€” Value Types

Structs are the workhorses of Swift. They are lightweight, fast, and safe because they are always copied. Methods that modify a struct must be marked mutating. Structs get a free "memberwise initializer" โ€” no need to write init() yourself.

main.swiftSwift
import Foundation

struct Student {
    // Properties
    let name: String
    var score: Int
    var isEnrolled: Bool = true  // default value

    // Computed property โ€” calculated from other properties
    var grade: String {
        switch score {
        case 90...100: return "A"
        case 80..<90:  return "B"
        case 70..<80:  return "C"
        case 60..<70:  return "D"
        default:       return "F"
        }
    }

    // Method
    func describe() -> String {
        return "\(name): \(score)% (\(grade))"
    }

    // Mutating method โ€” changes a property
    mutating func addBonus(_ points: Int) {
        score = min(score + points, 100)
    }
}

// Memberwise initializer โ€” free!
var alice = Student(name: "Alice", score: 87)
let bob   = Student(name: "Bob",   score: 62, isEnrolled: true)

print(alice.describe())
print(bob.describe())

// Structs are VALUE TYPES โ€” copied!
var backup = alice          // Independent copy
backup.score = 50
print("\nAlice:", alice.score)  // Still 87
print("Backup:", backup.score)  // 50

// Mutating method
alice.addBonus(10)
print("After bonus:", alice.describe())
โ–ถ Swift Playground
๐Ÿ’ก

Computed properties look like stored properties to the outside world but are calculated on the fly. They are perfect for derived values like grade that depend on other properties โ€” no need to keep them in sync manually.

Classes โ€” Reference Types & Inheritance

Classes add two things structs cannot do: inheritance (one class can build on another) and reference semantics (multiple variables share the same object). Classes require an explicit init() and use override to replace a parent method.

main.swiftSwift
import Foundation

class Animal {
    var name: String
    var sound: String

    // Classes need an explicit initializer
    init(name: String, sound: String) {
        self.name  = name
        self.sound = sound
    }

    func speak() -> String {
        return "\(name) says \(sound)!"
    }

    func describe() -> String {
        return "Animal: \(name)"
    }
}

// Subclass โ€” inherits everything from Animal
class Dog: Animal {
    var breed: String

    init(name: String, breed: String) {
        self.breed = breed
        super.init(name: name, sound: "Woof")
    }

    // override replaces the parent's method
    override func describe() -> String {
        return "Dog: \(name) (\(breed))"
    }

    func fetch() -> String {
        return "\(name) fetches the ball! ๐ŸŽพ"
    }
}

let animal = Animal(name: "Generic Animal", sound: "...")
let dog    = Dog(name: "Rex", breed: "Labrador")

print(animal.speak())
print(dog.speak())      // Inherited from Animal
print(dog.describe())   // Overridden
print(dog.fetch())      // Dog-specific

// Classes are REFERENCE TYPES โ€” shared!
let dog2 = dog          // Same object, not a copy
dog2.name = "Max"
print("\ndog.name:", dog.name)   // Also "Max"!
print("dog2.name:", dog2.name)
โ–ถ Swift Playground
๐Ÿ’ก

The reference type behaviour of classes catches many beginners off guard โ€” changing dog2 also changes dog because they are the SAME object. This is why Swift recommends structs by default. Only use classes when you need inheritance or shared mutable state.

๐Ÿ“‹
Module 06

Enums & Collections

Swift enums are far more powerful than enums in most languages โ€” they can have methods, computed properties, and associated values (data attached to each case). Arrays, Dictionaries and Sets are Swift's three main collection types, all generic and type-safe.

Enumerations โ€” Powerful Swift Enums

An enum defines a type with a fixed set of cases. In Swift, enums can have methods and associated values โ€” extra data attached to a specific case. This makes them far more expressive than enums in other languages.

main.swiftSwift
import Foundation

// Basic enum
enum Direction {
    case north, south, east, west
}

let heading = Direction.north

switch heading {
case .north: print("Heading north")
case .south: print("Heading south")
case .east:  print("Heading east")
case .west:  print("Heading west")
}

// Enum with a raw value (String)
enum Grade: String {
    case a = "Outstanding"
    case b = "Good"
    case c = "Average"
    case f = "Failed"
}

let myGrade = Grade.b
print("Grade B means: \(myGrade.rawValue)")

// Enum with associated values โ€” each case carries data
enum NetworkResult {
    case success(data: String, statusCode: Int)
    case failure(error: String)
    case loading
}

let result = NetworkResult.success(data: "User data", statusCode: 200)

switch result {
case .success(let data, let code):
    print("Success \(code): \(data)")
case .failure(let error):
    print("Failed: \(error)")
case .loading:
    print("Loading...")
}

// Enum with methods
enum Season {
    case spring, summer, autumn, winter

    func description() -> String {
        switch self {
        case .spring: return "Flowers bloom ๐ŸŒธ"
        case .summer: return "Hot and sunny โ˜€๏ธ"
        case .autumn: return "Leaves fall ๐Ÿ‚"
        case .winter: return "Cold and snowy โ„๏ธ"
        }
    }
}

let now = Season.summer
print(now.description())
โ–ถ Swift Playground
๐Ÿ’ก

Associated values make enums incredibly expressive. NetworkResult.success(data:statusCode:) carries the actual data with it. This pattern is used everywhere in professional Swift โ€” especially for Result<Success, Failure> which is built into the Swift standard library.

Arrays, Dictionaries & Sets

Swift has three main collection types. Arrays are ordered lists. Dictionaries store key-value pairs. Sets are unordered collections with no duplicates. All three are generics โ€” the type inside the angle brackets must match.

main.swiftSwift
import Foundation

// โ”€โ”€ ARRAYS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var fruits = ["apple", "banana", "cherry"]

fruits.append("mango")
fruits.insert("kiwi", at: 1)
fruits.remove(at: 0)

print("Fruits:", fruits)
print("Count:", fruits.count)
print("First:", fruits.first ?? "empty")
print("Contains banana:", fruits.contains("banana"))

// Sort
let sorted = fruits.sorted()
print("Sorted:", sorted)

print("---")

// โ”€โ”€ DICTIONARIES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var scores: [String: Int] = [
    "Alice":   95,
    "Bob":     82,
    "Charlie": 71,
]

// Access โ€” returns optional!
print("Alice:", scores["Alice"] ?? 0)
print("Dave:", scores["Dave"] ?? 0)

// Add / update
scores["Diana"] = 88
scores["Alice"] = 97
print("Updated Alice:", scores["Alice"]!)

// Iterate
for (name, score) in scores.sorted(by: { $0.key < $1.key }) {
    print("  \(name): \(score)")
}

print("---")

// โ”€โ”€ SETS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
var colours: Set = ["red", "green", "blue", "red", "green"]
print("Set (no duplicates):", colours)

let setA: Set = [1, 2, 3, 4, 5]
let setB: Set = [4, 5, 6, 7, 8]
print("Union:", setA.union(setB).sorted())
print("Intersection:", setA.intersection(setB).sorted())
โ–ถ Swift Playground
๐Ÿ’ก

Dictionary access always returns an optional โ€” scores["Alice"] returns Int?, not Int. This forces you to handle the case where the key might not exist. Use scores["Alice"] ?? 0 to provide a default, or scores["Alice"]! only when you are absolutely certain the key exists.

๐ŸŽ‰

You finished the Swift tutorial!

You now know variables, optionals, control flow, functions, closures, structs, classes, enums, arrays, dictionaries and sets. You have the foundation to start building real iOS and macOS apps with SwiftUI.