All articles
iOS19 min read

Mastering OOP in Swift: A Beginner-to-Advanced Guide to Object-Oriented Programming

🚀 Think you already know OOP? Let’s put it in Swift terms. This beginner-to-advanced guide unpacks Object-Oriented Programming the Swifty…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

🧭 Introduction: OOP in Swift? Still a Big Deal in 2025?

Ah yes, Object-Oriented Programming — the OG of software design. You’ve heard of it. You’ve probably used it. And at some point, you might’ve even tried to avoid it because you heard “Swift is all about protocols now.”

But here’s the thing: OOP is far from dead — especially in Swift. While Apple has leaned into protocol-oriented programming (POP), the truth is most production iOS codebases still rely on OOP fundamentals: classes, inheritance, encapsulation, and the whole gang.

And guess what? That’s not a bad thing.

Whether you’re:

This article is your deep-dive into OOP in Swift — not the generic academic stuff, but real, practical, Swift-native, and 2025-relevant knowledge. We’ll go from the core principles all the way to under-the-hood behavior, common pitfalls, and patterns you’ll actually use in apps.

💡 And no — this isn’t a battle between OOP and POP. We’re saving that spicy showdown for a separate article. 👉 Read: Protocol-Oriented Programming in Swift

**Protocol-Oriented Programming in Swift Explained: How POP Works & Why It Matters** _Think Swift is all about classes? Think again. Dive into Protocol-Oriented Programming (POP) in Swift and learn how…_medium.comhttps://medium.com/swift-pal/protocol-oriented-programming-in-swift-explained-how-pop-works-why-it-matters-ab264a8759ff

By the end of this guide, you’ll walk away with:

Let’s jump in.

🧱 Core OOP Concepts (In Swift Context)

Object-Oriented Programming is built around four fundamental principles — Encapsulation, Inheritance, Polymorphism, and Abstraction. Sounds fancy, but in Swift, these are less about showing off your CS degree and more about writing clean, reusable, and safe code.

Let’s break these down with some Swift-flavored seasoning.

🔐 1. Encapsulation: “Keep your secrets!”

Encapsulation is all about hiding internal implementation and exposing only what’s necessary. In Swift, this is done through access control like private, fileprivate, internal, public, and open.

class BankAccount {
    private var balance: Double = 0.0

    func deposit(amount: Double) {
        balance += amount
    }

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

Here, balance is private — we’re saying “you can put money in, you can check your balance, but you can’t mess with the internal numbers directly.” Just like a real bank account... ideally 😅

🔗 Want to dive deeper into access control and how it affects testability? Check out _Unit Testing in Swift_.

**Unit Testing in Swift Made Easy: A Beginner’s Guide With Real Examples** _A beginner’s guide to Swift unit testing — learn the basics, write your first tests, and improve your code quality._medium.comhttps://medium.com/swift-pal/unit-testing-in-swift-made-easy-a-beginners-guide-with-real-examples-0409f65e84f6

🧬 2. Inheritance: “Get it from your mama”

Inheritance lets you create a class hierarchy, where child classes inherit functionality from a parent.

class Vehicle {
    func startEngine() {
        print("Engine started")
    }
}

class Car: Vehicle {
    func playMusic() {
        print("Playing music 🎵")
    }
}

let myCar = Car()
myCar.startEngine() // Inherited!
myCar.playMusic()

Swift allows single inheritance only (a class can inherit from one parent). But beware: overusing inheritance is the fast lane to spaghetti code. Prefer composition when possible — we’ll revisit that later.

🧩 3. Polymorphism: “Same interface, different results”

Polymorphism means different types can respond to the same method call in their own way. It shines when using method overriding or protocols (but we’ll stick to class-based examples for now).

class Animal {
    func speak() {
        print("Some generic sound")
    }
}

class Dog: Animal {
    override func speak() {
        print("Woof! 🐶")
    }
}

class Cat: Animal {
    override func speak() {
        print("Meow! 🐱")
    }
}

let pets: [Animal] = [Dog(), Cat()]
pets.forEach { $0.speak() } // Woof! Meow!

Polymorphism is what makes code extensible and dynamic, especially when building UIs, networking modules, or working with design patterns like Strategy or Factory.

🎭 4. Abstraction: “Just tell me what, not how”

Abstraction is the idea of defining essential behavior without revealing the complex inner workings. It usually shows up when you use abstract classes or protocols — but in OOP terms, think of it like you can use it without understanding the mess inside.

Here’s a cleaner example:

class Payment {
    func process() {
        fatalError("This method should be overridden")
    }
}

class CreditCardPayment: Payment {
    override func process() {
        print("Processing credit card payment 💳")
    }
}

let payment = CreditCardPayment()
payment.process()

This makes Payment kind of a blueprint, forcing subclasses to implement process() — ensuring consistent structure across all payment types.

🧠 TL;DR on the 4 Pillars (Swifty Edition):

Next up — we answer the hot question:

_When should I actually use a class in Swift? And why do people love structs so much?_

Let’s jump into that debate (with a balanced, caffeine-fueled perspective) in the next section ☕👇

🆚 Classes vs Structs in Swift: When & Why

If you’ve been around Swift devs for more than 15 minutes, you’ve probably heard:

“Use _struct_ unless you really need a _class_.”

But what does that even mean in real-world code? And when is it actually better to use a class?

Let’s unpack it — calmly. With coffee. ☕🧠

🧱 Value Types vs Reference Types

At the core of this debate is how data is stored and passed around:

struct UserStruct {
    var name: String
}

class UserClass {
    var name: String
}

var structUser1 = UserStruct(name: "Alice")
var structUser2 = structUser1
structUser2.name = "Bob"

print(structUser1.name) // Alice
print(structUser2.name) // Bob — different copy!

var classUser1 = UserClass()
classUser1.name = "Alice"
var classUser2 = classUser1
classUser2.name = "Bob"

print(classUser1.name) // Bob — same object!
🧠 “Structs give you safety-by-default. Classes give you shared state. Choose your weapon wisely.”

🔄 ARC, Lifecycle, and Memory

Classes come with Automatic Reference Counting (ARC) — which means Swift keeps track of references to know when to deallocate memory. Structs don’t need ARC since they don’t share references.

This is important in:

If the object needs to stick around or be shared across multiple owners, use a class. If it’s a piece of data you don’t need to mutate globally — a struct is perfect.

📦 Use Case Examples

✅ When to Use struct

✅ When to Use class

🔗 Want to see real-world class-based architecture? Check out this _URLSession Networking Layer_ — it’s clean, testable, and shows exactly _when classes make sense._

**URLSession in Swift: Build a Clean and Testable Networking Layer** _Tired of tangled networking code in your iOS apps? Learn how to build a clean, testable, and scalable networking layer…_medium.comhttps://medium.com/swift-pal/urlsession-in-swift-build-a-clean-and-testable-networking-layer-261f96a3df63

🏗️ In Scalable Apps…

In modular, scalable apps, we often separate logic into class-based layers: Coordinators, Services, ViewModels — each with defined responsibilities and controlled mutability.

🔗 Building large apps? Learn how we use class-based structures in this _Scalable Modular Architecture Guide_.

**How to Structure a Scalable iOS App with Modular Architecture** _Struggling with messy codebases and slow builds? Learn how modular architecture can make your iOS app scalable…_medium.comhttps://medium.com/swift-pal/how-to-structure-a-scalable-ios-app-with-modular-architecture-b0130da83bca

⚖️ TL;DR — Which One Do I Pick?

Here’s a quick way to decide between struct and class in Swift:

1\. ✅ Choose **struct** if:

2\. ✅ Choose **class** if:

🔧 How OOP Works Under the Hood in Swift

So far, we’ve played on the surface: classes, inheritance, ARC, etc.

But what really happens behind the scenes when you create a class, call a method, or override a function in Swift?

Let’s go under the hood — but gently, like a dev on a Friday afternoon. 👨‍🔧

🧠 Swift’s Method Dispatch: Not All Calls Are Equal

In Swift, method calls can be resolved in three different ways:

  1. Static Dispatch — Compile-time resolution (most efficient).
  2. Table Dispatch (vtable) — Used for most class methods.
  3. Message Dispatch — Dynamic resolution (like in Objective-C).

Let’s break it down.

🚀 Static Dispatch (Fastest)

Used mostly with structs, enums, and final methods in classes. Since the compiler knows exactly which method to call, it resolves everything at compile time.

struct Rocket {
    func launch() {
        print("Liftoff! 🚀")
    }
}

No surprises. Super fast. Zero inheritance issues.

📦 Table Dispatch (vtable)

This is how Swift usually handles overridable class methods. It builds a virtual table — a lookup of function pointers. When you call a method, Swift looks it up in the vtable and runs it.

class Animal {
    func speak() {
        print("Some sound")
    }
}

class Dog: Animal {
    override func speak() {
        print("Woof!")
    }
}

Here, the compiler doesn’t hard-code the method. It inserts a pointer to speak() into a virtual table — so when you call speak() on Dog, the correct version is chosen at runtime.

🕵️ Dynamic Dispatch

This one is Objective-C style. Swift uses dynamic dispatch only when you explicitly ask for it — using the @objc or dynamic keywords.

@objc class OldSchool {
    @objc dynamic func talk() {
        print("Talking via dynamic dispatch ☎️")
    }
}

This is less efficient, but useful for things like:

⚠️ Tip: Avoid _@objc_ and _dynamic_ unless you need them — they bring runtime overhead and tie your code to Objective-C mechanics.

🔒 The Power of final

When you mark a class or method as final, you’re telling Swift:

“Hey, nobody’s subclassing or overriding this. Ever.”

This allows Swift to use static dispatch, which is faster and safer.

final class Logger {
    func log(_ message: String) {
        print("LOG:", message)
    }
}

Use final by default unless subclassing is absolutely needed — it makes your code faster and easier to reason about.

🧼 ARC in Swift — Automatic Reference Counting

You’ve probably heard this one before: Swift manages memory using ARC — it keeps track of how many things are referencing an object and deallocates it when that count hits zero.

Example:

class Person {
    var name: String

