Updated: September 9, 2024
SOLID Principles in iOS Development

SOLID are principles that lead you to write great code without additional effort.
With great application comes great responsibility. It means that the code base should be flexible, expandable without much effort, and easy to test. SOLID principles are the exact things that let you support the top level of quality.
In this article, I'll explain the meaning of each principle.
Five SOLID principles:
Single responsibility
There should never be more than one reason for a class to change.
In other words, every class should have only one responsibility. Each class needs to have a focused, specialized role.
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
initialSetup()
requestCurrentData()
}
func initialSetup() {
//Here is code for adding views and setting constraints
}
func requestCurrentData() {
//Here is code for network request
}
}
This is how the Single Responsibility principle is violated. We can’t use a single class for presenting UI and Network requests. Instead of this, we should use another class for Network requests.
class ViewController: BaseViewController {
var networkManager: NetworkManagerProtocol?
override func viewDidLoad() {
super.viewDidLoad()
initialSetup()
requestCurrentData()
}
func initialSetup() {
//Here is code for adding views and setting constraints
}
func requestCurrentData() {
networkManager.requestData()
}
}
Open-closed
Software entities should be open for extension but closed for modification.
An entity can allow its behavior to be extended without modifying its source code.
For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
A module will be said to be closed if it is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding).
Hotly disputed principle as it is not clear how to use this one. Someone provides an example of using an interface type instead of a particular class type. But it is the dependency inversion principle.
Instead of it, let's use protocols extensions. For example:
class FoxMiner {
func mine() -> String {
return "Fox don't know what to mine..."
}
}
let fox = FoxMiner()
print(fox.mine()) //Output: Fox don't know what to mine...
So we want to change the mine
function without changing FoxMiner class realization. Let’s create two protocols: DiamondMinerProtocol and RubyMinerProtocol.
protocol DiamondMinerProtocol {}
protocol RubyMinerProtocol {}
Now we will create extensions for these protocols:
extension DiamondMinerProtocol {
func mine() -> String {
return "Diamond +1"
}
}
extension RubyMinerProtocol {
func mine() -> String {
return "Ruby +1"
}
}
Now let's return to our FoxMiner class:
class FoxMiner: DiamondMinerProtocol, RubyMinerProtocol {
func mine() -> String {
return "Fox doesn't know what to mine..."
}
}
Now, if we want FoxMiner class realization, then we use FoxMiner type variable or variable without type:
let fox = FoxMiner() //or let fox: FoxMiner = FoxMiner()
print(fox.mine()) //Output: Fox don't know what to mine...
If we change variable fox type to DiamondMinerProtocol type:
let fox: DiamondMinerProtocol = FoxMiner()
print(fox.mine()) //Output: Diamond +1
And RubyMinerProtocol:
let fox: RubyMinerProtocol = FoxMiner()
print(fox.mine()) //Output: Ruby +1
Also, you can change function call without creating a new instance of the class:
let fox = FoxMiner()
print(fox.mine()) //Output: Fox don't know what to mine...
print((fox as DiamondMinerProtocol).mine()) //Output: Diamond +1
print((fox as RubyMinerProtocol).mine()) //Output: Ruby +1
That's how this principle could be used in Swift.
Liskov substitution
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
Liskov substitution is based on the concept of "substitutability"—a principle in object-oriented programming stating that an object (such as a class) and a sub-object (such as a class that extends the first class) must be interchangeable without breaking the program.
This principle is often used in iOS development as we love to use BaseClasses.
For example, we have an app with a similar design on every screen. This means we can create some BaseClass that will set up all UI, and we will just inherit from that BaseClass instead of copy-pasting the same code in every ViewController.
Interface segregation
Clients should not be forced to depend upon interfaces that they do not use.
The interface segregation principle (ISP) splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces. ISP is intended to keep a system decoupled and thus easier to refactor, change, and redeploy.
This is too easy — just don't make supermassive protocols (interfaces).
Dependency Inversion
Depend upon abstractions, not concretions.
When following this principle, the conventional dependency relationships established from high-level policy-setting modules to low-level dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details.
I've already mentioned this principle in open-closed. This one is very useful when we are working with third-party libraries. For example, we have FirebaseAnalytics for events logging, and after some time, we decide to change it to AppDynamics. To make it less painless, we should use this principle.
class SomeViewController {
...
@objc
func userTapCancel() {
Analytics.logEvent("Cancel", parameters: nil)
}
}
But AppDynamics has an API that is different from Firebase one. So instead of changing log events everywhere, we should use abstraction:
public protocol AnalyticsAdapting: AnyObject {
var identifier: String { get }
func send(eventName: String, parameters: [String: Any]?)
}
And implementation:
public class EventsServiceAdapter: AnalyticsAdapting {
private var options: FirebaseOptions
public init(options: FirebaseOptions) {
self.options = options
}
public var identifier: String {
return "firebase_\(options.clientID ?? "")"
}
public func send(eventName: String, parameters: [String : Any]?)
{
Analytics.logEvent(eventName, parameters: parameters)
}
}
Now in our app, we will send events through the adapter:
class SomeViewController {
var analytics: AnalyticsAdapting?
...
@objc
func userTapCancel() {
analytics.send(eventName: "Cancel", parameters: nil)
}
}
If we will move from FirebaseAnalytics to AppDynamics, it means that changes will be only in EventsServiceAdapter.
Conclusion
In this article, I've tried to describe all 5 principles of SOLID and given examples. SOLID principles are also great to use when you're creating app architecture. These principles should be used for simplifying your code life, not making it hell.
If you need more expertise in developing and supporting iOS applications, you can also get free consultation. Mad Devs is always ready to provide best mobile app development practices.