Object-Oriented Programming in Swift

Object-Oriented Programming in Swift

Object-oriented programming can feel intimidating at first, especially when you are trying to learn a language like Swift that looks modern, polished, and a little different from the older OOP languages many tutorials use as examples. You may hear words like class, object, inheritance, encapsulation, abstraction, and polymorphism, and it can sound like a theory lesson instead of something that helps you build real apps. But once you see how these ideas work in Swift, everything starts to feel much more practical. Swift does not just let you use object-oriented programming; it encourages you to write clean, safe, readable code with it.

The best way to understand OOP in Swift is not to memorize definitions, but to look at how real code behaves. Think of OOP as a way to organize your app around things. A thing can be a user, a car, a book, a bank account, a screen, or a product in an online store. Each thing has data and behavior. A book has a title and author, and it can be opened, borrowed, or returned. A bank account has a balance, and it can deposit or withdraw money. A user has a name, email, and login actions. OOP helps you model these things in a way that feels natural to humans.

Swift gives you tools to build these models in a clean way. It has classes, structs, enums, protocols, extensions, access control, and a strong type system. Even though classes are the classic OOP feature, Swift also gives a lot of power to structs and protocols, so the language is not just about inheritance and parent-child relationships. That is part of what makes Swift elegant. It lets you use OOP when it makes sense, but it also supports other styles that keep code simpler and safer.

In this article, we will walk through OOP in Swift step by step. We will keep things simple, practical, and realistic. You will see how to create classes and objects, how to use properties and methods, how inheritance works, how to control access, how to build initializers, how to override behavior, how polymorphism appears in Swift, and how protocols fit into the picture. By the end, you should feel much more comfortable reading and writing OOP code in Swift.

What Object-Oriented Programming Really Means

Object-oriented programming is a style of programming that organizes software around objects. An object is a bundle of data and behavior. In real life, objects are everywhere. A car has properties like color, brand, and speed. It also has behaviors like start, stop, and accelerate. A person has properties like name and age, and behaviors like speak, walk, and work. OOP tries to model those real-world ideas in code.

In Swift, objects are usually created from classes or structs. A class acts like a blueprint. The blueprint says what properties the object has and what methods it can perform. Once you create an object from that blueprint, you have an actual instance you can use in your app.

This is one of the easiest ways to think about OOP:

  • A class is the blueprint.

  • An object is the real thing made from that blueprint.

For example, if you were building an app for a library, you might create a Book class. That class could define properties like title, author, and pages, and methods like borrow() and returnBook(). Every actual book in your app would be an object made from that class.

The beauty of OOP is that it helps you keep related things together. Instead of scattering book title logic in one file, book borrowing logic in another file, and book display logic somewhere else, you can keep the Book behavior in one place. That makes the app easier to read, easier to change, and easier to grow.

Why Swift Makes OOP Feel Cleaner

Swift is a language that takes safety seriously. It wants you to write code that is less likely to break unexpectedly. That influences the way OOP works in Swift.

For example, Swift uses strong typing, optional values, access control, and initializer rules to make object creation safer. It also has structs, which are value types and often a better choice than classes for simple models. But when you do need the classic OOP features like inheritance and shared reference behavior, Swift gives you everything you need.

A lot of beginners come to Swift expecting a language that works exactly like Java or C#. It does not. Swift is more flexible. Classes are important, yes, but so are protocols and structs. That means you should not think of OOP in Swift as “only use classes.” Instead, think of it as “understand classes deeply, and also understand when Swift offers a better tool.”

That mindset will make you a better Swift developer.

Your First Class in Swift

Let’s start with something simple. A class in Swift is declared with the class keyword.

class Car {
    var brand: String
    var model: String
    var year: Int

    init(brand: String, model: String, year: Int) {
        self.brand = brand
        self.model = model
        self.year = year
    }

    func startEngine() {
        print("\(brand) \(model) engine started.")
    }
}

This Car class describes the properties a car has and one behavior it can perform. Let’s break it down.

brand, model, and year are properties. They store information about the car.
init(...) is an initializer. It creates a new car object and gives its properties values.
startEngine() is a method. It describes an action the car can perform.

Now let’s create an object from that class.

let myCar = Car(brand: "Toyota", model: "Corolla", year: 2022)
myCar.startEngine()

When you run this, Swift creates an actual car object and stores it in myCar. Then the startEngine() method prints a message.

This is the heart of OOP: define a blueprint, create an object, use that object in your program.

Properties: The Data Inside an Object

Properties are the stored values that belong to an object. They represent the state of the object. In a User class, properties may include name, email, and age. In a Product class, they may include price, stock, and description.

