All articles
iOS12 min read

SOLID Principles in Swift Made Easy (With Real-Life Examples)

A beginner-friendly guide to SOLID principles in Swift — packed with code examples and real-world use cases🤓🔥

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

👋 Introduction — Why Should You Care About SOLID?

If you’ve ever looked at your Swift code and thought,

“Wait… why is UserManager handling login, logout, API calls, and making coffee?” ☕😵
you’re not alone.

Writing scalable, testable, and clean code isn’t just about impressing your teammates (though that’s a nice flex 😎). It’s about avoiding future headaches, making onboarding easier, and reducing those mysterious bugs that love to show up in production.

That’s where the SOLID principles come in. Coined by Uncle Bob (the Gandalf of clean code), these five object-oriented programming principles help you:

And don’t worry — this isn’t a CS lecture with scary diagrams and UML monsters. We’re keeping it simple, practical, and Swift-friendly, with:

By the end of this article, you’ll not only understand what SOLID stands for, but you’ll also be able to apply it to your Swift code like a boss 🧑‍💻💼

So let’s clean up that code and make it… well… SOLID 💪

🧑‍💼 S — Single Responsibility Principle (SRP)

“A class should have one, and only one, reason to change.”
– Uncle Bob, probably sipping espresso while refactoring

Let’s be real. We’ve all written classes that slowly morphed into the Swiss Army Knife of doom — handling UI updates, API calls, business logic, and maybe even scheduling your dog’s vet appointment. 🐶📅

That’s a violation of SRP.

🧠 Think of it like this:

Your toaster doesn’t also make coffee (unless you bought it from Wish). Each appliance does one job well. Your classes should too.

🚫 The “Before” — A God Class

class UserManager {
    func login(email: String, password: String) { /* ... */ }
    func logout() { /* ... */ }
    func fetchUserProfile() { /* ... */ }
    func saveSessionToKeychain() { /* ... */ }
    func clearUserDefaults() { /* ... */ }
}

Looks innocent at first glance… until you realize:

This baby’s got more responsibilities than a project manager on a Friday afternoon 😅

✅ The “After” — Divide and Conquer

class UserSessionManager {
    func login(email: String, password: String) { /* ... */ }
    func logout() { /* ... */ }
}

class UserProfileService {
    func fetchUserProfile() { /* ... */ }
}

class SessionStorage {
    func saveToKeychain() { /* ... */ }
    func clearUserDefaults() { /* ... */ }
}

Each class now has a clear, focused responsibility:

😍 Why This Matters

**Understanding Clean Architecture in iOS: A Beginner’s Guide** _Tired of Spaghetti Code? 🍝 Let’s Clean Things Up in iOS!_medium.comhttps://medium.com/swift-pal/understanding-clean-architecture-in-ios-a-beginners-guide-69d09b4883c4

🧩 O — Open/Closed Principle (OCP)

“Software entities should be open for extension but closed for modification.”
– Uncle Bob, again, probably while watching us struggle

This one’s all about not touching code that already works, because let’s be honest — the moment you touch a stable class, production crashes faster than you can say git blame.

🧠 What does this really mean?

Imagine you built a nice PaymentProcessor that supports Credit Card payments. Now your product manager comes in hot:

“Let’s add PayPal support!”
You shouldn’t have to _modify_ your PaymentProcessor. You should be able to _extend_ it.

🚫 The “Before” — A Switch Case Nightmare

class PaymentProcessor {
    func process(paymentMethod: String) {
        switch paymentMethod {
        case "creditCard":
            // Process credit card
        case "paypal":
            // Process PayPal
        default:
            // 🫠
        }
    }
}

Every time you add a new method, you touch this class and risk breaking what already worked. Bad idea.

✅ The “After” — Hello Protocols & Extension Magic ✨

protocol PaymentMethod {
    func processPayment()
}

class CreditCardPayment: PaymentMethod {
    func processPayment() {
        print("Processing credit card payment")
    }
}

class PayPalPayment: PaymentMethod {
    func processPayment() {
        print("Processing PayPal payment")
    }
}

class PaymentProcessor {
    func process(method: PaymentMethod) {
        method.processPayment()
    }
}

Now your PaymentProcessor doesn’t care what kind of payment it’s processing. You can extend functionality without touching the existing logic.

😍 Why This Is Awesome:

**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

🚗 L — Liskov Substitution Principle (LSP)

“Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.”
– Uncle Bob, probably after rage-debugging someone else’s inheritance mess

This one sounds fancy but boils down to a simple truth:

_Don’t create subclasses that can’t do what the base class promised._

🧠 Imagine this:

You have a class Bird. You create a subclass Penguin (cute, right?).

Now you call penguin.fly().

🐧 ❌💨 Uh-oh. Penguins don’t fly.

Boom — you just violated LSP and disrespected both Swift and nature.

🚫 The “Before” — Inheritance Gone Wrong

class Bird {
    func fly() {
        print("Flies in the sky")
    }
}

class Penguin: Bird {
    override func fly() {
        fatalError("Penguins can't fly!")
    }
}

You can technically replace Bird with Penguin, but now you’ve introduced a runtime crash.

That’s like ordering “coffee” and getting a punch in the face instead. ☕👊

✅ The “After” — Embrace Protocols & Specialization

protocol Bird {}

protocol Flyable {
    func fly()
}

class Sparrow: Bird, Flyable {
    func fly() {
        print("Sparrow flying high")
    }
}

class Penguin: Bird {
    func swim() {
        print("Penguin swimming")
    }
}

Now you only give fly() to birds that can actually, well… fly.

No more false promises. No more airborne penguins.

😍 Why This Helps:

One more thing: the LSP isn’t just about inheritance — in Swift, it’s often about conforming to protocols responsibly. If you’re promising behavior with a protocol, make sure the conforming type can actually fulfill that promise.

🎧 I — Interface Segregation Principle (ISP)

“Clients should not be forced to depend on interfaces they do not use.”
– Uncle Bob, while politely breaking up bloated protocols

🧠 In Plain English:

If you’re building a protocol, don’t throw in every method you might need one day.

Instead, split it into smaller, focused protocols so classes can choose only what they need — like à la carte instead of forced buffet 🍱

🦆 Enter the Duck…

Say hello to our friend:

protocol Bird {
    func fly()
    func swim()
    func sing()
}

Now imagine you’re building:

All these birds now have to implement methods they don’t need. That’s protocol abuse 😤

🚫 The “Before” — One Bloated Protocol

protocol Bird {
    func fly()
    func swim()
    func sing()
}

class Penguin: Bird {
    func fly() {
        // 🫠 Fake it till you crash
    }

    func swim() {
        print("Swimming")
    }

    func sing() {
        // 🫠 Not a singer, sorry
    }
}

We’re forcing penguins to lie on their resumes. That’s just wrong.

✅ The “After” — Split and Conquer

protocol Flyable {
    func fly()
}

protocol Swimmable {
    func swim()
}

protocol Singable {
    func sing()
}

class Sparrow: Flyable, Singable {
    func fly() { print("Flying") }
    func sing() { print("Singing") }
}

class Penguin: Swimmable {
    func swim() { print("Swimming") }
}

Now each bird does only what it’s meant to do. No more awkward method stubs. No more feature faking.

😍 Why It Matters:

**Dependency Injection in Swift: A Beginner-to-Advanced Guide** _🧪 Feeling tangled in singletons and tightly-coupled code? Learn how Dependency Injection in Swift can help you write…_medium.comhttps://medium.com/swift-pal/dependency-injection-in-swift-a-beginner-to-advanced-guide-b85378c6f8d2

🔌 D — Dependency Inversion Principle (DIP)

“High-level modules should not depend on low-level modules. Both should depend on abstractions.”
– Uncle Bob, while breaking toxic codependencies since the ’90s

🧠 Wait… What?

Basically: Don’t hard-code your dependencies.

Let your classes depend on protocols, not concrete implementations.

Think of it like plugging your charger into a wall socket, not directly into a power station. Because… boom. ⚡💥

🚫 The “Before” — Tight Coupling Drama

class RealUserService {
    func fetchUser() {
        print("Fetching real user from API")
    }
}