    init(name: String) {
        self.name = name
        print("Created:", name)
    }

    deinit {
        print("Deallocated:", name)
    }
}

var p1: Person? = Person(name: "John")
var p2 = p1
p1 = nil
p2 = nil // Now it deinitializes!

ARC is not magic — it works great until you accidentally create strong reference cycles. (We’ll talk about that in the next section 👀)

🧠 TL;DR

⚠️ Common Mistakes in Swift OOP

Yes, classes are powerful. But with great power comes great responsibility… and also a surprising number of memory leaks and awkward code reviews if you’re not careful.

Let’s walk through some of the most common Swift OOP pitfalls — and how to avoid them like a pro.

🔄 1. Retain Cycles: The Silent Memory Killer

Swift uses ARC to manage memory. But if two objects hold strong references to each other, they’ll never be deallocated. Ever. This is called a retain cycle — and it’s the #1 gotcha in class-based code.

class ViewModel {
    var coordinator: Coordinator?
}

class Coordinator {
    var viewModel: ViewModel?
}

you’ve got a retain cycle. Neither can be deallocated, even if your entire app forgets about them.

✅ Solution:

Use weak or unowned to break the cycle:

class Coordinator {
    weak var viewModel: ViewModel?
}
🔗 Want to dig deeper into how memory issues show up in real apps? Check out _Race Conditions vs Deadlocks_.

**When Swift Code Goes Rogue: Race Conditions vs. Deadlocks Explained 🧨** _Ever seen your Swift app crash without mercy? 🧨 Meet the chaos duo: Race Condition (the reckless sprinter) and…_medium.comhttps://medium.com/swift-pal/when-swift-code-goes-rogue-race-conditions-vs-deadlocks-explained-4a80e75a835a

🧱 2. Overusing Inheritance

Inheritance feels powerful… until you’re five layers deep and one override breaks your entire app.

class Animal {}
class Dog: Animal {}
class SuperDog: Dog {}
class UltraDog: SuperDog {}
// 👀 What even is going on anymore?

Swift’s final keyword exists for a reason — to help you say “this stops here.”

✅ Best Practice:

Use composition over inheritance whenever possible. Instead of building massive class trees, inject behavior.

🔓 3. Bad Access Control

Everything doesn’t need to be public, open, or even internal. By default, Swift uses internal, but being intentional with your access control improves safety and testability.

public class PaymentManager {
    public var payments = [String]() // Why is this public??
}

Suddenly, your intern on another module can mutate critical state. Not great.

✅ Use access modifiers wisely:

🧠 4. Class Bloat: Doing Too Much

Ever seen a view controller with 500+ lines of code handling UI, networking, validation, analytics, and snacks ordering? That’s class bloat — a clear sign of OOP misuse.

✅ Fix it with proper responsibilities:

🔗 Want a full breakdown on how to structure large apps cleanly? Don’t miss the _Scalable Modular Architecture Guide_

**How to Structure a Scalable iOS App with Modular Architecture** _Struggling with messy codebases and slow builds? Learn how modular architecture can make your iOS app scalable…_medium.comhttps://medium.com/swift-pal/how-to-structure-a-scalable-ios-app-with-modular-architecture-b0130da83bca

🧠 TL;DR — Mistakes to Watch Out For

Don’t worry — you’re not alone. Every dev has tripped over these at some point. The key is learning from them and leveling up 💪

🛠️ Real-World OOP Design Patterns in Swift

Patterns aren’t just trendy acronyms we drop in interviews — they’re proven solutions to common software problems. And when used right in Swift, they can make your codebase cleaner, more testable, and honestly… more enjoyable to work in.

Let’s explore a few OOP-friendly patterns that you’ll actually use in production Swift code.

🧙‍♂️ 1. Singleton

Use when: You need only one instance of a class shared across the app — like a logger, user session, or networking layer.

class AnalyticsManager {
    static let shared = AnalyticsManager()

