Methods and Subscripts in Swift: Instance, Type, and Mutating


On the Scare Floor at Monsters, Inc., every monster has their own signature move — Sulley roars, Mike cracks jokes, Randall turns invisible. But some rules apply to the whole company: the daily scare quota, the all-hands meeting protocol. In Swift, methods are functions that belong to a type, and they follow the same split — some act on individual instances, others operate at the type level.

This guide covers instance methods, mutating methods, type methods, and subscripts. We won’t get into protocol method requirements or extensions — those each have their own posts.

What You’ll Learn

What Are Methods?

A method is simply a function that’s defined inside a type — a struct, class, or enum. The key difference between a method and a regular function is that methods have access to self, a reference to the instance they belong to.

Think of it this way: a regular function is a freelancer — it can work anywhere. A method is an employee — it’s tied to a specific company (type) and has access to the company’s internal data (properties).

Apple Docs: Methods — The Swift Programming Language

Instance Methods

An instance method is a function that operates on a specific instance of a type. It can read and (sometimes) modify the instance’s properties through self.

Let’s build a Monster struct for the Scare Floor:

struct Monster {
    let name: String
    var scareCount: Int

    func roar() {
        print("\(name) lets out a terrifying roar!")
    }

    func report() {
        print("\(name) has \(scareCount) scares.")
    }
}

let sulley = Monster(name: "Sulley", scareCount: 42)
sulley.roar()
sulley.report()
Sulley lets out a terrifying roar!
Sulley has 42 scares.

Both roar() and report() are instance methods. They access the instance’s name and scareCount properties through self. Swift lets you omit self when the meaning is clear, so name is shorthand for self.name.

Tip: You only need to write self explicitly when a parameter name conflicts with a property name, or inside closures that capture self.

Mutating Methods

Structs are value types, which means their properties are immutable by default inside methods. If you want a method to modify a property, you must mark it as mutating.

Think of it as asking permission to change the scoreboard — regular employees can read it, but only a manager (a mutating method) can update the numbers:

struct Monster {
    let name: String
    var scareCount: Int

    mutating func performScare() {
        scareCount += 1
        print("\(name) scared a kid! Total: \(scareCount)")
    }
}

var sulley = Monster(name: "Sulley", scareCount: 42)
sulley.performScare()
sulley.performScare()
Sulley scared a kid! Total: 43
Sulley scared a kid! Total: 44

The mutating keyword tells Swift that this method will modify the struct’s properties. Without it, the compiler would reject the scareCount += 1 line.

Note: Classes don’t need mutating because they’re reference types — their methods can always modify properties. The mutating keyword is specific to structs and enums.

Assigning to self

A mutating method can even replace the entire instance by assigning a new value to self:

struct ScareRecord {
    var topScore: Int

    mutating func reset() {
        self = ScareRecord(topScore: 0)
    }
}

var record = ScareRecord(topScore: 99)
record.reset()
print(record.topScore)
0

This creates an entirely new ScareRecord and replaces the current one. It’s useful when you want a clean-slate reset.

Type Methods

A type method belongs to the type itself, not to any particular instance. You call it on the type name, not on an instance. Declare one with static:

struct Monster {
    static var totalMonsters = 0

    let name: String

    init(name: String) {
        self.name = name
        Monster.totalMonsters += 1
    }

    static func headCount() {
        print("Monsters on staff: \(totalMonsters)")
    }
}

let sulley = Monster(name: "Sulley")
let mike = Monster(name: "Mike")
Monster.headCount()
Monsters on staff: 2

headCount() is called on Monster itself — not on sulley or mike. It accesses the type property totalMonsters directly because type methods can access other type properties.

static vs class

In classes, you have two options for type methods:

  • static func — cannot be overridden by subclasses.
  • class func — can be overridden by subclasses.
class Employee {
    class func companyMotto() -> String {
        "We scare because we care."
    }
}

class TopScarer: Employee {
    override class func companyMotto() -> String {
        "We scare because we're the best."
    }
}

print(Employee.companyMotto())
print(TopScarer.companyMotto())
We scare because we care.
We scare because we're the best.

If companyMotto() were declared with static, the override in TopScarer would cause a compiler error. Use class func when you want subclasses to customize behavior.

Tip: Structs and enums only support static — they can’t be subclassed, so class func doesn’t apply to them.

Subscripts

A subscript lets you access elements of a type using bracket syntax — just like you access array elements with array[0]. You can define custom subscripts to give your types the same convenient bracket access.

Imagine a Scare Floor scoreboard where you look up a monster’s score by their position:

struct ScoreBoard {
    let scores: [String: Int]

    subscript(monster: String) -> Int {
        scores[monster] ?? 0
    }
}

let board = ScoreBoard(scores: [
    "Sulley": 42,
    "Mike": 15,
    "Randall": 39
])
print(board["Sulley"])
print(board["Roz"])
42
0

The subscript takes a String key and returns the matching score, or 0 if the monster isn’t on the board. Callers use the familiar bracket syntax board["Sulley"] instead of calling a method.

Subscripts with integer indices

You can also define subscripts that take integers, making your custom type behave like an array:

struct ScareLine {
    let monsters: [String]

    subscript(index: Int) -> String {
        monsters[index]
    }
}

let line = ScareLine(monsters: ["Sulley", "Mike", "Randall"])
print(line[0])
print(line[2])
Sulley
Randall

Note: Subscripts can be read-write (with get and set blocks) or read-only (just a single return expression), just like computed properties.

Common Mistakes

Forgetting mutating on a struct method

If a struct method modifies a property without the mutating keyword, the compiler rejects it:

struct Monster {
    var scareCount: Int

    // ❌ Error: cannot assign to property: 'self' is immutable
    func performScare() {
        scareCount += 1
    }
}

The fix is simple — add mutating:

struct Monster {
    var scareCount: Int

    // ✅ Mark the method as mutating
    mutating func performScare() {
        scareCount += 1
    }
}

Confusing static func vs class func

Beginners sometimes use static func in a class and then wonder why they can’t override it in a subclass:

class Employee {
    // ❌ Using static means subclasses cannot override this
    static func motto() -> String {
        "We scare because we care."
    }
}

class TopScarer: Employee {
    // Error: cannot override static method
    override static func motto() -> String {
        "We're the best."
    }
}

If you need subclasses to override the method, use class func instead:

class Employee {
    // ✅ Use class func to allow overrides
    class func motto() -> String {
        "We scare because we care."
    }
}

The rule is straightforward: static = final (no overrides), class = overridable.

What’s Next?

  • Instance methods are functions bound to a type that can access self.
  • Mutating methods let struct methods modify properties — required for value types.
  • Type methods (static func / class func) belong to the type, not an instance.
  • Subscripts let you define bracket-access syntax for your custom types.

You’ve learned how to attach both data (properties) and behavior (methods) to your types. Next, let’s look at how those types come to life in the first place. Head over to init in Swift to learn about initializers, convenience inits, and deinit.