Skip to content

Swift Inheritance — Class Hierarchies, Overriding, and Initialization

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Swift Inheritance. We cover key concepts, practical examples, and best practices to help you master this topic.

Swift inheritance enables a class to inherit properties, methods, and other characteristics from another class, forming a hierarchy where subclasses extend and specialize the behavior of their parent without duplicating code.

What You'll Learn

  • How class inheritance works in Swift
  • Overriding instance methods, properties, and subscripts
  • Preventing overrides with final
  • Initializer inheritance and required initializers
  • When to prefer inheritance over protocol-oriented design

Why It Matters

Inheritance is a cornerstone of object-oriented programming and is widely used in Apple's UIKit and AppKit frameworks. Every UIViewController, UIView, and NSObject subclass you create relies on inheritance. Understanding how to properly override, extend, and constrain subclasses is essential for building stable iOS and macOS applications.

Real-World Use

The iOS SDK itself is built on deep inheritance hierarchies. UILabel inherits from UIView, which inherits from UIResponder, which inherits from NSObject. When you create a custom UIButton subclass to add a loading spinner, you are using inheritance to extend built-in behavior with minimal code.

Learning Path

flowchart LR
  A[Properties
Lesson 11] --> B[Inheritance
You are here] B --> C[Protocols
Lesson 13] B --> D[Extensions
Lesson 14] style B fill:#f90,color:#fff

Defining a Base Class

Any class that does not inherit from another is a base class. Swift classes do not inherit from a universal base class like some languages — you start from scratch unless you specify a parent.

class Vehicle {
    var currentSpeed = 0.0
    var description: String {
        return "Traveling at \(currentSpeed) km/h"
    }

    func makeNoise() {
        // No default implementation — subclasses override this
    }
}

let someVehicle = Vehicle()
print(someVehicle.description)

Output: Traveling at 0.0 km/h

Subclassing

A subclass inherits all characteristics from its parent class and can add new ones. You declare a subclass by placing the parent class name after a colon.

class Bicycle: Vehicle {
    var hasBasket = false

    override var description: String {
        return "Bicycle" + (hasBasket ? " with basket" : "") + " — \(super.description)"
    }
}

class Tandem: Bicycle {
    var passengerCount = 2

    override var description: String {
        return "Tandem for \(passengerCount)\(super.description)"
    }
}

let tandem = Tandem()
tandem.hasBasket = true
tandem.currentSpeed = 15.0
print(tandem.description)

Output: Tandem for 2 — Bicycle with basket — Traveling at 15.0 km/h

The Tandem class inherits from Bicycle, which inherits from Vehicle. The currentSpeed property exists in Vehicle, but Tandem and Bicycle both have access to it. Each subclass refines description by calling super.description.

Overriding Methods

Subclasses can provide their own implementation of any method inherited from a parent class using the override keyword.

class Train: Vehicle {
    override func makeNoise() {
        print("Choo Choo!")
    }
}

class ElectricCar: Vehicle {
    override func makeNoise() {
        print("... (silent electric motor)")
    }

    func chargeBattery() {
        print("Charging battery...")
    }
}

let train = Train()
train.makeNoise()
let tesla = ElectricCar()
tesla.makeNoise()
tesla.chargeBattery()

Output:

Choo Choo!
... (silent electric motor)
Charging battery...

Subclasses can also add entirely new methods that did not exist in the parent. The chargeBattery() method is unique to ElectricCar.

Overriding Properties

You can override any inherited property to provide a custom getter, setter, or property Observer. The overriding property must match the name and type of the inherited property.

class Car: Vehicle {
    var gear = 1

    override var description: String {
        return super.description + " in gear \(gear)"
    }

    override var currentSpeed: Double {
        didSet {
            gear = Int(currentSpeed / 20.0) + 1
            if gear < 1 { gear = 1 }
            if gear > 6 { gear = 6 }
        }
    }
}

let car = Car()
car.currentSpeed = 45.0
print(car.description)
car.currentSpeed = 95.0
print(car.description)

Output:

Traveling at 45.0 km/h in gear 3
Traveling at 95.0 km/h in gear 6

The description property uses super.description to include the parent's output and appends the gear information. The currentSpeed observer automatically adjusts the gear whenever the speed changes.

Preventing Overrides

Mark a method, property, subscript, or entire class with the final keyword to prevent subclasses from overriding it.

class Engine {
    final func start() {
        print("Engine starting...")
    }
}

class HybridEngine: Engine {
    // This will NOT compile:
    // override func start() { }
}

Marking the entire class as final prevents any subclassing at all:

final class ImmutableVehicle {
    var speed = 0
}
// class SubVehicle: ImmutableVehicle { } // error

Initializer Inheritance

