DEV Community

Gamya
Gamya

Posted on

Swift Structs โ€” Custom Initializers & the Power of self ๐Ÿ”ง

Every time you create a struct instance โ€” like AnimeCharacter(name: "Naruto", powerLevel: 9000) โ€” you're calling an initializer. Swift generates one for you automatically, but sometimes you need more control over how your struct is set up. That's where custom initializers come in. ๐Ÿฅ


The Default Memberwise Initializer

When you create a struct with properties, Swift automatically generates what's called a memberwise initializer โ€” a special function that accepts each property as a parameter, in the order they're defined:

struct Ninja {
    let name: String
    let village: String
    let rank: Int
}

let naruto = Ninja(name: "Naruto", village: "Konoha", rank: 7)
Enter fullscreen mode Exit fullscreen mode

Swift silently created Ninja(name:village:rank:) for us. We didn't have to write anything. That's convenient โ€” but sometimes we want to take control of that process ourselves.


Writing Your Own Initializer

Here's how you write a custom initializer that does the same thing as the default one:

struct Ninja {
    let name: String
    let village: String
    let rank: Int

    init(name: String, village: String, rank: Int) {
        self.name = name
        self.village = village
        self.rank = rank
    }
}
Enter fullscreen mode Exit fullscreen mode

A few important things to notice:

  • No func keyword โ€” initializers look like functions but Swift treats them specially
  • No return type โ€” an initializer always returns an instance of the struct it belongs to
  • self โ€” we use self.name to mean "the name property of this instance," as opposed to the name parameter coming in

That last point is really important. Without self, writing name = name would be confusing โ€” is that assigning the property to the parameter, or the other way around? self.name = name makes it crystal clear: "assign the parameter name to my own property name."


Why Use self?

self refers to the current instance of the struct โ€” the specific ninja, character, or object you're working with right now.

The most common place you'll use it is in initializers, exactly as above. Without it, you'd have to use awkward parameter naming to avoid the clash:

// Without self โ€” clunky parameter names:
init(ninjaName: String, ninjaVillage: String, ninjaRank: Int) {
    name = ninjaName
    village = ninjaVillage
    rank = ninjaRank
}

// With self โ€” clean, matching names:
init(name: String, village: String, rank: Int) {
    self.name = name
    self.village = village
    self.rank = rank
}
Enter fullscreen mode Exit fullscreen mode

The self version is much more readable. Outside of initializers, you generally don't need self โ€” Swift can figure out what you mean. But inside an initializer where property names and parameter names match, self is the clear, unambiguous solution.


The Golden Rule

There is one rule for initializers that Swift enforces strictly:

Every property must have a value by the time the initializer ends.

No exceptions. If you forget to assign a value to even one property, Swift will refuse to build your code.

struct Ninja {
    let name: String
    let village: String
    let rank: Int

    init(name: String, village: String) {
        self.name = name
        self.village = village
        // โŒ rank was never assigned โ€” Swift won't build this!
    }
}
Enter fullscreen mode Exit fullscreen mode

You can satisfy this rule by assigning a value directly in the initializer, or by giving the property a default value when you declare it.


Custom Initializers With Different Behavior

The real power of custom initializers is that they don't have to work the same way as the default one. For example, what if every new ninja is automatically assigned a random mission rank between 1 and 10?

struct Ninja {
    let name: String
    let village: String
    let missionRank: Int

    init(name: String, village: String) {
        self.name = name
        self.village = village
        self.missionRank = Int.random(in: 1...10) // assigned automatically!
    }
}

let sakura = Ninja(name: "Sakura", village: "Konoha")
print(sakura.missionRank) // random number between 1 and 10
Enter fullscreen mode Exit fullscreen mode

We provide name and village โ€” Swift generates missionRank for us inside the initializer. The golden rule is still satisfied because all three properties get a value before the initializer ends.


Multiple Initializers

You can have more than one initializer in a struct โ€” useful when you want to create instances in different ways:

struct Ninja {
    let name: String
    let village: String

    // Create a named ninja from a specific village
    init(name: String, village: String) {
        self.name = name
        self.village = village
    }

    // Create a mysterious unknown ninja
    init() {
        self.name = "???"
        self.village = "Unknown"
    }
}

let kakashi = Ninja(name: "Kakashi", village: "Konoha")
let mystery = Ninja() // name: "???", village: "Unknown"
Enter fullscreen mode Exit fullscreen mode

The Catch: Custom Initializers Remove the Default One

Here's something important: as soon as you write your own initializer, Swift removes the automatically generated memberwise initializer.

struct Ninja {
    let name: String
    let village: String

    init() {
        self.name = "???"
        self.village = "Unknown"
    }
}

let kakashi = Ninja(name: "Kakashi", village: "Konoha") // โŒ No longer available!
let mystery = Ninja() // โœ… Works fine
Enter fullscreen mode Exit fullscreen mode

Swift does this deliberately โ€” if you wrote a custom initializer, it assumes that's your intended setup process, and the default one might skip important steps.

If you want to keep both the default memberwise initializer and your custom one, move the custom initializer into an extension:

struct Ninja {
    let name: String
    let village: String
}

extension Ninja {
    init() {
        self.name = "???"
        self.village = "Unknown"
    }
}

let kakashi = Ninja(name: "Kakashi", village: "Konoha") // โœ… Still works!
let mystery = Ninja() // โœ… Custom one works too!
Enter fullscreen mode Exit fullscreen mode

By putting the custom initializer in an extension, Swift keeps the automatically generated memberwise initializer in place alongside it. Both are available. ๐ŸŽ‰


Wrap Up

Concept What It Means
Memberwise initializer Auto-generated by Swift โ€” accepts each property as a parameter
Custom initializer Written by you โ€” init() with no func keyword and no return type
self Refers to the current instance โ€” used to distinguish property names from parameter names
Golden rule Every property must have a value by the end of the initializer
Extension trick Add a custom init in an extension to keep the default memberwise init too

This article was written by me; AI was used to improve grammar and readability.


Top comments (0)