Object-Oriented Programming in Swift: classes, objects, inheritance, polymorphism, encapsulation, and initializers.
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 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().
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.
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 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.
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
| Keyword | Scope |
|---|---|
private | Only within the enclosing declaration |
fileprivate | Anywhere within the same source file |
internal | Anywhere within the same module (default) |
public | Accessible from other modules, cannot be subclassed |
open | Accessible and subclassable from other modules |
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.
// 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
final class Vehicle { ... } to prevent any subclass from inheriting it. You can also mark individual methods as final.
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.
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!
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 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.
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
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
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
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...
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().
Key Swift OOP concepts
| Concept | Swift Syntax | Notes |
|---|---|---|
| Define a class | class Name { ... } | Reference type, needs explicit init |
| Create an object | let obj = MyClass() | Calls the initializer |
| Inheritance | class Child: Parent { ... } | Only classes, not structs |
| Override a method | override func method() | Must match parent signature |
| Call parent method | super.method() | Inside an override |
| Prevent subclassing | final class Name { ... } | Also works on methods |
| Private property | private var x: Int | Encapsulation |
| Designated init | init(param: Type) | Main initializer |
| Convenience init | convenience init(...) | Must call self.init(...) |
| Mutating method | mutating func f() | Structs only — modify properties |
| Type check | obj is ClassName | Returns Bool |
| Downcast | obj as? ClassName | Returns optional |
| Identity check | obj1 === obj2 | Classes only — same instance? |
private / fileprivate and expose a controlled public interface. Protects object integrity.class Child: Parent. Promotes code reuse.override. Enables flexible, generic code.
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()).
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! 🐕
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
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
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.