Swift supports object-oriented programming with classes, structs, and protocols. OOP is a programming paradigm based on the concept of objects, which contain data in the form of properties and code in the form of methods. Swift distinguishes between value types (structs, enums) and reference types (classes), a core concept for writing correct, efficient code.


These notes cover the four pillars of OOP — Encapsulation, Inheritance, Polymorphism, and Abstraction — applied to Swift's type system with practical examples.

🏗️ Classes & Objects

Classes are reference types — when you assign or pass a class instance, multiple variables point to the same object in memory. Classes support inheritance, deinitializers, and reference counting. Unlike structs, classes do not get a free memberwise initializer — you must write your own init().

Defining a Class

Person.swift
class Person {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func greet() -> String {
        return "Hi, I'm \(name) and I'm \(age) years old."
    }
}

// Creating an object (instance)
let person = Person(name: "Carlos", age: 25)
print(person.greet())  // Hi, I'm Carlos and I'm 25 years old.

Classes Are Reference Types

reference_type.swift
class Counter {
    var value = 0
}

let c1 = Counter()
let c2 = c1       // c2 points to the SAME object as c1
c2.value = 5

print(c1.value)   // 5  — both references see the change
print(c2.value)   // 5

🔒 Encapsulation

Encapsulation hides internal implementation details and exposes only what is necessary through a controlled interface. Swift provides access control keywords: private, fileprivate, internal (default), public, and open.

BankAccount.swift
class BankAccount {
    private var balance: Double = 0.0   // hidden from outside

    var owner: String

    init(owner: String) {
        self.owner = owner
    }

    // Public interface — controlled access
    func deposit(amount: Double) {
        guard amount > 0 else { return }
        balance += amount
    }

    func withdraw(amount: Double) -> Bool {
        guard amount > 0, amount <= balance else { return false }
        balance -= amount
        return true
    }

    func getBalance() -> Double {
        return balance
    }
}

let account = BankAccount(owner: "Ana")
account.deposit(amount: 1000)
account.withdraw(amount: 200)
print(account.getBalance())   // 800.0

// account.balance = 999     // ❌ Error: 'balance' is private

Access control levels in Swift

KeywordScope
privateOnly within the enclosing declaration
fileprivateAnywhere within the same source file
internalAnywhere within the same module (default)
publicAccessible from other modules, cannot be subclassed
openAccessible and subclassable from other modules

🧬 Inheritance

A class can inherit properties, methods, and other characteristics from another class. The inheriting class is called a subclass, and the class it inherits from is the superclass. Use override to replace a superclass method, and super to call the parent implementation.

Vehicle.swift
// Superclass (parent)
class Vehicle {
    var brand: String
    var speed: Int

    init(brand: String, speed: Int) {
        self.brand = brand
        self.speed = speed
    }

    func describe() -> String {
        return "\(brand) moving at \(speed) km/h"
    }
}

// Subclass (child) — inherits from Vehicle
class Car: Vehicle {
    var doors: Int

    init(brand: String, speed: Int, doors: Int) {
        self.doors = doors
        super.init(brand: brand, speed: speed)   // call parent init
    }

    override func describe() -> String {
        return "\(brand) car with \(doors) doors at \(speed) km/h"
    }
}

let car = Car(brand: "Toyota", speed: 120, doors: 4)
print(car.describe())  // Toyota car with 4 doors at 120 km/h
Prevent inheritance: Mark a class as final class Vehicle { ... } to prevent any subclass from inheriting it. You can also mark individual methods as final.

🎭 Polymorphism

Polymorphism means "many forms" — a subclass instance can be treated as its superclass type, but it still runs its own overridden methods. This allows writing flexible, generic code that works with families of related types.

polymorphism.swift
class Animal {
    var name: String

    init(name: String) {
        self.name = name
    }

    func speak() -> String {
        return "..."
    }
}

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

class Cat: Animal {
    override func speak() -> String {
        return "Meow!"
    }
}

// Polymorphism: array of Animal, each runs its own speak()
let animals: [Animal] = [
    Dog(name: "Rex"),
    Cat(name: "Luna"),
    Dog(name: "Max")
]

for animal in animals {
    print("\(animal.name): \(animal.speak())")
}
// Rex: Woof!
// Luna: Meow!
// Max: Woof!

Type Checking & Casting

type_casting.swift
for animal in animals {
    if animal is Dog {
        print("\(animal.name) is a dog")
    }

    if let dog = animal as? Dog {
        print("Dog found: \(dog.name)")
    }
}

// is  → checks type (returns Bool)
// as? → conditional downcast (returns optional)
// as! → forced downcast (crashes if wrong type)

🔧 Initializers (Constructors)

Initializers set up the initial state of an instance. Swift has designated initializers (the main init), convenience initializers (secondary shortcuts), and structs get a free memberwise initializer. Every stored property must have a value before init completes.

Simple Initializer (No Parameters)

simple_init.swift
class Light {
    var isOn: Bool
    var brightness: Int

    // Simple init — sets default values
    init() {
        self.isOn = false
        self.brightness = 0
    }
}

let lamp = Light()
print(lamp.isOn)         // false
print(lamp.brightness)   // 0

Designated Initializer (With Parameters)

