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🤓🔥

👋 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:
- ✅ Keep classes focused and manageable
- 🔄 Make changes without breaking half the codebase
- 🧪 Test with ease (and fewer tears)
- 📦 Build reusable and extendable modules
- 💡 Write code your future self won’t hate
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:
- ✍️ Real-world examples from iOS apps
- 🧩 Analogies you’ll actually remember
- 😂 Sprinkles of humor to keep you awake
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:
- It’s managing networking
- Handling storage
- Controlling session logic
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:
- UserSessionManager: Auth stuff
- UserProfileService: Data fetching
- SessionStorage: Persistence
😍 Why This Matters
- 🔄 Easier to modify without breaking unrelated things
- 🧪 Unit tests are focused and clean
- 🔍 Debugging becomes way less painful
- 🤝 Plays nicely with Clean Architecture (psst… you might like this)
**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:
- 🧼 Cleaner, modular code
- ➕ Add new features without fear
- 🧪 Test new methods independently
- 🧩 Pairs beautifully with modular architecture (more on that here)
**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:
- 🧼 Your code is logically accurate (no weird behaviors)
- 🧪 Subclasses become predictable and safe to use
- ⚠️ You avoid crashing apps with surprise fatalErrors
- 🙌 Bonus: Pairs beautifully with Interface Segregation — which is up next!
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:
- 🐦 Sparrow: Can fly and sing
- 🐧 Penguin: Can swim
- 🦆 RubberDuck: Can float but can’t sing (or even quack on command)
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:
- 🎯 Keeps your protocols focused and meaningful
- 🧪 Easier to mock/test — you test only what’s needed
- 🧩 Plays very well with dependency injection (wink wink, more on that here)
**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:
- ✅ Write unit tests
- 🔁 Switch to a mock service
- 🔍 Add analytics or logging
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:
- 🔧 Swap dependencies easily (mock vs. real)
- 🧪 Unit tests become a breeze
- 🧱 Decouples layers in your app (hellooo Clean Architecture 👋)
- 🌱 Makes your codebase future-proof
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:
- TaskRepository handles data storage
- TaskService manages task business logic
- TaskViewModel formats data for the UI
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:
- LocalNotificationSender
- SlackNotificationSender
- Both work seamlessly because they follow the same contract — no surprises 🎁
🎧 I — Interface Segregation
Instead of one big fat protocol for TaskView, you break it into:
- TaskListDisplayable
- TaskDetailPresentable
- TaskCompletable
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:
- Clean ✅
- Modular ✅
- Testable ✅
- Maintainable ✅
- Swift code that won’t make you rage-quit at 2AM.
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! 🚀
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
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read