SOLID Principles for iOS Apps
SOLID is a group of principles that lead you to write clear and organized code without additional effort. Learn how to apply it to your SwiftUI iOS apps. By Ehab Amer.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SOLID Principles for iOS Apps
40 mins
- Getting Started
- Understanding SOLID’s Five Principles
- Single Responsibility
- Open-Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
- Auditing the Project
- Invoking the Single Responsibility Principle
- Using the New Persistence
- Implementing the Open-Closed Principle
- Creating the Enum
- Cleaning up the Reports
- Updating ContentView.swift
- Adding Weekly Reports
- Applying Dependency Inversion
- Removing the Core Data Dependency
- Seeing Your Changes in Action
- Simplifying the Reports Datasource Interface
- Refactoring ExpensesView
- Adding Interface Segregation
- Splitting up Protocols
- Implementing Liskov Substitution
- Auditing the App Again
- Where to Go From Here?
To write a great app, you not only have to come up with a great idea, but you also need to think about the future. The flexibility to adapt, improve and expand the features in your app quickly and efficiently is critical. Whether you’re working in a team or by yourself, how you write and organize your code will make a huge difference in maintaining your code in the long run. That’s where the SOLID principles come in.
Imagine you have a mess of paper on your desk. You might be able to find any given paper quickly but when someone else looks for something, it’s hard to find what they need. Your code is much like your desk, except that it’s even more likely that other people will need something from it.
If your desk were neat and organized, on the other hand, you’d have what developers refer to as clean code: Code that’s clear about what it does, maintainable and easy for others to understand. SOLID is a collection of principles that help you write clean code.
In this tutorial, you’ll:
- Learn the five principles of SOLID.
- Audit a working project that didn’t follow them.
- Update the project and see how much of a difference SOLID makes.
Since your goal is to learn how to improve your existing code, this SOLID tutorial assumes you already have a grasp of the basics of Swift and iOS.
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Unzip it and open ExpenseTracker.xcodeproj in the starter folder.
The app allows users to store their expenses so they can track how much they spend daily or monthly.
Build and run the app. Try adding a few entries yourself:
The app works, but it isn’t in the best shape and it doesn’t follow the SOLID principles. Before you audit the project to identify its shortcomings, you should understand what those principles are.
Understanding SOLID’s Five Principles
The five principles of SOLID don’t directly relate to each other, but they all serve the same purpose: keeping code simple and clear.
These are the five SOLID principles:
- Single Responsibility
- Open-Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
Here’s an overview of what each principle means:
Single Responsibility
A class should have one, and only one, reason to change.
Each class or type you define should have only one job to do. That doesn’t mean you can only implement one method, but each class needs to have a focused, specialized role.
Open-Closed
Software entities, including classes, modules and functions, should be open for extension but closed for modification.
This means you should be able to expand the capabilities of your types without having to alter them drastically to add what you need.
Liskov Substitution
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
In other words, if you replace one object with another that’s a subclass and this replacement could break the affected part, then you’re not following this principle.
Interface Segregation
Clients should not be forced to depend upon interfaces they do not use.
When designing a protocol you’ll use in different places in your code, it’s best to break that protocol into multiple smaller pieces where each piece has a specific role. That way, clients depend only on the part of the protocol they need.
Dependency Inversion
Depend upon abstractions, not concretions.
Different parts of your code should not depend on concrete classes. They don’t need that knowledge. This encourages the use of protocols instead of using concrete classes to connect parts of your app.
Auditing the Project
The starter project breaks all five principles. It does work and, at a glance, it doesn’t feel very complicated or seem to require a lot of effort to maintain. If you look more closely, however, you’ll see that’s not true.
The easiest principle to spot being broken is dependency inversion. There are no protocols at all in the project, which means there are no interfaces to segregate, either.
Open AppMain.swift. All the Core Data setup takes place there, which doesn’t sound like a single responsibility at all. If you wanted to reuse the same Core Data setup in a different project, you’d find yourself taking pieces of code instead of the whole file.
Next, open ContentView.swift. This is the first view in the app, where you choose which kind of expense report you want to show: daily or monthly.
Say you wanted to add a report for the current week. With this setup, you’d need to create a new report screen to match DailyExpensesView
and MonthlyExpensesView
. Then, you’d alter ContentView
with a new list item and create a new DailyReportsDataSource
.
That’s quite messy and a lot of work just to add a variant of the functionality you already have. It’s safe to say that this is a violation of the open-closed principle.
Adding unit tests won’t be easy since almost all the modules are connected.
Additionally, if at some point you wanted to remove CoreData and replace it with something else, you’d need to change almost every file in this project. The simple reason for that is because everything is using the ManagedObject
subclass, ExpenseModel
.
Overall, the project gives minimum room for alteration. It focuses on the initial requirements and doesn’t allow for any future additions without considerable changes to the project as a whole.
Now, you’ll learn how you can apply each principle to clean up the project and see the benefits refactoring offers your app.
Invoking the Single Responsibility Principle
Open AppMain.swift again and look at the code. It has four main properties:
- container: The main persistence container for your app.
- previewContainer: A preview/mock container to use for SwiftUI previews. This eliminates the need for an actual database.
-
previewItem: This is a single item for previewing in
ExpenseItemView
. -
body: The body of the app itself. This is
AppMain
‘s main responsibility.
The only property you really need to have here is body
— the other three are out of place. Remove them and create a new Swift file named Persistence.swift in the Storage group.
In the new file, define a new struct named PersistenceController
:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
}
This persistence controller is responsible for storing and retrieving data. shared
is a shared instance you’ll use across the app.
Within the new structure, add this property and initializer:
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "ExpensesModel")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(
fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
The parameter in the initializer defines whether the container will be temporary in memory or an actual container with a database file stored on the device. You’ll need in-memory storage for showing fake data in SwiftUI previews.
Next, define two new properties you’ll use for SwiftUI previews:
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for index in 1..<6 {
let newItem = ExpenseModel(context: viewContext)
newItem.title = "Test Title \(index)"
newItem.date = Date(timeIntervalSinceNow: Double(index * -60))
newItem.comment = "Test Comment \(index)"
newItem.price = Double(index + 1) * 12.3
newItem.id = UUID()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
static let previewItem: ExpenseModel = {
let newItem = ExpenseModel(context: preview.container.viewContext)
newItem.title = "Preview Item Title"
newItem.date = Date(timeIntervalSinceNow: 60)
newItem.comment = "Preview Item Comment"
newItem.price = 12.34
newItem.id = UUID()
return newItem
}()
preview
is another instance of PersistenceController
similar to shared
, but the container
inside preview
doesn't read from a database file. Instead, it contains five expense entries that are hard-coded and stored in memory.
previewItem
is a single stub instance of ExpenseModel
, which is identical to the one you removed from AppMain.swift.
Why do all this? Currently, all of your app's classes use ExpenseModel
directly. You can't create an instance of this class without defining a persistent container. It's best to group properties related to Core Data setup and previews together.
Later in the refactoring, you'll be able to completely remove those preview support objects and replace them with something more organized.
static
properties are lazy by default. Until you use them, they'll never be allocated in memory. Because you only use them in previews, you shouldn't worry about them existing in memory at all.