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.

4.6 (28) · 3 Reviews

Download materials
Save for later

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.

ExpenseTracker Project Navigation

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:

Daily report with two entries added

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.


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.

Note: When you refactor an existing project, it isn’t important to follow the SOLID principles in order. Rather, it’s important to use them correctly.

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.

Initial view, where you choose the type of report to display

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:

  1. container: The main persistence container for your app.
  2. previewContainer: A preview/mock container to use for SwiftUI previews. This eliminates the need for an actual database.
  3. previewItem: This is a single item for previewing in ExpenseItemView.
  4. 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)" = Date(timeIntervalSinceNow: Double(index * -60))
    newItem.comment = "Test Comment \(index)"
    newItem.price = Double(index + 1) * 12.3 = UUID()
  do {
  } 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" = Date(timeIntervalSinceNow: 60)
  newItem.comment = "Preview Item Comment"
  newItem.price = 12.34 = 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.

Note: 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.