Swift 6.4: Smarter Member wise Initialisers

Swift’s synthesised member wise initialiser is one of those features you rarely think about—until it stops working. For years, adding a single private stored property, even one with a default value, caused the synthesized initializer to become private, forcing developers to write boilerplate initializers just to preserve the original API. Swift 6.4 changes that with SE-0502, making memberwise initializer synthesis smarter while preserving Swift’s access control model.


What is a Memberwise Initializer?

  • When you create a struct, Swift automatically writes an init for all its stored properties.
  • You get this for free — no code required.
  • It has been around since Swift 1.0.
struct User {
    var name: String
    var age: Int
}

// Swift auto-generates this behind the scenes:
// init(name: String, age: Int)

let user = User(name: "Sarah", age: 28) // ✅ Works 

Default Values Make It Even Nicer

  • If a property has a default value, Swift adds it as a default argument in the generated init.
  • This means callers can skip providing that value if the default is good enough.
  • Introduced in Swift 5.1 via SE-0242.
struct User {
    var name: String
    var age: Int
    var isActive: Bool = true
}

// Swift generates:
// init(name: String, age: Int, isActive: Bool = true)

let user = User(name: "Sarah", age: 28) // ✅ isActive defaults to true 

The Problem: One private Property Broke Everything

Here’s where things went wrong. Make that isActive property private:

struct User {
    var name: String
    var age: Int
    private var isActive: Bool = true
} 
  • You would still expect User(name: “Sarah”, age: 28) to work.
  • But before Swift 6.4, it doesn’t.
  • Swift included isActive in the memberwise init — and because isActive is private, the entire init became private too.

What Swift actually generated:

// ❌ The whole init is now private!
private init(name: String, age: Int, isActive: Bool = true) 

Trying to create a User from outside the struct gives this error:

❌ error: 'User' initializer is inaccessible
   due to 'private' protection level 

One internal flag, with a default value, took down your entire public-facing initializer.


The Old Workaround: Write It Yourself

The only fix was to manually write the init you expected in the first place:

struct User {
    var name: String
    var age: Int
    private var isActive: Bool = true

    // Had to write this manually 😩
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
} 
  • This works, but it’s pure boilerplate.
  • It doesn’t do anything interesting — it just copies two values.
  • Thousands of Swift projects have code exactly like this sitting in them right now.

The Fix in Swift 6.4: SE-0502

Swift 6.4 ships SE-0502: Exclude private initialized properties from memberwise initializer.

The new rule:

A property is excluded from the memberwise initializer when it is less accessible than the initializer would be, and it already has an initial value.

Both conditions must be true. If they are, the property is simply left out — it keeps its default value, stays private, and callers never need to know it exists.

struct User {
    var name: String
    var age: Int
    private var isActive: Bool = true
}

// Swift 6.4 now generates:
// ✅ init(name: String, age: Int)

let user = User(name: "Sarah", age: 28) // ✅ Works, no manual init needed 

What Counts as “Having an Initial Value”?

Not just explicit values — Swift is smarter than that:

Explicit default:

private var score: Int = 0          // ✅ excluded
private var tags: [String] = []     // ✅ excluded
private var label: String = "none"  // ✅ excluded 

Implicit default (Optionals start as nil):

private var nickname: String?       // ✅ excluded — nil is the implicit default
private var cachedData: Data?       // ✅ excluded 

No default — still included:

private let id: UUID                // ❌ still in the init, no default exists 

The Rule That Still Applies: No Default = Still Included

If a private property has no default value, Swift still has to include it in the init — because there’s no value to fall back on.

struct User {
    var name: String
    private let id: UUID            // no default value
}

// Still generates a private init:
// private init(name: String, id: UUID) 

In this case, you still need to write your own init to hide id from callers:

struct User {
    var name: String
    private let id: UUID

    init(name: String) {
        self.name = name
        self.id = UUID()            // you decide how id is set
    }
} 

This makes total sense — Swift can’t make up a value for id, so it has to ask for one.


Two Initializers Now Exist Behind the Scenes

Conceptually, Swift 6.4 creates two versions of the memberwise init for the same struct:

struct User {
    var name: String
    var age: Int
    private var isActive: Bool = true
} 

Who uses it What it looks like Outside the struct init(name: String, age: Int) Inside the struct init(name: String, age: Int, isActive: Bool)

Try it in Xcode:

  • Type User( from outside the struct → autocomplete shows only name and age
  • Type Self( from inside the struct → autocomplete shows all three, including isActive

What About fileprivate?

  • The same rule applies to fileprivate properties.
  • If a fileprivate property has a default value and is less accessible than the init, it gets excluded too.
  • Only properties that are already at internal or above participate normally.

Public Structs — Nothing Changed Here

This improvement does not give you a free public initializer. That rule hasn’t changed:

public struct User {
    public var name: String
    public var age: Int
    private var isActive: Bool = true
} 
  • Code inside your own module can now create User(name:age:) — the private property no longer blocks that.
  • But consumers of your Swift package or framework still cannot create a User unless you write an explicit public init.
  • This is intentional — your public API should always be deliberate.
public struct User {
    public var name: String
    public var age: Int
    private var isActive: Bool = true

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

Why This Also Matters for Macros

  • Macros often add hidden private storage to a type behind the scenes.
  • Something like private var _backing: Storage = .init().
  • Before Swift 6.4, that generated property could silently make the struct’s memberwise init private — breaking code the developer never touched.
  • Now, as long as the generated property has a default value, it stays out of the way.
  • Macro authors can safely attach hidden state without unexpected side effects for users.

Source Compatibility Note

  • Swift 6.4 keeps a compatibility overload for the old behavior temporarily.
  • Existing code that passes private properties through the old synthesized init won’t immediately break.
  • But a future language mode may remove this — so if your code relies on it, write an explicit private init now.
// If you do this internally, make it explicit instead of relying on synthesis:
func resetUser() -> User {
    User(name: name, age: age, isActive: false) // ⚠️ won't work in future
}

// Better — write an explicit private init:
private init(name: String, age: Int, isActive: Bool) { ... } 

Quick Summary

Scenario Before Swift 6.4 Swift 6.4 private var x = 0 in a struct Init becomes private x is excluded, init stays internal private var x: String? Init becomes private x is excluded (nil is default) private let x: UUID (no default) Init becomes private Still private — no default exists public struct No public init Still no public init — must be explicit


The Takeaway

Article content
  • private properties with default values are implementation details — callers shouldn’t care about them.
  • Swift 6.4 finally treats them that way, keeping them out of the synthesized memberwise init.
  • You can now delete a lot of manual boilerplate inits that existed only to work around this limitation.
  • The best language improvements aren’t always new syntax — sometimes they’re the ones that let you quietly delete code.

Leave a Reply

Your email address will not be published. Required fields are marked *