Swift lets you define two main kinds of properties:

Stored properties hold actual data.
Computed properties calculate a value when needed.

Here is an example:

class Rectangle {
    var width: Double
    var height: Double

    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double {
        return width * height
    }
}

width and height are stored properties because they store actual values. area is a computed property because it calculates the result from the other properties.

Using it:

let box = Rectangle(width: 10, height: 5)
print(box.area) // 50

A computed property is useful when you do not want to store duplicate data. You let Swift calculate the value when needed.

Methods: The Behavior Inside an Object

Methods are functions that belong to a class or struct. They define what an object can do.

Let’s improve our Car example:

class Car {
    var brand: String
    var model: String
    var fuelLevel: Int

    init(brand: String, model: String, fuelLevel: Int) {
        self.brand = brand
        self.model = model
        self.fuelLevel = fuelLevel
    }

    func startEngine() {
        if fuelLevel > 0 {
            print("\(brand) \(model) engine started.")
        } else {
            print("Cannot start the engine. No fuel left.")
        }
    }

    func drive() {
        if fuelLevel > 0 {
            fuelLevel -= 1
            print("\(brand) \(model) is driving. Fuel left: \(fuelLevel)")
        } else {
            print("The car cannot drive without fuel.")
        }
    }
}

Now the object does more than just exist. It can make decisions based on its state. That is a major OOP idea: data and behavior stay together.

let car = Car(brand: "Honda", model: "Civic", fuelLevel: 3)
car.startEngine()
car.drive()
car.drive()

The object changes over time. That is one reason OOP feels powerful in app development. You are not just storing data; you are modeling how something behaves in the real world.

Initializers: Giving Objects a Proper Start

In Swift, an initializer prepares an object before it is used. It ensures all required properties have values.

Without an initializer, your object may be incomplete. Swift prevents that by requiring a proper setup process.

Here is a simple initializer:

class User {
    var name: String
    var email: String

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

self.name = name means “assign the value passed into the initializer to the property on the object itself.”

Usage:

let user = User(name: "Amina", email: "amina@example.com")
print(user.name)

Swift also supports multiple initializers, default values, and failable initializers. These become useful when your object creation has rules.

For example, a failable initializer:

class Temperature {
    var celsius: Double

    init?(celsius: Double) {
        if celsius < -273.15 {
            return nil
        }
        self.celsius = celsius
    }
}

This initializer may return nil if the value is impossible. That kind of safety is one of Swift’s strengths.

let temp = Temperature(celsius: -300)

In this case, temp would be nil because the value is below absolute zero.

Classes and Objects: Reference Types in Swift

Classes in Swift are reference types. That means when you assign one class instance to another variable, both variables point to the same object in memory.

This can be surprising at first.

class Person {
    var name: String

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

let person1 = Person(name: "Sara")
let person2 = person1

person2.name = "Lina"

print(person1.name) // Lina
print(person2.name) // Lina

Even though person1 and person2 look like different variables, they both refer to the same object. Changing one changes the other because they are linked to the same instance.

This is different from structs, which are value types. With structs, assignment usually creates a copy. That difference matters a lot in Swift, especially when choosing between a class and a struct.

If you need shared mutable state, classes are often appropriate. If you want safer value semantics, structs may be better.

Encapsulation: Protecting the Inside of the Object

Encapsulation means hiding the internal details of an object and exposing only what the outside world needs. This is one of the most important ideas in object-oriented programming.

Why does it matter? Because when everything is open and editable, code becomes fragile. Anyone can change internal data in the wrong way. Encapsulation helps prevent that.

In Swift, access control helps with encapsulation. You can make properties private so they cannot be modified directly from outside the class.

class BankAccount {
    private var balance: Double

    init(initialBalance: Double) {
        self.balance = initialBalance
    }

    func deposit(amount: Double) {
        guard amount > 0 else { return }
        balance += amount
    }

    func withdraw(amount: Double) {
        guard amount > 0, amount <= balance else { return }
        balance -= amount
    }

    func currentBalance() -> Double {
        return balance
    }
}

Here, balance is private. No one outside the class can change it directly. They must use deposit() and withdraw(), which enforce business rules.

Usage:

let account = BankAccount(initialBalance: 1000)
account.deposit(amount: 250)
account.withdraw(amount: 100)
print(account.currentBalance())

This is a great example of encapsulation in action. You keep the important data protected and control the allowed behavior.

Access Control in Swift

Swift gives you several access levels:

  • open

  • public

  • internal

  • fileprivate

