Logo

dev-resources.site

for different kinds of informations.

Object-Oriented Programming in Swift

Published at
1/10/2025
Categories
swift
ios
oop
mobile
Author
raphacmartin
Categories
4 categories in total
swift
open
ios
open
oop
open
mobile
open
Author
12 person written this
raphacmartin
open
Object-Oriented Programming in Swift

Introduction

It has lovers and haters, but Object-Oriented Programming (OOP) is decidedly a subject that market expects you to master as a software engineer.

What we learn in school vs what happens in industry

What we learn in school vs what happens in industry - r/ProgrammerHumor

Theory

Literature says that it's a popular paradigm used in software development to organize code into modular and reusable objects. Swift, being an OOP language, supports four pillars of OOP: abstraction, inheritance, encapsulation, and polymorphism. In this post, we will try to explore these pillars:


Abstraction 

Abstraction is the process of hiding implementation details and exposing only the "abilities" of the object. Think in Abstraction as not telling to the others how you do things, but instead what you are capable of doing.

Imagine that for cold starting a car, your car needs to turn on the battery, start the starter motor and then pump gas. It does all of these steps when you press the "Start" button.

Start/Stop button of a car

Now, imagine that you'll represent this process in your code:

class Car {
  var name: String

  init(name: String) {
    self.name = name
  }

  func start() {
    turnBatteryOn()
    startStarterMotor()
    pumpFuel()
  }

  func turnBatteryOn() { /../ }

  func startStarterMotor() { /../ }

  func pumpFuel() { /../ }
}

let car = Car(name: "Ferrari")
car.start()
Enter fullscreen mode Exit fullscreen mode

The Abstraction concept of OOP tells us that, whoever holds an instace of Car shouldn't "know" that this object is capable of, for example, turning the battery on, or pumping fuel, since it's not relevant for the software requirements. What is relevant is that, the car can start.

In Swift, we can use protocols to achieve abstraction. A protocol defines a set of methods and properties that a class, struct, or enum can adopt without needing to specify how they work. This allows us to declare what actions an object can perform, without revealing how it does so. This is similar to interfaces in languages like Java or C#.

Let's take a look at an example:

// MARK: - Abstraction
protocol Vehicle {
  var name: String { get set }
  func start()
}

// MARK: - Concrete implementation
class Car: Vehicle {
  var name: String

  init(name: String) {
    self.name = name
  }

  func start() {
    turnBatteryOn()
    startStarterMotor()
    pumpFuel()
  }

  func turnBatteryOn() { /../ }

  func startStarterMotor() { /../ }

  func pumpFuel() { /../ }
}

// MARK: - Using the code
let car: Vehicle = Car(name: "Ferrari")
car.start()
// If you try this:
car.startStarterMotor()
// You'll receive: protocol 'Vehicle' has no instances of `startStarterMotor`. 
// This ensures that only intended methods are accessible.
Enter fullscreen mode Exit fullscreen mode

In the above code, we define a protocol Vehicle that has a name property and a start() method. We then create a Car class that conforms to the Vehicle protocol. The Car class has a name property and implements the start() method. We can now create instances of the Car class and call the start() method on them.

If we have now other types of Vehicles, that starts in a different way, our code that expects a Vehicle object is already compatible:

class Boat: Vehicle {
  var name: String

  init(name: String) {
    self.name = name
  }

  func start() {
    turnBatteryOn()
    // Look that this boat doesn't have a starter motor. You need to manually pull a rope to start it, but this doesn't matter to whoever is using the object.
    pullStarterRope()
    pumpFuel()
  }

  func turnBatteryOn() { /../ }

  func pullStarterRope() { /../ }

  func pumpFuel() { /../ }
}

let boat: Vehicle = Boat(name: "Fishing Boat")
// Just start the same way it'd start a car
boat.start()
Enter fullscreen mode Exit fullscreen mode

Inheritance

Maybe the most known OOP principle, inheritance is pretty straight-forward: children inherits behaviors from parents.

To understand it better, we need to bring what a child, a parent and a behavior is in a software code context. In Swift, children and parents are object types, more specifically Classes. In this language it's not possible to do inheritance between struct objects.
Behaviors are the properties and/or the functions of these classes. Let's see an example:

class Pet {
  var name: String
  init(name: String) {
    self.name = name
  }
  func eatFood() {
    print("The pet is eating the food")
  }
}

class Dog: Pet {
}

class Cat: Pet {
}

let dog = Dog(name: "Joey")
dog.eatFood()

let cat = Cat(name: "Phoebe")
cat.eatFood()
Enter fullscreen mode Exit fullscreen mode

In our app, we have Dogs and Cats. Since both of them share some behaviors (as having a name and eating food) we can use the concept of inheritance to improve our code, writting the logic only one time and sharing with them. If we have a bug inside our eatFood() function, we have only one place to fix it for the entire app. That's the main benefit of using inheritance in your code.

The down-side

Even though it's considered one of the best practices of OOP, using inheritance can bring problems to your development cycle.
The concept leads to a tight coupling in your code, making it less flexible, and flexibility is something very important during software development, because softwares are not static things, but instead "living" beings that should be able to change due time.

Imagine having a Car type in your code that consumes fuel. You have also other types of Vehicles that also consumes fuel, so you decide to use inheritance to obtain the benefits of avoid repetition:

class Vehicle {
    var speed: Double = 0.0
    var fuelLevel: Double = 0.0

    func fillTank() {
        fuelLevel += 10
    }

    func throttle() {
        if fuelLevel > 0 {
            speed += 10
            fuelLevel -= 5
        } else {
            print("Out of fuel!")
        }
    }
}

class Car: Vehicle {
}

class Motorcycle: Vehicle {
}
Enter fullscreen mode Exit fullscreen mode

That works perfectly, in 2012 when you wrote this piece of code. But now your app should also support hybrid cars, that consumes fuel and electricity. If you update your Vehicle.throttle function to support electricity, it will affect all the other types that shouldn't support it. So now, you have a huge work to do in order to support it.

A better approach would be using the concept of composition instead of inheritance for this case, but that's a subject for another post.


Encapsulation

Encapsulation is the process of hiding the implementation details of an object from the outside world. But differently from the abstraction, that says that you have to create abstract types and make your code depend on it, encapsulation is more about the concrete types (classes and structs). So, in order to "hide" things in concrete types, you need to use the language access control modifiers.

Swift’s access control modifiers are essential for encapsulation, limiting access to properties and methods based on scope:

  • public: Accessible from any module.
  • internal (default): Accessible within the same module.
  • fileprivate: Accessible within the same file.
  • private: Only accessible within the same class or struct.

To makes things easier and learning OOP concepts only, let's consider only private and internal, being this last the default modifier (the one the compiler selects when you don't specify anything)

Here's an example:

class Person {
  private var name: String
  init(name: String) {
    self.name = name
  }
  func greet() {
    print("Hello, my name is \(name)")
  }
}

let person = Person(name: "John")
person.greet() // Output: "Hello, my name is John"
person.name = "Mike" // Error: 'name' is inaccessible due to 'private' protection level
Enter fullscreen mode Exit fullscreen mode

In the above code, we define a Person class that has a private name property and an internal greet() method (we set it as internal by not specifying an accessor). We use the private access control modifier to make the name property inaccessible from outside the Person class scope. We can create an instance of the Person class and call the greet() method on it, but we cannot access the name property directly.

That's important to guarantee that whoever holds an instance of your object, can do only things that they are supposed to.

Let's take a look in another example to make it clearer:

enum Rate {
    static var hourlyRate: Double = 10
}

class Employee {
    var workedHours: Double
    private var salary: Double = 0

    init(workedHours: Double) {
        self.workedHours = workedHours
    }

    func calculateSalary() {
        salary = workedHours * Rate.hourlyRate
    }

    func getSalary() -> Double {
        return salary
    }
}

let employee = Employee(workedHours: 40)
employee.calculateSalary()
print(employee.getSalary()) // ✅ Will display the correct salary based in the worked hours and rate

employee.workedHours = 48 // ✅ You can change how much the employee worked
employee.calculateSalary()
print(employee.getSalary()) // ✅ Displays correctly

employee.salary = 500 // 🚫 You'll receive "'salary' is inaccessible due to 'private' protection level". You cannot pass by the calculation logic
Enter fullscreen mode Exit fullscreen mode

Polymorphism

Polymorphism means literally "many forms". In OOP, it's the ability of several objects of same type having totally different behaviors.

This concept could be confusing for beginners, you can find it sometimes similar to abstraction, and also to inheritance. And that's because you need to use both of these previous pillars to achieve polymorphism.

To implement Polymorphism, the commom type between different objects should be an abstraction. In Swift, we use protocols for that (in other languages you can use abstract classes also, but Swift doesn't have it), and also inheritance of classes to implement the different behaviors.

Let's see an example:

protocol Shape {
  func area() -> Double
}

class Rectangle: Shape {
  var width: Double
  var height: Double
  init(width: Double, height: Double) {
    self.width = width
    self.height = height
  }
  func area() -> Double {
    return width * height
  }
}

class Circle: Shape {
  var radius: Double
  init(radius: Double) {
    self.radius = radius
  }
  func area() -> Double {
    return Double.pi * radius * radius
  }
}

// Here we have an array of `Shape`s. All the array elements are the same type
let shapes: [Shape] = [Rectangle(width: 10, height: 10), Circle(radius: 10)]

for shape in shapes {
    // Here, the `shape` variable is always considered the type `Shape` by the compiler in all loops, but what the `area()` function does is different each time
    print(shape.area())
}
Enter fullscreen mode Exit fullscreen mode

This brings flexibility to the code, since you can create new specific types and pass it to every place that expects a Shape.


Wrapping up

So recaping, Object-Oriented Programming (OOP) revolves around four key principles:

  • Abstraction: Hides implementation details, exposing only what’s essential.
  • Encapsulation: Protects internal state by restricting external access.
  • Inheritance: Shares common behaviors across related types.
  • Polymorphism: Allows objects to take on multiple forms, enhancing flexibility.

In a job interview or in your daily work, mastering OOP always is a good thing. As mentioned in the introduction, this set of concepts has lovers and haters. But in my entire career, all the engineers I met that stands against OOP are the ones that better understanding of its concepts. So studying it is not only a good idea, but mandatory in my opinion. And for the market, if you are an intermediate or senior professional, excelling it will be expected from you.

swift Article's
30 articles in total
Favicon
MVVM directory structure for larger project
Favicon
What Do All iOS Engineers Keep Forgetting?
Favicon
Mastering 4 way Infinite Scroll in SwiftUI!
Favicon
iOS Background Modes: A Quick Guide
Favicon
Debugging in Xcode: Tips to Save Your Time 🛠️
Favicon
I created a cool SwiftUI library!
Favicon
Optimizing iOS App Performance
Favicon
The Ultimate Guide to iOS Development: Closures (Part 7)
Favicon
Boost Your App’s Performance with Lazy Stacks in SwiftUI
Favicon
The Ultimate Guide to iOS Development: Collections (Part 6)
Favicon
Access Control Levels in Swift
Favicon
Meet swift-api-client
Favicon
The Ultimate Guide to iOS Development: Functions (Part 5)
Favicon
Unlocking Face ID Integration: Boost Security and User Experience 🚀
Favicon
Unlocking Business Growth with iOS Development Services
Favicon
MVC vs MVVM: A Real-Life iOS Interview Insight
Favicon
The Ultimate Guide to iOS Development: Control Flow (Part 4)
Favicon
Swift: A Journey into Open Source Excellence
Favicon
Object-Oriented Programming in Swift
Favicon
A Drum machine for iOS
Favicon
The Ultimate Guide to iOS Development: Variables, Data Types, and Basic Operations in Swift (Part 3)
Favicon
The Ultimate Guide to iOS Development: From Programming Basics to Building Your First App (Part 2)
Favicon
Top 5 iOS App Templates to Kickstart Your Next Project
Favicon
The Ultimate Guide to iOS Development: From Programming Basics to Building Your First App (Part 1)
Favicon
Cosmos is hiring a senior iOS eng
Favicon
How To Add Multiple Modules In The Swift Package Manager
Favicon
Enhance Debugging in Swift with #file, #line, and #function
Favicon
Directly store value using "if expression" - Swift
Favicon
Day 3: Mull It Over | Advent of Code 2024 | Swift | 中文
Favicon
Property Wrappers in Swift: From Basics to Advanced Techniques

Featured ones: