Swift โ Complete Beginner Tutorial
6 modules ยท 17 examples ยท Click โถ Run on any example to see the output
๐ก How to use this page
Read the explanation, study the code, then hit โถ Run to see the output.
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.
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 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).
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)")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.
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)")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.
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.
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")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.
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"))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.
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.
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 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.
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! ๐")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.
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.
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)")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.
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)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.
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.
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())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.
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)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.
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.
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())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.
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())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.