  • private

You do not need to memorize all of them at once, but you should understand the general idea. Access control helps you decide who can see and use your code.

private means only the current class, struct, or extension can use it.
fileprivate means only code in the same file can use it.
internal means code in the same module can use it. This is Swift’s default.
public means the code can be used outside the module, but not overridden unless allowed.
open is similar to public, but allows subclassing and overriding outside the module.

A simple example:

class Profile {
    private var password: String
    var username: String

    init(username: String, password: String) {
        self.username = username
        self.password = password
    }

    func changePassword(newPassword: String) {
        password = newPassword
    }
}

This protects the password from direct external access. That is not just good style; it is often necessary for safety and design.

Inheritance: Reusing and Extending Behavior

Inheritance lets one class build on another class. The new class inherits properties and methods from the parent class.

This is a classic OOP feature. It helps you reuse code and create specialized versions of something.

Here is a base class:

class Animal {
    var name: String

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

    func makeSound() {
        print("Some generic animal sound")
    }
}

Now let’s create a subclass:

class Dog: Animal {
    func bark() {
        print("\(name) says woof!")
    }
}

Dog inherits from Animal. That means it already has name and makeSound(). It also adds bark().

Usage:

let dog = Dog(name: "Buddy")
dog.makeSound()
dog.bark()

Inheritance is useful when the child class really is a kind of parent class. A dog is an animal. A car is not a kind of bank account. So inheritance should be used where the relationship makes sense.

A lot of beginners overuse inheritance. That can make designs rigid. In Swift, many cases are better handled with protocols and composition instead of deep class hierarchies.

Overriding Methods

Sometimes a subclass needs to change the behavior it inherited from the parent class. That is called overriding.

Swift requires the override keyword so it is clear you are replacing inherited behavior.

class Animal {
    func makeSound() {
        print("Generic sound")
    }
}

class Cat: Animal {
    override func makeSound() {
        print("Meow")
    }
}

Usage:

let cat = Cat()
cat.makeSound()

This is polymorphism in action, which we will discuss more shortly. Different objects can respond differently to the same method call.

You can also override computed properties, initializers, and other members in Swift.

Superclass Methods with super

When you override a method, you can still call the parent version using super.

class Animal {
    func makeSound() {
        print("Animal sound")
    }
}

class Bird: Animal {
    override func makeSound() {
        super.makeSound()
        print("Bird chirping")
    }
}

This lets you extend behavior instead of fully replacing it.

let bird = Bird()
bird.makeSound()

This may print both messages. Sometimes that is exactly what you want. You keep the parent behavior and add your own twist.

Polymorphism: One Interface, Many Forms

Polymorphism sounds complicated, but the idea is simple. It means the same method name can behave differently depending on the object using it.

In Swift, polymorphism appears often when you use inheritance or protocols.

Look at this example:

class Shape {
    func draw() {
        print("Drawing a shape")
    }
}

class Circle: Shape {
    override func draw() {
        print("Drawing a circle")
    }
}

class Square: Shape {
    override func draw() {
        print("Drawing a square")
    }
}

Now use them together:

let shapes: [Shape] = [Circle(), Square(), Shape()]

for shape in shapes {
    shape.draw()
}

Even though the array type is Shape, each object responds in its own way. That is polymorphism.

This becomes extremely useful when you are building real applications. You can store many different objects in one collection and ask them all to do the same thing in a consistent way.

Swift and the Surprise of Protocol-Oriented Design

If you are learning OOP in Swift, one of the biggest surprises is that Swift is not only about classes. Swift developers use protocols constantly.

A protocol defines what capabilities a type should have. It does not provide stored data like a class, but it can define methods, properties, and requirements that many types can adopt.

This gives you a different kind of polymorphism.

protocol Drawable {
    func draw()
}

Now any type that conforms to Drawable must implement draw().

class Triangle: Drawable {
    func draw() {
        print("Drawing a triangle")
    }
}

struct Line: Drawable {
    func draw() {
        print("Drawing a line")
    }
}

Now both class and struct types can behave in a similar way.

let items: [Drawable] = [Triangle(), Line()]

for item in items {
    item.draw()
}

This is one reason Swift feels modern. It gives you object-oriented ideas, but it does not force everything into class inheritance.

When to Use a Class and When to Use a Struct

This question matters a lot in Swift.

Use a class when you need:

  • shared reference behavior

  • inheritance

  • identity matters

  • mutation should be seen by all references

Use a struct when you need:

  • value semantics

  • simple data models

  • safer copying behavior

