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
structoverclass— value semantics suffice for most data. - Use
classwhen identity, inheritance, or shared mutation matters. - Mark classes
finalby default — admit subclassing only when intended. - Use
private initfor singletons or factory-only classes. - Use
convenience initfor substantial initialisation flexibility. - Use
failable init(init?) for “construction may fail”. - Use
required initfor protocol-required initialisers. - Use property observers (
didSet) for reactive patterns. - Use computed properties for derived values.
- Use
lazyfor substantial deferred initialisation. - Use protocols with default implementations over deep inheritance hierarchies.
- Use
class(notstatic) for overridable type members. - Use
super.initlast 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.