designated_init.swift
class Student {
    var name: String
    var grade: Int
    var gpa: Double

    // Designated initializer — receives all properties
    init(name: String, grade: Int, gpa: Double) {
        self.name = name
        self.grade = grade
        self.gpa = gpa
    }
}

let student = Student(name: "Maria", grade: 3, gpa: 9.5)
print("\(student.name) — Grade \(student.grade), GPA: \(student.gpa)")
// Maria — Grade 3, GPA: 9.5

Convenience Initializer

convenience_init.swift
class Color {
    var red: Double
    var green: Double
    var blue: Double

    // Designated initializer
    init(red: Double, green: Double, blue: Double) {
        self.red = red
        self.green = green
        self.blue = blue
    }

    // Convenience — shortcut for grayscale
    convenience init(gray: Double) {
        self.init(red: gray, green: gray, blue: gray)
    }

    // Convenience — shortcut for white
    convenience init() {
        self.init(gray: 1.0)
    }
}

let red   = Color(red: 1.0, green: 0.0, blue: 0.0)  // designated
let gray  = Color(gray: 0.5)                        // convenience
let white = Color()                                  // convenience

Initializers in Subclasses

subclass_init.swift
class Shape {
    var color: String

    init(color: String) {
        self.color = color
    }
}

class Circle: Shape {
    var radius: Double

    // Subclass designated init
    init(color: String, radius: Double) {
        self.radius = radius              // 1. Init own properties first
        super.init(color: color)           // 2. Then call super.init
    }

    func area() -> Double {
        return Double.pi * radius * radius
    }
}

let circle = Circle(color: "blue", radius: 5.0)
print("Area: \(circle.area())")   // Area: 78.539...
Rule: In a subclass init, always set your own properties before calling super.init(). A convenience init must call another init in the same class with self.init(...), never super.init().

📋 Quick Reference

Key Swift OOP concepts

ConceptSwift SyntaxNotes
Define a classclass Name { ... }Reference type, needs explicit init
Create an objectlet obj = MyClass()Calls the initializer
Inheritanceclass Child: Parent { ... }Only classes, not structs
Override a methodoverride func method()Must match parent signature
Call parent methodsuper.method()Inside an override
Prevent subclassingfinal class Name { ... }Also works on methods
Private propertyprivate var x: IntEncapsulation
Designated initinit(param: Type)Main initializer
Convenience initconvenience init(...)Must call self.init(...)
Mutating methodmutating func f()Structs only — modify properties
Type checkobj is ClassNameReturns Bool
Downcastobj as? ClassNameReturns optional
Identity checkobj1 === obj2Classes only — same instance?

🏛️ The Four Pillars of OOP

🧩 Objects — Create Instances & Call Methods

An object is an instance of a class or struct. You create one by calling its initializer with ClassName(). Once created, you access its properties and methods using dot notation (object.property, object.method()).

Example 1 — Simple object with method call

dog.swift
class Dog {
    var name: String
    var breed: String

    init(name: String, breed: String) {
        self.name = name
        self.breed = breed
    }

    func bark() -> String {
        return "\(name) says: Woof! 🐕"
    }
}

// Create an object (instance of Dog)
let myDog = Dog(name: "Rocky", breed: "Labrador")

// Access properties and call methods
print(myDog.name)     // Rocky
print(myDog.breed)    // Labrador
print(myDog.bark())   // Rocky says: Woof! 🐕

Example 2 — Multiple objects from the same class

product.swift
class Product {
    var name: String
    var price: Double
    var quantity: Int

    init(name: String, price: Double, quantity: Int) {
        self.name = name
        self.price = price
        self.quantity = quantity
    }

    func totalValue() -> Double {
        return price * Double(quantity)
    }

    func summary() -> String {
        return "\(name) — $\(price) x \(quantity) = $\(totalValue())"
    }
}

// Create multiple objects from the same class
let laptop = Product(name: "MacBook Air", price: 999.0, quantity: 3)
let phone  = Product(name: "iPhone 16",  price: 799.0, quantity: 5)

print(laptop.summary())  // MacBook Air — $999.0 x 3 = $2997.0
print(phone.summary())   // iPhone 16 — $799.0 x 5 = $3995.0
print(phone.totalValue()) // 3995.0

Example 3 — Modify properties and chain method calls

player.swift
class Player {
    var name: String
    var score: Int
    var level: Int

    init(name: String) {
        self.name = name
        self.score = 0
        self.level = 1
    }

    func addPoints(_ points: Int) {
        score += points
        // Level up every 100 points
        level = (score / 100) + 1
    }

    func status() -> String {
        return "[\(name)] Level \(level) — \(score) pts"
    }
}

// Create object and interact with it
let player = Player(name: "Alex")
print(player.status())    // [Alex] Level 1 — 0 pts

player.addPoints(75)
print(player.status())    // [Alex] Level 1 — 75 pts

player.addPoints(50)
print(player.status())    // [Alex] Level 2 — 125 pts

// Modify properties directly
player.name = "Alex Pro"
print(player.status())    // [Alex Pro] Level 2 — 125 pts
Key pattern: Create with let obj = ClassName(params), read with obj.property, modify with obj.property = newValue, and call behavior with obj.method(). Each object holds its own independent state.

📚 References