class ProfileViewModel {
    let userService = RealUserService() // 🚨 Tight coupling!

    func loadProfile() {
        userService.fetchUser()
    }
}

Looks okay… until you need to:

Suddenly, you’re stuck. This view model is too clingy — like an app that insists on notifications right now 🫠

✅ The “After” — DIP to the Rescue

protocol UserServiceProtocol {
    func fetchUser()
}

class RealUserService: UserServiceProtocol {
    func fetchUser() {
        print("Fetching real user from API")
    }
}

class MockUserService: UserServiceProtocol {
    func fetchUser() {
        print("Returning mock user data")
    }
}

class ProfileViewModel {
    let userService: UserServiceProtocol

    init(userService: UserServiceProtocol) {
        self.userService = userService
    }

    func loadProfile() {
        userService.fetchUser()
    }
}

Now ProfileViewModel doesn’t care who does the fetching — real, mock, or alien 👽 — as long as they follow the rules (aka the protocol).

🔗 Real-World Tip:

If you want to dive deeper into how this powers testability, modular architecture, and scalable systems, check out this full guide on Dependency Injection.

**Dependency Injection in Swift: A Beginner-to-Advanced Guide** _🧪 Feeling tangled in singletons and tightly-coupled code? Learn how Dependency Injection in Swift can help you write…_medium.comhttps://medium.com/swift-pal/dependency-injection-in-swift-a-beginner-to-advanced-guide-b85378c6f8d2

😍 Why DIP is a Game-Changer:

You’ve officially made it through all five SOLID principles — and your Swift code is now cleaner, friendlier, and way less of a mess 💅

🧠 Putting It All Together: A SOLID Mini App Example

Let’s imagine you’re building a simple Task Manager app.

Here’s how SOLID would sneak into your architecture like a stealthy design ninja 🥷:

🧑‍💼 S — Single Responsibility

You split responsibilities:

Each class does one thing — no juggling, no chaos.

🧩 O — Open/Closed

You support multiple task filters (e.g., by date, priority, tags).

Instead of modifying a giant switch case, you just add new filter types via protocol extensions.

protocol TaskFilter {
    func filter(_ tasks: [Task]) -> [Task]
}

Want a new filter? Just create a new class — no changes to existing code.

🚗 L — Liskov Substitution

You use a base TaskNotifier protocol and swap in:

🎧 I — Interface Segregation

Instead of one big fat protocol for TaskView, you break it into:

So a simple widget doesn’t have to pretend it can do everything. Win.

🔌 D — Dependency Inversion

Your TaskViewModel depends on TaskRepositoryProtocol, not a concrete class.

So in unit tests, you inject MockTaskRepository. In production, it’s CoreDataTaskRepository.

Your code? Blissfully unaware.

🧾 TL;DR — The SOLID Cheat Sheet

🍞 S — Single Responsibility

One class = one job.

_Real-life analogy_: A toaster that only toasts (not brews coffee).

👉 Keep your classes focused and clean.

🧱 O — Open/Closed

Your code should be open for extension, but closed for modification.

_Real-life analogy_: Add Lego blocks, don’t melt them to change shape.

👉 Add features without touching existing logic.

🐧 L — Liskov Substitution

Subtypes should behave like their base type.

_Real-life analogy_: Don’t make penguins fly — it’s unnatural and it breaks things.

👉 In Swift, favor protocols and make sure conformance actually makes sense.

🎤❌ I — Interface Segregation

No bloated protocols, please.

_Real-life analogy_: Don’t make a duck sing if it wasn’t born to croon.

👉 Split protocols so classes only implement what they need.

🔌 D — Dependency Inversion

High-level modules shouldn’t depend on concrete implementations.

_Real-life analogy_: Plug into the wall socket, not the power grid.

👉 Depend on protocols and inject dependencies to keep things testable and flexible.

🙌 Wrapping Up

SOLID principles aren’t just academic fluff — they’re battle-tested ideas that help you write:

So next time you feel your code is spiraling out of control, take a deep breath and ask:

“Am I being SOLID right now?” 😌

If not — you know where to start.

🎉 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