  • less complexity

A class example:

class UserSession {
    var token: String

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

A struct example:

struct Point {
    var x: Double
    var y: Double
}

The Point model is a perfect struct because it is just data. A UserSession might make more sense as a class if multiple parts of the app need to share the same session state.

This is one of the places where Swift differs from older object-oriented languages. You do not always reach for a class first.

Composition: A Better Alternative to Deep Inheritance

A lot of beginners think OOP means building huge class trees. In practice, that is often not the best approach. Composition can be cleaner.

Composition means building an object out of smaller objects instead of inheriting everything from a parent.

For example:

class Engine {
    func start() {
        print("Engine started")
    }
}

class Car {
    let engine = Engine()

    func startCar() {
        engine.start()
        print("Car is ready")
    }
}

Here, the car has an engine instead of being an engine. This is often a better design than forcing everything into inheritance.

Composition keeps your code flexible. If the engine changes, the car can still remain simple.

Real-World Example: A Library System

Let’s create a small example that feels like a real app.

class Book {
    let title: String
    let author: String
    private(set) var isBorrowed: Bool = false

    init(title: String, author: String) {
        self.title = title
        self.author = author
    }

    func borrow() {
        guard !isBorrowed else {
            print("\(title) is already borrowed.")
            return
        }
        isBorrowed = true
        print("You borrowed \(title).")
    }

    func returnBook() {
        guard isBorrowed else {
            print("\(title) is not borrowed.")
            return
        }
        isBorrowed = false
        print("You returned \(title).")
    }
}

private(set) means other code can read isBorrowed, but only the class itself can change it.

Usage:

let book = Book(title: "1984", author: "George Orwell")
print(book.isBorrowed)
book.borrow()
print(book.isBorrowed)
book.returnBook()

This example uses encapsulation, properties, methods, and state changes in a simple and readable way.

Another Real-World Example: A Shopping App Product

Imagine building a shopping app.

class Product {
    let name: String
    let price: Double
    private(set) var stock: Int

    init(name: String, price: Double, stock: Int) {
        self.name = name
        self.price = price
        self.stock = stock
    }

    func buy(quantity: Int) -> Bool {
        guard quantity > 0 else { return false }
        guard quantity <= stock else {
            print("Not enough stock for \(name).")
            return false
        }

        stock -= quantity
        print("Purchased \(quantity) of \(name).")
        return true
    }

    func restock(quantity: Int) {
        guard quantity > 0 else { return }
        stock += quantity
        print("\(name) restocked by \(quantity).")
    }
}

Usage:

let laptop = Product(name: "MacBook Air", price: 999.99, stock: 5)
laptop.buy(quantity: 2)
laptop.restock(quantity: 3)

This kind of code is the practical side of OOP. You are not just learning theory; you are building rules around real things.

Lazy Properties

Swift supports lazy properties, which are created only when needed. This can be useful when a property is expensive to create.

class DataManager {
    lazy var data: [String] = {
        print("Loading data...")
        return ["A", "B", "C"]
    }()
}

The data array is not created until it is first used.

let manager = DataManager()
print("Before accessing data")
print(manager.data)

Lazy properties can improve performance when handled carefully.

Type Properties

Sometimes a property belongs to the type itself, not to each object. In that case, you use static.

class AppConfig {
    static let appName = "Swift Shop"
    static let version = "1.0"
}

Usage:

print(AppConfig.appName)
print(AppConfig.version)

These are shared by the type, not copied into every instance. Type properties are useful for constants, shared settings, and global-style data that belongs to the model itself.

Type Methods

Type methods also belong to the class or struct itself.

class MathHelper {
    static func square(_ number: Int) -> Int {
        return number * number
    }
}

Usage:

print(MathHelper.square(6))

This is helpful for utility functions that logically belong to the type.

Deinitializers

Swift classes can have deinitializers, which are called when an object is about to be destroyed.

class Session {
    init() {
        print("Session started")
    }

    deinit {
        print("Session ended")
    }
}

When an instance is no longer needed, Swift releases it automatically if there are no strong references left. The deinit method is a good place to clean up resources.

This is part of Swift’s memory management system, which uses Automatic Reference Counting, or ARC.

Memory Management and Strong Reference Cycles

Because classes are reference types, they can create strong reference cycles. This happens when two objects keep strong references to each other, and neither gets released.

For example:

class Person {
    var name: String
    var apartment: Apartment?

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

    deinit {
        print("\(name) is being deinitialized")
    }
}

class Apartment {
    var unit: String
    var tenant: Person?

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

    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

If Person and Apartment reference each other strongly, they may never be released. Swift solves this with weak and unowned references.

class Apartment {
    var unit: String
    weak var tenant: Person?

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

Using weak breaks the cycle. This is one of those topics that seems advanced at first, but becomes very important in real apps.

OOP and UIKit / SwiftUI Thinking

When people learn OOP in Swift, they often connect it with app development right away. That makes sense. In UIKit, you often work with classes such as view controllers, custom views, and model objects. OOP helps you organize those pieces.

In SwiftUI, the style is a bit different, because SwiftUI leans more toward structs and declarative UI. Still, OOP ideas remain useful for data models, services, managers, and app logic. You may not build the whole interface as a class hierarchy, but you will still use OOP concepts everywhere.

For example, you might have:

  • a network manager class

  • a data storage service class

  • a session tracker class

  • model objects for user, product, or order

Even in a SwiftUI app, OOP can still form the backbone of your business logic.

Common Mistakes Beginners Make

One common mistake is trying to force everything into classes. Not every problem needs inheritance. Not every piece of data needs a class. Sometimes a simple struct is enough.

Another mistake is ignoring access control. Beginners often make all properties public because it feels easier. That usually leads to messy code and accidental bugs.

Another mistake is building inheritance chains that are too deep. A parent class, child class, grandchild class, and so on can become hard to understand. In many cases, composition or protocols are better.

Another mistake is forgetting that Swift values safety and clarity. The language is designed to help you write code that is predictable. So the best OOP code in Swift is usually not the most complicated code. It is the clearest code.

A More Complete Example: Student Management

Let’s put several OOP ideas together in one example.

class Student {
    let name: String
    private var grades: [Double]

    init(name: String, grades: [Double] = []) {
        self.name = name
        self.grades = grades
    }

    func addGrade(_ grade: Double) {
        guard grade >= 0 && grade <= 100 else {
            print("Invalid grade")
            return
        }
        grades.append(grade)
    }

    func averageGrade() -> Double {
        guard !grades.isEmpty else { return 0 }
        return grades.reduce(0, +) / Double(grades.count)
    }

    func printReport() {
        print("Student: \(name)")
        print("Grades: \(grades)")
        print("Average: \(averageGrade())")
    }
}

Usage:

let student = Student(name: "Omar")
student.addGrade(90)
student.addGrade(85)
student.addGrade(96)
student.printReport()

This example includes encapsulation, validation, methods, and useful state management. It also feels close to something you might actually build in a real app.

How to Think Like an OOP Developer in Swift

The most important skill is not writing long code. It is thinking in objects.

Ask yourself questions like these:

What are the important things in this system?
What data does each thing need?
What actions should each thing perform?
What should be hidden from outside code?
What should be shared?
What should be reusable?
What should be a class, and what should be a struct?

That way of thinking helps you design better software. Instead of beginning with syntax, you begin with structure.

Suppose you are building a food delivery app. You may identify objects like:

  • User

  • Restaurant

  • MenuItem

  • Order

  • DeliveryDriver

  • Payment

Each object has data and behavior. That is OOP in action.

Why OOP Still Matters

Some developers say OOP is old-fashioned, but that misses the point. OOP is still everywhere in app development, backend systems, game development, and large software projects. Even when a language supports other paradigms, OOP remains a powerful tool for organizing complex code.

What matters is using it wisely.

Swift gives you a modern way to use object-oriented programming without forcing you into outdated patterns. That means you can combine OOP with protocol-oriented ideas, value types, and functional features when useful. This flexibility is a big strength.

The goal is not to prove that you know every pattern. The goal is to build software that is easy to understand, easy to test, and easy to improve later. OOP helps with that when used with care.

Final Thoughts

Object-oriented programming in Swift becomes much easier once you stop seeing it as a list of definitions and start seeing it as a way to model real things. A class is a blueprint. An object is the real instance. Properties hold data. Methods hold behavior. Encapsulation protects your internals. Inheritance can reuse and extend behavior. Polymorphism lets different types respond in their own ways. Protocols and composition give you even more flexibility.

Swift is a language that respects OOP, but it also improves it. It encourages safer, cleaner, more expressive code. That is why learning OOP in Swift is not just about understanding one programming style. It is about learning how to build software in a modern, thoughtful way.

If you are just starting out, keep your examples small. Build a Car, a Book, a Student, a BankAccount. Give each one clear properties and methods. Experiment with inheritance. Try protocols. Make mistakes and fix them. That is how the ideas start to stick.

And once they do, you will notice something nice: OOP stops feeling like theory and starts feeling like a useful habit. That is when Swift becomes much more enjoyable to write.

#Swift OOP #Object-Oriented Programming in Swift #Swift classes #Swift objects #Swift inheritance #Swift encapsulation #Swift polymorphism #Swift init #Swift protocols #Swift programming tutorial

Subscribe to our newsletter

12k+

Subscribers

Weekly

Frequency

Free

Always