Polyglot
Languages Swift oop
Swift § oop

Classes and OOP

Swift’s class is the principal reference type — admits inheritance, deinitialisers, mutable state through references, and identity comparison (===). Classes complement structs (value types, treated in Value and reference types). The class system is single-inheritance — a class extends at most one parent — but admits multiple protocol conformance via the colon syntax. The principal class features: initialisers (designated, convenience, required, failable), property observers (willSet, didSet), computed properties, type properties (static/class), inheritance with override, the final keyword for sealing, and deinit for cleanup. The combination — single-inheritance classes, multiple protocol conformance, the substantial initialiser machinery, the property observers, the conventional final-by-default discipline — is the substance of Swift’s class-oriented surface.

Class declarations

class Person {
    let name: String
    var age: Int

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

    func greet() -> String {
        "Hello, I am \(name)"
    }
}

let p = Person(name: "Alice", age: 30)
print(p.greet())

The form: class Name { ... }. The init is the constructor — required unless all properties have defaults.

Initialisers

Three principal kinds:

Designated initialisers

The conventional initialiser — fully constructs the instance:

class Person {
    let name: String
    var age: Int

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

A class must have at least one designated initialiser. All stored properties must be initialised by the time the designated initialiser exits.

Convenience initialisers

Delegate to a designated initialiser; admit substantial initialisation flexibility:

class Person {
    let name: String
    var age: Int

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

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

    convenience init() {                           // chains via convenience
        self.init(name: "anonymous")
    }
}

Required initialisers

The required admits “all subclasses must implement this”:

class Person {
    let name: String

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

class Student: Person {
    let school: String

    required init(name: String) {                  // required override
        self.school = "Unknown"
        super.init(name: name)
    }
}

Conventional in protocols requiring init.

Failable initialisers

The init? returns Optional<Self>:

class URL {
    let value: String

    init?(_ raw: String) {
        guard raw.contains("://") else { return nil }
        self.value = raw
    }
}

let valid = URL("https://example.com")             // Optional<URL>
let invalid = URL("not a url")                     // nil

The conventional pattern for “construction may fail”.

Inheritance

The : admits inheritance:

class Animal {
    let name: String

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

    func speak() -> String {
        "..."
    }
}

class Dog: Animal {
    let breed: String

    init(name: String, breed: String) {
        self.breed = breed                         // initialise own properties
        super.init(name: name)                     // then super.init
    }

    override func speak() -> String {              // override required
        "Woof!"
    }
}

The override keyword is required — admits explicit intent.

The super admits calling the parent’s implementation:

class Dog: Animal {
    override func speak() -> String {
        super.speak() + " Woof!"
    }
}

final

The final keyword admits sealing — preventing further inheritance or override:

final class Logger { /* ... */ }                   // cannot be inherited

class Animal {
    final func breathe() { /* ... */ }             // cannot be overridden

    final var species: String                      // cannot be overridden
}

The conventional Swift discipline marks classes final unless inheritance is genuinely intended — admits substantial compiler optimisations.

Properties

Stored properties

class Counter {
    var count: Int = 0                             // stored, mutable
    let initial: Int = 0                            // stored, immutable
}

Computed properties

Properties with get and optional set:

class Rectangle {
    var width: Double = 0
    var height: Double = 0

    var area: Double {                             // computed (read-only)
        width * height
    }

    var perimeter: Double {
        get { 2 * (width + height) }
        set { /* ... */ }
    }
}

Read-only computed properties may omit get:

var area: Double { width * height }                // implicit get

Property observers

willSet and didSet admit hooking property changes:

class Account {
    var balance: Double = 0 {
        willSet {
            print("about to change to \(newValue)")
        }
        didSet {
            print("changed from \(oldValue)")
        }
    }
}

The observers do not run during the initial init — only on subsequent assignments.

Type properties

static admits class-level (not instance-level) properties:

class Config {
    static let `default`: Config = Config()
    static var current: Config = .default

    let host: String
    init(host: String = "localhost") {
        self.host = host
    }
}

let c = Config.current
Config.current = Config(host: "example.com")

For overridable type members in classes, the class keyword:

class Animal {
    class func describe() -> String {              // overridable in subclasses
        "an animal"
    }

    static func breathe() -> String {              // not overridable
        "breathing"
    }
}

class Dog: Animal {
    override class func describe() -> String {     // override admitted
        "a dog"
    }
}

The conventional discipline:

  • static — for non-overridable type members.
  • class — for overridable type members in inheritance hierarchies.

lazy properties

lazy admits computing on first access:

class DataLoader {
    lazy var data: [Item] = {                      // computed on first access
        return loadFromDisk()
    }()
}

Must be var (not let); admits substantial deferred initialisation.

self and Self

The self refers to the current instance; Self refers to the type:

class Foo {
    func instanceMethod() {
        // self is the instance
    }

    static func staticMethod() {
        // self is the class
    }

    func returnSelfType() -> Self {                // Self = the conforming type
        return self                                // for non-final, this is the dynamic type
    }
}

Deinitialiser

deinit runs when the instance is deallocated:

class FileHandle {
    let descriptor: Int32

    init(path: String) throws {
        self.descriptor = open(path, O_RDONLY)
        guard descriptor >= 0 else { throw IOError.openFailed }
    }

    deinit {
        close(descriptor)                          // automatic cleanup
    }
}

Treated in Memory and ARC.

Access control on classes

public class API {
    public init(host: String) { /* ... */ }

    private var connection: Connection             // hidden
    fileprivate var helper: Helper                 // file-only

    public func fetch() async throws -> Data { /* ... */ }
    private func setup() { /* ... */ }
}

The modifiers admit substantial visibility control; treated in Scope and modules.

Protocol conformance

Classes admit conforming to multiple protocols:

class Service: Configurable, Logger, ErrorHandler {
    /* ... */
}

The colon-separated list admits multiple protocols and (optionally) one parent class. The parent class must come first:

class Dog: Animal, Walking, Swimming, Barking {
    // Animal is the parent class
    // Walking, Swimming, Barking are protocols
}

Subscripts

Classes admit subscripts — indexing-like access via subscript:

class Storage {
    private var data: [String: Any] = [:]

    subscript(key: String) -> Any? {
        get { data[key] }
        set { data[key] = newValue }
    }

    subscript<T>(key: String, type type: T.Type) -> T? {
        data[key] as? T
    }
}

let s = Storage()
s["count"] = 42
let n = s["count", type: Int.self]                 // Optional<Int>

The subscript admits substantial encapsulation; conventional in collection-like classes.

Common patterns

Builder pattern

class HTTPRequest {
    var url: URL
    var method: String = "GET"
    var headers: [String: String] = [:]
    var body: Data?

    init(url: URL) {
        self.url = url
    }

    @discardableResult
    func setMethod(_ m: String) -> Self {
        method = m
        return self
    }

    @discardableResult
    func addHeader(_ key: String, _ value: String) -> Self {
        headers[key] = value
        return self
    }

    @discardableResult
    func setBody(_ b: Data) -> Self {
        body = b
        return self
    }
}

let req = HTTPRequest(url: url)
    .setMethod("POST")
    .addHeader("Content-Type", "application/json")
    .setBody(jsonData)

Singleton

final class APIClient {
    static let shared = APIClient()

    private init() { }                             // private — prevents external construction

    func fetch(_ url: URL) async throws -> Data {
        /* ... */
    }
}

let data = try await APIClient.shared.fetch(url)

The final and private init admit a substantial singleton implementation.

Factory method

class User {
    let id: UUID
    let name: String

    private init(id: UUID, name: String) {
        self.id = id
        self.name = name
    }

    static func create(name: String) -> User {
        User(id: UUID(), name: name)
    }
}

let u = User.create(name: "Alice")

Designated and convenience initialisers

class Vehicle {
    let make: String
    let model: String
    var year: Int

    init(make: String, model: String, year: Int) { // designated
        self.make = make
        self.model = model
        self.year = year
    }

    convenience init(make: String, model: String) {
        self.init(make: make, model: model, year: 2025)
    }
}

Property observer for synchronisation

class Settings {
    var theme: Theme = .light {
        didSet {
            if theme != oldValue {
                NotificationCenter.default.post(name: .themeChanged, object: self)
                UserDefaults.standard.set(theme.rawValue, forKey: "theme")
            }
        }
    }
}

Computed property with both get and set

class CenteredView {
    var origin: CGPoint = .zero
    var size: CGSize = .zero

    var center: CGPoint {
        get {
            CGPoint(x: origin.x + size.width / 2, y: origin.y + size.height / 2)
        }
        set {
            origin = CGPoint(x: newValue.x - size.width / 2,
                             y: newValue.y - size.height / 2)
        }
    }
}

final for performance and clarity

public final class Logger {
    public static let shared = Logger()

    private init() { }

    public func log(_ message: String, level: Level = .info) {
        // ...
    }
}

The final admits the compiler to substantially optimise method dispatch (no virtual call overhead).

Inheritance with template method

class Report {
    func generate() -> String {
        var output = "==== \(title) ====\n"
        output += body() + "\n"
        output += "==== End ===="
        return output
    }

    var title: String { fatalError("subclass must implement") }
    func body() -> String { fatalError("subclass must implement") }
}

class SalesReport: Report {
    override var title: String { "Sales" }
    override func body() -> String { "Sales data" }
}

Conventional Swift contemporary discipline favours protocols with default implementations over template-method patterns:

protocol Report {
    var title: String { get }
    func body() -> String
}

extension Report {
    func generate() -> String {
        "==== \(title) ====\n" + body() + "\n==== End ===="
    }
}

struct SalesReport: Report {                        // struct, not class
    var title: String { "Sales" }
    func body() -> String { "Sales data" }
}

Failable init with validation

final class Email {
    let value: String

    init?(_ raw: String) {
        guard raw.contains("@") else { return nil }
        self.value = raw
    }
}

let valid = Email("alice@example.com")             // Optional<Email>
let invalid = Email("not-an-email")                // nil

Required init for protocol conformance

protocol Createable {
    init()
}

class Foo: Createable {
    required init() { }                             // required for protocol
}

class Bar: Foo {
    required init() {                               // also required in subclass
        super.init()
    }
}

Subscript-based DSL

class Grid {
    private var data: [[Int]] = Array(repeating: Array(repeating: 0, count: 10), count: 10)

    subscript(row: Int, col: Int) -> Int {
        get { data[row][col] }
        set { data[row][col] = newValue }
    }
}

let g = Grid()
g[3, 4] = 42
print(g[3, 4])                                     // 42

Class hierarchy with abstract-like base

class Animal {
    func makeSound() -> String {
        fatalError("subclass must override")
    }
}

class Dog: Animal {
    override func makeSound() -> String { "Woof!" }
}

class Cat: Animal {
    override func makeSound() -> String { "Meow!" }
}

The conventional contemporary discipline uses protocols for abstract requirements:

protocol Animal {
    func makeSound() -> String
}

struct Dog: Animal {
    func makeSound() -> String { "Woof!" }
}

struct Cat: Animal {
    func makeSound() -> String { "Meow!" }
}

Treated in Protocols.

A note on the conventional discipline

The contemporary Swift OOP advice:

  • Default to struct over class — value semantics suffice for most data.
  • Use class when identity, inheritance, or shared mutation matters.
  • Mark classes final by default — admit subclassing only when intended.
  • Use private init for singletons or factory-only classes.
  • Use convenience init for substantial initialisation flexibility.
  • Use failable init (init?) for “construction may fail”.
  • Use required init for protocol-required initialisers.
  • Use property observers (didSet) for reactive patterns.
  • Use computed properties for derived values.
  • Use lazy for substantial deferred initialisation.
  • Use protocols with default implementations over deep inheritance hierarchies.
  • Use class (not static) for overridable type members.
  • Use super.init last in initialisers — initialise own properties first, then super.

The combination — single-inheritance classes, the substantial initialiser machinery (designated, convenience, required, failable), property observers, computed properties, the final discipline, multiple protocol conformance — is the substance of Swift’s class-oriented surface. The discipline produces clear, type-safe, well-encapsulated reference-type code; the conventional contemporary Swift design favours protocols and structs over deep inheritance hierarchies.