SOLID Principles in iOS with Swift Code Examples
A guide to writing cleaner, more maintainable, and scalable Swift code using SOLID principles.

Most iOS apps don’t become hard to maintain because of bad syntax or wrong APIs. They become hard because the codebase slowly turns rigid, fragile, and difficult to extend. That usually happens when SOLID is ignored. Not intentionally. Just gradually!
What’s inside?
What SOLID really means in the context of Swift
Practical examples you can relate to in iOS projects
Common mistakes developers make while trying to “follow” SOLID
How SOLID improves testing, scalability, and team collaboration
SOLID iOS Interview questions to check your real understanding
Why SOLID matters in Swift projects
Swift gives us powerful tools: protocols, extensions, value types, generics. But without clear design principles, even modern Swift code can become messy.
SOLID helps you decide:
When to create a protocol
When to split a class
How to structure dependencies
How to make features easier to extend later
It is less about rules and more about writing code that survives growth. We need to use SOLID Principles for Better Swift Code in iOS Apps.
S: Single Responsibility Principle
A class should have only one reason to change.
In many iOS apps, ViewControllers often violate this. They fetch data, handle UI, manage state, perform navigation, and sometimes even format data.
When one class does too much, any small change risks breaking something unrelated.
Applying SRP in Swift often means:
Moving networking to a service layer
Using ViewModels for presentation logic
Keeping ViewControllers focused on UI only
This separation makes the code easier to read, test, and modify.
Example:
Problem: One class is doing too many things.
class UserViewController: UIViewController {
func fetchUser() { /* API call */ }
func formatDate() -> String { /* formatting */ }
func saveToCache() { /* caching */ }
}
Better: Split responsibilities.
class UserService {
func fetchUser() { }
}
class DateFormatterService {
func formatDate() -> String { "" }
}
class UserViewController: UIViewController {
let service = UserService()
}
Now UI, networking, and formatting are separated.
O: Open/Closed Principle
Software entities should be open for extension but closed for modification. Instead of editing existing code every time a new requirement comes, you design it so new behavior can be added without touching old logic. In Swift, this is commonly achieved using protocols and protocol-oriented design.
For example, instead of modifying a payment class to support new methods, you define a PaymentMethod protocol and add new implementations as needed. Your existing code stays untouched, reducing risk.
Example:
Problem: Modifying existing class for every new case.
class PaymentProcessor {
func pay(type: String) {
if type == "card" { }
else if type == "cash" { }
}
}
Better: Extend with protocols.
protocol PaymentMethod {
func pay()
}
class CardPayment: PaymentMethod {
func pay() { }
}
class CashPayment: PaymentMethod {
func pay() { }
}
class PaymentProcessor {
func process(_ method: PaymentMethod) {
method.pay()
}
}
Add new payment types without touching old code.
L: Liskov Substitution Principle
Subtypes must be replaceable with their base types without breaking behavior. This issue appears when subclasses change expected behavior in surprising ways.
For example, if a subclass overrides a method but changes what it is supposed to do, parts of the app relying on the original behavior start failing.
In Swift, careful protocol design and avoiding unnecessary inheritance help maintain this principle. Favor composition over inheritance whenever possible.
Example:
Problem: Subclass changes expected behavior.
class Bird {
func fly() { }
}
class Penguin: Bird {
override func fly() {
fatalError("Penguins can't fly")
}
}
This breaks expectations.
Better: Design with proper abstraction.
protocol Bird { }
protocol FlyingBird: Bird {
func fly()
}
class Sparrow: FlyingBird {
func fly() { }
}
class Penguin: Bird { }
No broken assumptions.
I: Interface Segregation Principle
Clients should not be forced to depend on methods they do not use. Large protocols are common in iOS projects. A single delegate or manager protocol with many responsibilities forces conforming classes to implement unnecessary methods. Breaking large protocols into smaller, focused ones keeps implementations clean and reduces coupling. Swift protocols make this very natural when designed thoughtfully.
Example:
Problem: Fat protocol.
protocol Worker {
func code()
func test()
func deploy()
}
Not everyone does all three.
Better: Split protocols.
protocol Coder {
func code()
}
protocol Tester {
func test()
}
protocol Deployer {
func deploy()
}
Classes adopt only what they need.
D: Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions. Instead of a ViewModel directly depending on a concrete API service, it should depend on a protocol.
This makes it easier to:
Replace implementations
Write unit tests with mock services
Modify underlying systems without affecting business logic
This is one of the most powerful principles for testable Swift code.
Example:
Problem: Tight coupling to concrete class.
class UserViewModel {
let api = APIService()
}
Hard to test and replace.
Better: Depend on abstraction.
protocol APIServiceProtocol {
func fetch()
}
class APIService: APIServiceProtocol {
func fetch() { }
}
class UserViewModel {
let api: APIServiceProtocol
init(api: APIServiceProtocol) {
self.api = api
}
}
Now you can inject a mock service for testing.
Common mistakes when applying SOLID
Many developers try to apply SOLID by creating too many protocols and abstractions too early. That leads to overengineering.
SOLID is not about adding layers. It is about removing unnecessary coupling and making future changes easier. Start simple. Introduce abstractions only when the need becomes clear.
How SOLID improves real iOS development
When SOLID is applied correctly:
Features are easier to extend
Refactoring becomes safer
Unit testing becomes simpler
Team members understand the code faster
Bugs from side effects reduce significantly
Over time, the project feels organized rather than chaotic.
Frequently Asked SOLID iOS Interview Questions
If you truly understand SOLID, you should be able to confidently explain and apply these in real scenarios. Try answering these yourself.
What problem does the Single Responsibility Principle actually solve in large iOS codebases?
How would you apply the Open/Closed Principle using protocols in Swift?
Can you share a real example where violating Liskov Substitution caused bugs?
Why do large protocols become a maintenance issue, and how does Interface Segregation fix that?
How does Dependency Inversion make unit testing easier in Swift projects?
How do SOLID principles influence your choice between classes, structs, and protocols?
Which SOLID principle do you think is most commonly violated in iOS apps and why?
Take time to articulate these answers from what you learned in this article. That exercise alone will prepare you far better than memorizing definitions.
SOLID is a survival guide for growing codebases. Swift gives you the tools. SOLID tells you how to use them wisely. When followed properly, your code stops feeling fragile and starts feeling scalable, readable, and future-proof.
For further guidance or consultancy, feel free to connect at hello@sajidhasan.com