    private init() {}

    func track(event: String) {
        print("Tracked:", event)
    }
}
🔧 Tip: Consider injecting it where needed rather than accessing _shared_ everywhere. 🔗 Want to see this in action with better design? Check out this _URLSession Networking Article_

**URLSession in Swift: Build a Clean and Testable Networking Layer** _Tired of tangled networking code in your iOS apps? Learn how to build a clean, testable, and scalable networking layer…_medium.comhttps://medium.com/swift-pal/urlsession-in-swift-build-a-clean-and-testable-networking-layer-261f96a3df63

🏭 2. Factory

Use when: You want to create objects without exposing the creation logic to the caller.

protocol Shape {
    func draw()
}

class Circle: Shape {
    func draw() { print("Drawing circle ⭕️") }
}

class Square: Shape {
    func draw() { print("Drawing square ⬛️") }
}

class ShapeFactory {
    static func makeShape(type: String) -> Shape {
        switch type {
        case "circle": return Circle()
        case "square": return Square()
        default: fatalError("Unknown shape")
        }
    }
}

🎭 3. Strategy Pattern (via Delegation or Closures)

Use when: You want to dynamically choose behavior at runtime.

protocol PaymentStrategy {
    func pay(amount: Double)
}

class CreditCardPayment: PaymentStrategy {
    func pay(amount: Double) {
        print("Paid \(amount) with 💳")
    }
}

class UPIBasedPayment: PaymentStrategy {
    func pay(amount: Double) {
        print("Paid \(amount) using UPI 📱")
    }
}

class Checkout {
    var paymentMethod: PaymentStrategy?

    func process(amount: Double) {
        paymentMethod?.pay(amount: amount)
    }
}

You can inject any PaymentStrategy, and Checkout doesn't care how it works. Clean. Flexible. Reusable.

🧱 4. MVC (and how OOP fits in)

Model-View-Controller is the backbone of many UIKit apps (and even some SwiftUI ones, under the hood).

Using OOP:

🧠 5. MVVM Using Classes

MVVM in iOS almost always uses class-based ViewModels, especially when binding to UIKit views or using Combine/SwiftUI.

Why classes?

class LoginViewModel: ObservableObject {
    @Published var username = ""
    @Published var password = ""

    func login() {
        // Perform login logic
    }
}

Yes, you could make it work with structs — but at the cost of reactivity, mutation, and sanity 😅

🔗 Want to see these patterns come together in scalable apps? Don’t miss _How to Structure a Scalable iOS App with Modular Architecture_

**How to Structure a Scalable iOS App with Modular Architecture** _Struggling with messy codebases and slow builds? Learn how modular architecture can make your iOS app scalable…_medium.comhttps://medium.com/swift-pal/how-to-structure-a-scalable-ios-app-with-modular-architecture-b0130da83bca

TL;DR — Patterns That Actually Work in Swift

You don’t have to use every pattern in every project. Just keep them in your toolkit, and pull them out when the problem fits.

🧳 Conclusion: OOP in Swift — Still Worth Mastering

So… is Object-Oriented Programming still relevant in Swift in 2025?

Absolutely. Just not in the “inherit-everything-and-call-it-a-day” kind of way.

Swift encourages modern, thoughtful design — and OOP, when used well, is still a crucial tool in your toolbox. Classes give you:

But like any sharp tool, misuse it… and it bites back 🐍

✅ Quick Takeaways:

🎯 What’s Next?

While OOP helps you build solid foundations, Swift has another powerful paradigm that many engineers swear by: Protocol-Oriented Programming (POP).

👀 If you’re wondering how POP differs from OOP, when to use it, and whether it’s all just a trendy buzzword — don’t miss the upcoming article: 👉 _Protocol-Oriented Programming in Swift_

**Protocol-Oriented Programming in Swift Explained: How POP Works & Why It Matters** _Think Swift is all about classes? Think again. Dive into Protocol-Oriented Programming (POP) in Swift and learn how…_medium.comhttps://medium.com/swift-pal/protocol-oriented-programming-in-swift-explained-how-pop-works-why-it-matters-ab264a8759ff

🧠 Want to Keep Learning?

Check out some of the best-performing guides to level up your architecture and codebase:

And if this guide helped you think a little differently about OOP in Swift, here’s how you can show some love:

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading