Swift
Abstract Functions
Programming
Swift Language
Software Development

Abstract functions in Swift Language

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Swift does not have an abstract keyword like Java or C#. Instead, Swift uses protocols to define abstract interfaces and protocol extensions to provide default implementations. A method declared in a protocol without a body is effectively abstract — any conforming type must implement it. For class hierarchies that need abstract base classes, Swift developers use protocols, fatalError() in base class methods, or a combination of both. This article covers all the patterns for achieving abstract function behavior in Swift.

Protocols as Abstract Interfaces

swift
1protocol Shape {
2    var name: String { get }
3    func area() -> Double
4    func perimeter() -> Double
5}
6
7struct Circle: Shape {
8    let name = "Circle"
9    let radius: Double
10
11    func area() -> Double {
12        return .pi * radius * radius
13    }
14
15    func perimeter() -> Double {
16        return 2 * .pi * radius
17    }
18}
19
20struct Rectangle: Shape {
21    let name = "Rectangle"
22    let width: Double
23    let height: Double
24
25    func area() -> Double {
26        return width * height
27    }
28
29    func perimeter() -> Double {
30        return 2 * (width + height)
31    }
32}
33
34// Usage
35let shapes: [Shape] = [Circle(radius: 5), Rectangle(width: 4, height: 6)]
36for shape in shapes {
37    print("\(shape.name): area = \(shape.area())")
38}

Protocol methods without bodies are abstract — conforming types must provide implementations.

Protocol Extensions for Default Implementations

swift
1protocol Logger {
2    var prefix: String { get }
3    func log(_ message: String)     // Abstract — must be implemented
4}
5
6extension Logger {
7    // Default implementation — can be overridden
8    func log(_ message: String) {
9        print("[\(prefix)] \(message)")
10    }
11
12    // Convenience method using the abstract property
13    func warn(_ message: String) {
14        log("WARNING: \(message)")
15    }
16
17    func error(_ message: String) {
18        log("ERROR: \(message)")
19    }
20}
21
22struct ConsoleLogger: Logger {
23    let prefix = "Console"
24    // Uses default log() implementation
25}
26
27struct FileLogger: Logger {
28    let prefix = "File"
29
30    // Override default implementation
31    func log(_ message: String) {
32        // Write to file instead
33        print("Writing to file: [\(prefix)] \(message)")
34    }
35}
36
37let logger: Logger = ConsoleLogger()
38logger.warn("Disk space low")
39// [Console] WARNING: Disk space low

Abstract Base Class with fatalError()

When you need class inheritance with abstract methods:

swift
1class Animal {
2    let name: String
3
4    init(name: String) {
5        self.name = name
6    }
7
8    // Abstract method — subclasses must override
9    func speak() -> String {
10        fatalError("Subclasses must implement speak()")
11    }
12
13    // Concrete method — shared implementation
14    func describe() -> String {
15        return "\(name) says: \(speak())"
16    }
17}
18
19class Dog: Animal {
20    init() { super.init(name: "Dog") }
21
22    override func speak() -> String {
23        return "Woof!"
24    }
25}
26
27class Cat: Animal {
28    init() { super.init(name: "Cat") }
29
30    override func speak() -> String {
31        return "Meow!"
32    }
33}
34
35let animals: [Animal] = [Dog(), Cat()]
36for animal in animals {
37    print(animal.describe())
38}
39// Dog says: Woof!
40// Cat says: Meow!
41
42// This crashes at runtime — not caught at compile time
43// let animal = Animal(name: "Generic")
44// animal.speak()  // fatalError!

The fatalError() approach works but has a major drawback: forgetting to override speak() in a subclass is not caught until runtime.

Protocol + Class Combination (Best Practice)

Combine a protocol for the abstract contract with a base class for shared state:

swift
1protocol Drawable {
2    func draw(on canvas: Canvas)
3    var bounds: CGRect { get }
4}
5
6class BaseView {
7    var frame: CGRect
8    var isHidden: Bool = false
9
10    init(frame: CGRect) {
11        self.frame = frame
12    }
13
14    func show() { isHidden = false }
15    func hide() { isHidden = true }
16}
17
18class CircleView: BaseView, Drawable {
19    var bounds: CGRect { return frame }
20
21    func draw(on canvas: Canvas) {
22        // Draw circle implementation
23    }
24}
25
26class SquareView: BaseView, Drawable {
27    var bounds: CGRect { return frame }
28
29    func draw(on canvas: Canvas) {
30        // Draw square implementation
31    }
32}

The protocol enforces draw() and bounds at compile time. The base class provides shared functionality (show(), hide()).

Associated Types for Generic Abstractions

swift
1protocol Repository {
2    associatedtype Item
3    associatedtype ID: Hashable
4
5    func findById(_ id: ID) -> Item?
6    func save(_ item: Item) -> ID
7    func delete(_ id: ID) -> Bool
8    func findAll() -> [Item]
9}
10
11class UserRepository: Repository {
12    typealias Item = User
13    typealias ID = String
14
15    private var storage: [String: User] = [:]
16
17    func findById(_ id: String) -> User? {
18        return storage[id]
19    }
20
21    func save(_ item: User) -> String {
22        storage[item.id] = item
23        return item.id
24    }
25
26    func delete(_ id: String) -> Bool {
27        return storage.removeValue(forKey: id) != nil
28    }
29
30    func findAll() -> [User] {
31        return Array(storage.values)
32    }
33}

Common Pitfalls

  • Relying on fatalError() for abstract enforcement: fatalError() crashes at runtime, not compile time. A subclass that forgets to override the method compiles successfully but crashes when called. Prefer protocols, which enforce implementation at compile time.
  • Putting abstract methods in protocol extensions: A method defined in a protocol extension provides a default implementation. If a conforming type does not override it, the default silently runs. Only declare methods in the protocol body (without extension) if they must be implemented by every conforming type.
  • Confusing protocol conformance with class inheritance: Protocols define what a type can do (interface). Classes define what a type is (hierarchy). In Swift, prefer protocols for abstraction and use class inheritance only when shared state or identity semantics are needed.
  • Making protocols too large: A protocol with 20 required methods is hard to conform to. Split large protocols into smaller, focused ones (Interface Segregation Principle). Use protocol composition (Drawable & Animatable) to combine them.
  • Forgetting that protocol methods use static dispatch in extensions: Methods declared in a protocol extension (not in the protocol itself) use static dispatch. This means the extension's version is called based on the variable's declared type, not the runtime type. Override behavior requires the method to be declared in the protocol body.

Summary

  • Swift has no abstract keyword — use protocols to define abstract interfaces
  • Protocol methods without bodies must be implemented by conforming types (compile-time enforcement)
  • Protocol extensions provide default implementations that conforming types can override
  • Use fatalError() in base class methods as a last resort — it only fails at runtime
  • Combine protocols (for abstraction) with base classes (for shared state) for the best of both worlds
  • Prefer protocols over class hierarchies — Swift is protocol-oriented by design

Course illustration
Course illustration

All Rights Reserved.