Swift Inheritance: Subclassing Classes and Overriding Methods

Swift inheritance lets one class build on the behavior of another class. It is the core feature that makes class hierarchies possible, so you can reuse code, specialize behavior, and model “is-a” relationships in a clean way.

Quick answer: In Swift, inheritance is available only for classes, not structs or enums. A subclass inherits stored and computed properties, methods, and subscripts from its superclass, and can customize them with override or add new behavior of its own.

Difficulty: Beginner

You'll understand this better if you know: basic Swift types, how functions and properties work, and the difference between classes and structs.

1. What Is Swift Inheritance?

Inheritance is a relationship between two classes where a new class, called a subclass, starts with the behavior of an existing class, called a superclass. The subclass can keep what it inherits, add more properties or methods, and replace some behavior when needed.

For example, a Car class might inherit from a more general Vehicle class, because every car is a vehicle, but not every vehicle is a car.

2. Why Swift Inheritance Matters

Inheritance helps you avoid repeating shared code across related classes. It also gives you a structured way to extend behavior while keeping common logic in one place.

In real code, inheritance is useful when several types share the same base state or rules, but each type needs its own details. It is especially helpful for object-oriented models, framework subclasses, and type hierarchies where shared behavior should live in a common base class.

Inheritance is not the default way to design every Swift program. In many cases, composition is a better fit, but inheritance remains important when a true superclass/subclass relationship exists and polymorphic behavior is needed.

3. Basic Syntax or Core Idea

A subclass is declared with a colon after the class name. The subclass name comes first, then the superclass name.

Minimal subclass syntax

This example shows the basic shape of a class hierarchy and one overridden method.

class Vehicle {
    var speed : Int = 0
    
    func describe() -> String {
        return "Moving at \(speed) km/h"
    }
}

class Car : Vehicle {
    var gearCount : Int = 6
    
    override func describe() -> String {
        return "Car moving at \(speed) km/h with \(gearCount) gears"
    }
}

Here, Car inherits speed and describe() from Vehicle, then adds gearCount and overrides the method to provide more specific output.

4. Step-by-Step Examples

Example 1: Inheriting stored properties

A subclass automatically receives the superclass’s stored properties, so you can use them immediately in the child type.

class Animal {
    var name : String
    
    init(name: String) {
        self.name = name
    }
}

class Dog : Animal {
    func speak() -> String {
        return "\(name) says woof"
    }
}

let pet = Dog(name: "Milo")
print(pet.speak())

The subclass uses the inherited name property without redefining it. Because the superclass initializer sets that property, the subclass can focus on its own behavior.

Example 2: Overriding a method

When inherited behavior is close, but not quite right, override the method and use super if you want to keep part of the original implementation.

class Appliance {
    func status() -> String {
        return "Appliance is on"
    }
}

class Washer : Appliance {
    override func status() -> String {
        return super.status() + " and washing clothes"
    }
}

This pattern is useful when the subclass wants to refine the superclass result instead of replacing it completely.

Example 3: Adding new properties and methods

Inheritance is not only about reuse. A subclass can also introduce new state and behavior that belongs only to the more specific type.

class Employee {
    var name : String
    
    init(name: String) {
        self.name = name
    }
}

class Manager : Employee {
    var teamSize : Int
    
    init(name: String, teamSize: Int) {
        self.teamSize = teamSize
        super.init(name: name)
    }
    
    func report() -> String {
        return "\(name) manages \(teamSize) people"
    }
}

The subclass stores its own data while still relying on the inherited name property from Employee.

Example 4: Overriding a computed property

Inherited computed properties can also be replaced if the subclass needs a different result.

class Shape {
    var description: String {
        return "A shape"
    }
}

class Circle : Shape {
    override var description: String {
        return "A circle"
    }
}

This is a common pattern when a base type provides a general description and the subclass needs a more precise one.

5. Practical Use Cases

In Swift, inheritance is most appropriate when shared identity and behavior are real, not just convenient. If types are not truly in an is-a relationship, composition is often the better design.

6. Common Mistakes

Mistake 1: Trying to inherit from a struct or enum

Only classes support inheritance in Swift. Beginners often try to use a struct as a base type because it feels lighter than a class.

Problem: A struct cannot be a superclass, so Swift reports an error such as “inheritance from non-protocol type” or a similar compiler message depending on the declaration.

struct Animal {
    var name: String
}

class Dog : Animal {
}

Fix: Make the base type a class if you need inheritance.

class Animal {
    var name: String
}

class Dog : Animal {
}

The corrected version works because classes are the only nominal types that can participate in inheritance.

Mistake 2: Forgetting override on a replaced member

When a subclass intentionally replaces inherited behavior, Swift requires the override keyword. This protects you from accidentally shadowing a superclass member.

Problem: Without override, Swift emits an error like “overriding declaration requires an override keyword.”

class Bird {
    func sound() -> String {
        return "chirp"
    }
}

class Parrot : Bird {
    func sound() -> String {
        return "hello"
    }
}

Fix: Add override to make the intent explicit.

class Bird {
    func sound() -> String {
        return "chirp"
    }
}

class Parrot : Bird {
    override func sound() -> String {
        return "hello"
    }
}

The fixed version works because Swift now knows the subclass is intentionally replacing the inherited method.

Mistake 3: Calling super.init too late or not at all

Subclass initializers must fully initialize subclass properties before calling a superclass initializer, and they must eventually initialize the inherited state correctly.

Problem: If you call super.init before setting all subclass stored properties, Swift rejects the initializer because the object is not fully initialized yet.

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Teacher : Person {
    var subject: String
    
    init(name: String, subject: String) {
        super.init(name: name)
        self.subject = subject
    }
}

Fix: Initialize subclass properties first, then call super.init.

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Teacher : Person {
    var subject: String
    
    init(name: String, subject: String) {
        self.subject = subject
        super.init(name: name)
    }
}

The corrected initializer works because Swift’s two-phase initialization rules are satisfied.

7. Best Practices

Practice 1: Use inheritance only for a real is-a relationship

Inheritance should represent genuine specialization, not just code reuse. If the subclass is not really a more specific version of the superclass, the hierarchy becomes confusing.

// Better design: a Car is a Vehicle
class Vehicle { }
class Car : Vehicle { }

This keeps the hierarchy meaningful and makes future maintenance easier.

Practice 2: Mark classes final when you do not want subclassing

If a class is not designed as a base class, declare it final. This prevents accidental inheritance and can make intent clearer.

final class SessionCache {
    func clear() {
        // implementation
    }
}

Using final avoids unnecessary inheritance and documents that the class is meant to stand on its own.

Practice 3: Keep superclass APIs small and focused

A superclass with too many responsibilities becomes hard to subclass safely. A smaller base API makes overriding easier and reduces the chance of fragile behavior.

class Report {
    func title() -> String {
        return "Monthly Report"
    }
}

Smaller superclass contracts are easier to understand and safer to extend in subclasses.

8. Limitations and Edge Cases

One common surprise is that inheritance is not a substitute for every kind of shared code. Many Swift APIs prefer protocols and composition when the goal is shared capability rather than shared state.

9. Practical Mini Project

Let’s build a tiny inventory model with a base Product class and two subclasses. The goal is to show a complete class hierarchy with inheritance, overriding, and superclass initializers.

class Product {
    let name: String
    let price: Double
    
    init(name: String, price: Double) {
        self.name = name
        self.price = price
    }
    
    func summary() -> String {
        return "\(name) costs $\(price)"
    }
}

class Book : Product {
    let author: String
    
    init(name: String, price: Double, author: String) {
        self.author = author
        super.init(name: name, price: price)
    }
    
    override func summary() -> String {
        return super.summary() + " by \(author)"
    }
}

class DigitalProduct : Product {
    let downloadSizeMB: Int
    
    init(name: String, price: Double, downloadSizeMB: Int) {
        self.downloadSizeMB = downloadSizeMB
        super.init(name: name, price: price)
    }
}

let novel = Book(name: "Swift Patterns", price: 29.99, author: "A. Developer")
let app = DigitalProduct(name: "Photo Editor", price: 49.0, downloadSizeMB: 180)

print(novel.summary())
print(app.summary())

This example shows how a shared superclass can hold common data and behavior while subclasses add details that are specific to each product type. It also demonstrates the use of super.init and super.summary() in a realistic way.

10. Key Points

11. Practice Exercise

Expected output: A string that includes the appliance brand and its power rating.

Hint: Remember to initialize subclass properties before calling super.init, and use override on the method.

class Appliance {
    let brand: String
    
    init(brand: String) {
        self.brand = brand
    }
    
    func details() -> String {
        return "Brand: \(brand)"
    }
}

class Microwave : Appliance {
    let powerWatts: Int
    
    init(brand: String, powerWatts: Int) {
        self.powerWatts = powerWatts
        super.init(brand: brand)
    }
    
    override func details() -> String {
        return super.details() + ", Power: \(powerWatts)W"
    }
}

let oven = Microwave(brand: "KitchenPro", powerWatts: 900)
print(oven.details())

12. Final Summary

Swift inheritance gives classes a way to share and extend behavior through a superclass and subclass relationship. It is a powerful tool for modeling specialized types, reducing repeated code, and customizing inherited methods or properties with override.

At the same time, inheritance comes with rules: only classes can participate, only one superclass is allowed, and initializers must follow Swift’s strict initialization model. Those rules help keep class hierarchies predictable and safe.

If you are learning Swift inheritance, a good next step is to study override, super, designated and convenience initializers, and when to choose composition instead of subclassing.