Swift follows specific rules for initializer inheritance. By default, subclasses do not inherit their parent's initializers. You must implement them or provide default values.

Designated Initializers

A designated initializer fully initializes all properties introduced by the class and calls an appropriate parent initializer.

class Animal {
    let name: String
    var age: Int

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

    convenience init(name: String) {
        self.init(name: name, age: 0)
    }
}

class Dog: Animal {
    var breed: String

    init(name: String, age: Int, breed: String) {
        self.breed = breed
        super.init(name: name, age: age)
    }

    convenience init(name: String, breed: String) {
        self.init(name: name, age: 0, breed: breed)
    }
}

let puppy = Dog(name: "Buddy", breed: "Golden Retriever")
print("\(puppy.name) is a \(puppy.breed) aged \(puppy.age)")

Output: Buddy is a Golden Retriever aged 0

The Dog class must set its own properties before calling super.init. Convenience initializers in the subclass delegate to the subclass's designated initializer, which in turn calls the parent's designated initializer.

Required Initializers

Mark an initializer with required to force every subclass to implement it.

class Shape {
    var color: String
    required init(color: String) {
        self.color = color
    }
}

class Circle: Shape {
    var radius: Double

    required init(color: String) {
        self.radius = 1.0
        super.init(color: color)
    }

    init(radius: Double, color: String) {
        self.radius = radius
        super.init(color: color)
    }
}

let defaultCircle = Circle(color: "red")
let customCircle = Circle(radius: 5.0, color: "blue")
print("\(defaultCircle.color) circle radius \(defaultCircle.radius)")
print("\(customCircle.color) circle radius \(customCircle.radius)")

Output:

red circle radius 1.0
blue circle radius 5.0

Inheritance vs Protocols

Swift encourages protocol-oriented programming over deep class hierarchies. Protocols allow value types (structs and enums) to share behavior without inheritance.

protocol Drivable {
    var speed: Double { get set }
    func accelerate()
    func brake()
}

struct CarStruct: Drivable {
    var speed: Double = 0

    mutating func accelerate() {
        speed += 10
    }

    mutating func brake() {
        speed = max(0, speed - 10)
    }
}

Structs conforming to protocols avoid the complexity of class inheritance (reference semantics, superclass fragility) while still providing polymorphic behavior.

Common Mistakes

  1. Calling super methods unnecessarily: Only call super.someMethod() when you want to extend the parent behavior, not replace it entirely. Calling super when not needed adds coupling to the parent implementation.

  2. Forgetting to call super.init: A subclass designated initializer must call a parent initializer. Swift enforces this with a compile-time error if you skip it.

  3. Overriding without the override keyword: Swift requires explicit override for any method or property that replaces a parent version. If you get a "method does not override any method from its superclass" error, check the spelling and signature.

  4. Deep inheritance hierarchies: More than 3-4 levels of inheritance makes code hard to understand and maintain. Prefer composition or protocols for complex behavior sharing.

  5. Using final too late: Once a class is released as part of a framework or library, removing final is a breaking change. Design your class hierarchy with final in mind from the start.

Practice Questions

  1. What keyword must you use when providing a new implementation of an inherited method?
  2. How does Swift prevent a subclass from overriding a method?
  3. What is the difference between designated and convenience initializers in an inheritance chain?
  4. Why does Swift prefer protocols over deep inheritance hierarchies?
  5. Challenge: Create a class hierarchy for a media player with Playable as the base, AudioFile, VideoFile, and StreamingMedia as subclasses. Each should override a play() method. Add a final method called metadata() that cannot be overridden.

Mini Project

Build a Shape hierarchy with:

  • Base class Shape with a color property and area() method returning 0
  • Subclass Circle with radius and overriding area()
  • Subclass Rectangle with width and height and overriding area()
  • Subclass Square inheriting from Rectangle with convenience init
  • Each subclass must have proper description override

FAQ

Can a Swift class inherit from multiple classes?

No. Swift supports single inheritance only. However, a class can conform to multiple protocols, which provides many of the benefits of multiple inheritance.

Do structs support inheritance?

No. Structs are value types and do not support inheritance. Use protocols to share behavior between structs.

Can I override a stored property?

You cannot override a stored property with another stored property. You can only override computed properties or add property observers to inherited stored properties.

What happens if I don't call super.init?

Swift enforces that every stored property must be initialized. If you skip super.init, the compiler produces an error. However, if your class only overrides methods and doesn't add stored properties, you may omit super.init if all properties have default values.

When should I use final?

Use final when the implementation is complete and should not change, for security-sensitive operations, or to enable compiler optimizations like static dispatch.

What's Next

After mastering inheritance, learn how Protocols enable polymorphic behavior without class coupling, or explore Extensions to add functionality to existing types.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro