Swift
02 / 05

OOP, Protocols & Extensions

Swift: OOP, Protocols & Extensions

Classes vs Structs

  • Struct: value type — copied on assignment. Prefer structs for data models (String, Array, Int are all structs)

  • Class: reference type — shared reference. Use for identity-based models, inheritance, Objective-C interop

  • Struct is default choice in Swift — safer, no retain cycles, better with SwiftUI

  • Mutating methods: structs need `mutating` keyword on methods that modify self

// Struct (value type)
struct Point {
    var x: Double
    var y: Double

    mutating func translate(dx: Double, dy: Double) {
        x += dx
        y += dy
    }

    func distanceTo(_ other: Point) -> Double {
        let dx = x - other.x
        let dy = y - other.y
        return (dx*dx + dy*dy).squareRoot()
    }
}

// Class (reference type)
class Person {
    let id: UUID
    var name: String
    private(set) var age: Int   // public get, private set

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

    deinit {
        print("\(name) deallocated")
    }
}

class Employee: Person {
    var department: String

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

    override var description: String {
        "\(name) in \(department)"
    }
}

Protocols

// Protocol — like interface in other languages
protocol Drawable {
    var color: String { get }
    func draw() -> String
    func area() -> Double
}

protocol Identifiable {
    var id: UUID { get }
}

// Protocol composition
typealias DrawableIdentifiable = Drawable & Identifiable

// Protocol with default implementation (via extension)
protocol Greetable {
    var name: String { get }
    func greet() -> String
}

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

// Conformance
struct Circle: Drawable {
    let radius: Double
    var color: String

    func draw() -> String { "Circle(r=\(radius))" }
    func area() -> Double { .pi * radius * radius }
}

// Protocol as type (existential)
let shapes: [any Drawable] = [Circle(radius: 5, color: "red")]

// Generic constraint
func printArea<T: Drawable>(_ shape: T) {
    print("Area: \(shape.area())")
}

Extensions

// Add methods to existing types
extension String {
    var isPalindrome: Bool {
        self == String(self.reversed())
    }

    func truncated(to length: Int, ellipsis: String = "...") -> String {
        guard self.count > length else { return self }
        return String(self.prefix(length)) + ellipsis
    }
}

"racecar".isPalindrome   // true
"Hello, World!".truncated(to: 5)  // "Hello..."

// Retroactive conformance
extension Int: Drawable {
    var color: String { "black" }
    func draw() -> String { "\(self)" }
    func area() -> Double { 0 }
}

// Computed properties on Collection
extension Collection {
    var isNotEmpty: Bool { !isEmpty }
}

// Constrained extensions
extension Array where Element: Comparable {
    func isSorted() -> Bool {
        zip(self, dropFirst()).allSatisfy { $0 <= $1 }
    }
}

Property Wrappers & Result Builders

// Property wrappers (power SwiftUI @State, @Published, etc.)
@propertyWrapper
struct Clamped<T: Comparable> {
    private var value: T
    let range: ClosedRange<T>

    var wrappedValue: T {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }

    init(wrappedValue: T, _ range: ClosedRange<T>) {
        self.range = range
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}

struct Player {
    @Clamped(0...100) var health: Int = 100
}

var player = Player()
player.health = 150   // clamped to 100
player.health = -10   // clamped to 0

// Combine — reactive framework
import Combine

class ViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var results: [String] = []
    private var cancellables = Set<AnyCancellable>()

    init() {
        $searchText
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .sink { [weak self] text in
                self?.search(text)
            }
            .store(in: &cancellables)
    }
}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free