Skip to content

gohanlon/swift-memberwise-init-macro

Repository files navigation

@MemberwiseInit

GitHub Workflow Status (with event)

A Swift Macro for enhanced automatic memberwise initializers, greatly reducing manual boilerplate:

swift-memberwise-init-hero04

Informed by explicit developer cues, MemberwiseInit can more often automatically provide your intended memberwise init, while following the same safe-by-default semantics underlying Swift’s memberwise initializers.

Important

@MemberwiseInit is a Swift Macro requiring swift-tools-version: 5.9 or later (Xcode 15 onwards).

Quick start

To use MemberwiseInit:

  1. Installation
    In Xcode, add MemberwiseInit with: FileAdd Package Dependencies… and input the package URL:

    https://github.com/gohanlon/swift-memberwise-init-macro

    Or, for SPM-based projects, add it to your package dependencies:

    dependencies: [
      .package(url: "https://github.com/gohanlon/swift-memberwise-init-macro", from: "0.1.1")
    ]

    And then add the product to all targets that use MemberwiseInit:

    .product(name: "MemberwiseInit", package: "swift-memberwise-init-macro"),
  2. Import & basic usage
    After importing MemberwiseInit, add @MemberwiseInit before your type definition. This will mirror Swift’s behavior: it provides an initializer with up to internal access, but scales down if any properties are more restrictive. Here, age being private makes the initializer private too:

    import MemberwiseInit
    
    @MemberwiseInit
    struct Person {
      let name: String
      private var age: Int? = nil
    }
  3. Customize visibility
    Make the struct public and use @MemberwiseInit(.public) to enable up to a public initializer. At this point, the init will still be private because age is private.

    @MemberwiseInit(.public)
    public struct Person {
      let name: String
      private var age: Int? = nil
    }

    Make name public instead of internal, and tell MemberwiseInit to ignore age with @Init(.ignore):

    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
      @Init(.ignore) private var age: Int? = nil
    }

    Alternatively, you can use @Init(.public) to include and expose age publicly in the init:

    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
      @Init(.public) private var age: Int? = nil
    }

Quick reference

MemberwiseInit includes two autocomplete-friendly macros:

@MemberwiseInit

Attach to struct, actor (experimental), or class (experimental).

  • @MemberwiseInit
    Provide up to an internal memberwise init, closely mimicking Swift’s memberwise init.

  • @MemberwiseInit(.public)
    Provide a memberwise init with up to the provided access level. Valid access levels: .private, .fileprivate, .internal, .public, .open.

  • @MemberwiseInit(_deunderscoreParameters: true) (experimental)
    Drop underscore prefix from generated init parameter names, unless doing so would result in a naming conflict. (Ignored properties won’t contribute to conflicts.)

  • @MemberwiseInit(_optionalsDefaultNil: true) (experimental)
    When set to true, give all optional properties a default init parameter value of nil. For non-public initializers, optional var properties default to nil unless this parameter is explicitly set to false.

@Init

Attach to member property declarations of a struct, actor, or class that @MemberwiseInit is providing an init for.

  • @Init
    Include a property that would otherwise be ignored, e.g., attributed properties such as SwiftUI’s @State properties.

  • @Init(.ignore)
    Ignore that member property. The access level of an ignored property won’t affect that of the provided init, and the property won’t be included in the init. Note: Ignored properties must be initialized elsewhere.

  • @Init(.public)
    For calculating the provided init’s access level, consider the property as having a different access level than its declared access level. Valid access levels: .private, .fileprivate, .internal, .public, .open.

  • @Init(.escaping)
    To avoid compiler errors when a property’s init argument can’t automatically be @escaped, e.g. when a property’s type uses a typealias that represents a closure.

  • @Init(.public, .escaping)
    Access level and escaping behaviors can be used together.

  • @Init(label: String)
    Assigns a custom parameter label in the provided init.

    • Use @Init(label: "_") to make the init parameter label-less.
    • Diagnostic errors arise from invalid labels, or conflicts among properties included in the init. (Ignored properties don’t cause conflicts.)
    • Overrides MemberwiseInit’s experimental _deunderscoreParameters behavior.
  • @Init(.public, label: String)
    Custom labels can be combined with all other behaviors.

Features and limitations

Custom init parameter labels

To control the naming of parameters in the provided initializer, use @Init(label: String). Tip: For a label-less parameter, use @Init(label: "_").

Explanation

Customize your initializer parameter labels with @Init(label: String):

  1. Label-less parameters

    @MemberwiseInit
    struct Point2D {
      @Init(label: "_") let x: Int
      @Init(label: "_") let y: Int
    }

    Yields:

    init(
      _ x: Int,
      _ y: Int
    ) {
      self.x = x
      self.y = y
    }
  2. Custom parameter labels

    @MemberwiseInit
    struct Receipt {
      @Init(label: "for") let item: String
    }

    Yields:

    init(
      for item: String  // 👈
    ) {
      self.item = item
    }

Explicitly ignore properties

Use @Init(.ignore) to exclude a property from MemberwiseInit’s initializer; ensure ignored properties are otherwise initialized to avoid compiler errors.

Explanation

The @Init(.ignore) attribute excludes properties from the initializer, potentially allowing MemberwiseInit to produce a more accessible initializer for the remaining properties.

For example:

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  @Init(.ignore) private var age: Int? = nil  // 👈 Ignored and given a default value
}

By marking age as ignored, MemberwiseInit creates a public initializer without the age parameter:

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

If age weren't marked as ignored, the initializer would be private and would include the age property.

Note In line with Swift’s memberwise initializer, MemberwiseInit automatically ignores let properties with assigned default values, as reassigning such properties within the initializer would be invalid.

Attributed properties are ignored by default, but includable

If MemberwiseInit ignores an attributed property and causes a compiler error, you have two immediate remedies:

  1. Assign a default value to the property.
  2. Explicitly include the property in the initializer using the @Init annotation.

Explanation

Unlike the compiler’s default behavior, MemberwiseInit takes a more cautious approach when dealing with member properties that have attributes attached.

For a SwiftUI-based illustration, let’s look at a view without MemberwiseInit:

import SwiftUI
struct MyView: View {
  @State var isOn: Bool

  var body: some View {  }
}

Swift provides the following memberwise init:

internal init(
  isOn: Bool
) {
  self.isOn = isOn
}

However, initializing @State properties in this manner is a common pitfall in SwiftUI. The isOn state is only assigned upon the initial rendering of the view, and this assignment doesn’t occur on subsequent renders. To safeguard against this, MemberwiseInit defaults to ignoring attributed properties:

import SwiftUI
@MemberwiseInit  // 👈
struct MyView: View {
  @State var isOn: Bool

  var body: some View {  }
}

This leads MemberwiseInit to provided the following initializer:

internal init() {
}  // 🛑 Compiler error:↵
// Return from initializer without initializing all stored properties

From here, you have two alternatives:

  1. Assign a default value
    Defaulting the property to a value makes the provided init valid, as the provided init no longer needs to initialize the property.

    import SwiftUI
    @MemberwiseInit
    struct MyView: View {
      @State var isOn: Bool = false  // 👈 Default value provided
    
      var body: some View {  }
    }

    The resulting init is:

    internal init() {
    }  // 🎉 No error, all stored properties are initialized
  2. Use @Init annotation
    If you understand the behavior the attribute imparts, you can explicitly mark the property with @Init to include it in the initializer.

    import SwiftUI
    @MemberwiseInit
    struct MyView: View {
      @Init @State var isOn: Bool  // 👈 `@Init`
    
      var body: some View {  }
    }

    This yields:

    internal init(
      isOn: Bool
    ) {
      self.isOn = isOn
    }

Automatic @escaping for closure types (usually)

MemberwiseInit automatically marks closures in initializer parameters as @escaping. If using a typealias for a closure, explicitly annotate the property with @Init(.escaping).

Explanation

Swift Macros operate at the syntax level and don’t inherently understand type information. MemberwiseInit will add @escaping for closure types, provided that the closure type is directly declared as part of the property. Fortunately, this is the typical scenario.

In contrast, Swift’s memberwise initializer has the advantage of working with type information. This allows it to recognize and add @escaping even when the closure type is “obscured” within a typealias.

Consider the following struct:

public struct TaskRunner {
  public let onCompletion: () -> Void
}

Through observation (or by delving into the compiler’s source code), we can see that Swift automatically provides the following internal init:

internal init(
  onCompletion: @escaping () -> Void  // 🎉 `@escaping` automatically
) {
  self.onCompletion = onCompletion
}

Now, with MemberwiseInit:

@MemberwiseInit  // 👈
public struct TaskRunner {
  public let onCompletion: () -> Void
}

we get the same init, which we can inspect using Xcode’s “Expand Macro” command:

internal init(
  onCompletion: @escaping () -> Void  // 🎉 `@escaping` automatically
) {
  self.onCompletion = onCompletion
}

And we can have MemberwiseInit provide a public init:

@MemberwiseInit(.public)  // 👈 `.public`
public struct TaskRunner {
  public let onCompletion: () -> Void
}

This yields:

public init(  // 🎉 `public`
  onCompletion: @escaping () -> Void
) {
  self.onCompletion = onCompletion
}

Now, suppose the type of onCompletion got more complex and we decided to extract a typealias:

public typealias CompletionHandler = @Sendable () -> Void

@MemberwiseInit(.public)
public struct TaskRunner: Sendable {
  public let onCompletion: CompletionHandler
}

Because Swift Macros don’t inherently understand type information, MemberwiseInit cannot “see” that CompletionHandler represents a closure type that needs to be marked @escaping. This leads to a compiler error:

public init(
  onCompletion: CompletionHandler  // 👈 Missing `@escaping`!
) {
  self.onCompletion = onCompletion  // 🛑 Compiler error:↵
  // Assigning non-escaping parameter 'onCompletion' to an @escaping closure
}

To address this, when using a typealias for closures, you must explicitly mark the property with @Init(.escaping):

public typealias CompletionHandler = @Sendable () -> Void

@MemberwiseInit(.public)
public struct TaskRunner: Sendable {
  @Init(.escaping) public let onCompletion: CompletionHandler  // 👈
}

which results in the following valid and inspectable public init:

public init(
  onCompletion: @escaping CompletionHandler  // 🎉 Correctly `@escaping`
) {
  self.onCompletion = onCompletion
}

Experimental: Deunderscore parameter names

Note Prefer using @Init(label:) at the property level to explicitly specify non-underscored names—@MemberwiseInit(_deunderscoreParmeters:) may be deprecated soon.

Set @MemberwiseInit(_deunderscoreParmeters: true) to strip the underscore prefix from properties when generating initializer parameter names. If you wish to maintain the underscore or provide a custom label on a particular property, use @Init(label: String).

If the removal of the underscore would lead to a naming conflict among the properties included in the initializer, MemberwiseInit will not strip the underscore. (Ignored properties won’t contribute to conflicts.)

Explanation

In Swift, properties prefixed with an underscore are conventionally used as internal storage or backing properties. Setting _deunderscoreParameters: true respects this convention, producing initializer parameter names that omit the underscore:

@MemberwiseInit(.public, _deunderscoreParmeters: true)
public struct Review {
  @Init(.public) private let _rating: Int

  public var rating: String {
    String(repeating: "⭐️", count: self._rating)
  }
}

This yields:

public init(
  rating: Int  // 👈 Non-underscored parameter
) {
  self._rating = rating
}

To override the deunderscore behavior at the property level, use @Init(label: String):

@MemberwiseInit(.public, _deunderscoreParameters: true)
public struct Review {
  @Init(.public, label: "_rating") private let _rating: Int
}

This yields:

public init(
  _rating: Int  // 👈 Underscored parameter
) {
  self._rating = _rating
}

Experimental: Defaulting optionals to nil

Use @MemberwiseInit(_optionalsDefaultNil: Bool) to explicitly control whether optional properties are defaulted to nil in the provided initializer:

  • Set _optionalsDefaultNil: true to default all optional properties to nil, trading off compile-time guidance.
  • Set _optionalsDefaultNil: false to ensure that MemberwiseInit never defaults optional properties to nil.

The default behavior of MemberwiseInit regarding optional properties aligns with Swift’s memberwise initializer:

  • For non-public initializers, var optional properties automatically default to nil.
  • For public initializers, MemberwiseInit follows Swift’s cautious approach to public APIs by requiring all parameters explicitly, including optionals, unless _optionalsDefaultNil is set to true.
  • let optional properties are never automatically defaulted to nil. Setting _optionalsDefaultNil to true is the only way to cause them to default to nil.

Note @Init(default:) is a planned future enhancement to generally specify default values, and will be a safer, more explicit alternative to _optionalsDefaultNil.

Explanation

With _optionalsDefaultNil, you gain control over a default behavior of Swift’s memberwise init. And, it allows you to explicitly opt-in to your public initializer defaulting optional properties to nil.

Easing instantiation is the primary purpose of _optionalsDefaultNil, and is especially useful when your types mirror a loosely structured external dependency, e.g. Codable structs that mirror HTTP APIs. However, _optionalsDefaultNil has a drawback: when properties change, the compiler won’t flag outdated instantiations, risking unintended nil assignments and potential runtime errors.

In Swift:

  • var property declarations that include an initial value naturally lead to default memberwise init parameter values in both Swift’s and MemberwiseInit’s initializers.
  • let properties assigned a value at declaration become immutable, so they can’t be leveraged to specify default init parameter values.

For instance, var property declarations can be initialized to nil:

@MemberwiseInit(.public)
public struct User {
  public var name: String? = nil  // 👈
}
_ = User()  // 'name' defaults to 'nil'

Yields:

public init(
  name: String? = nil  // 👈
) {
  self.name = name
}

This isn’t feasible for let properties:

@MemberwiseInit(.public)
public struct User {
  public let name: String? = nil  // ✋ 'name' is 'nil' forever
}

While MemberwiseInit doesn’t (yet) solve for default values of let properties, _optionalsDefaultNil serves the specific case of defaulting optional properties to nil:

@MemberwiseInit(.public, _optionalsDefaultNil: true)
public struct User: Codable {
  public let id: Int
  public let name: String?
  public let email: String?
  public let address: String?
}

Yields:

public init(
  id: Int,
  name: String? = nil,
  email: String? = nil,
  address: String? = nil
) {
  self.id = id
  self.name = name
  self.email = email
  self.address = address
}

Tuple destructuring in property declarations isn’t supported (yet)

Using tuple syntax in property declarations isn’t supported:

@MemberwiseInit
struct Point2D {
  let (x, y): (Int, Int)
//┬─────────────────────
//╰─ 🛑 @MemberwiseInit does not support tuple destructuring for
//     property declarations. Use multiple declartions instead.
}

Background

Swift’s automatically provided memberwise initializers deftly cut down on boilerplate for structs. Yet, they must always error on the side of caution to ensure no presumptions are made about the developer’s intent. While this conservative approach is essential for avoiding unintended behaviors, it too often leads back to using boilerplate initializers.

Swift’s memberwise initializer can’t assume that a public type should be constructible from external modules, so it never provides an initializer having an access level greater than “internal.” To safely add a public initializer to a type requires an explicit developer intent. Traditionally, that means manually declaring an initializer, or using Xcode to generate a boilerplate initializer. Take this simple example:

public struct Person {
  public let name: String
}

Swift transparently adds the following, familiar init:

internal init(
  name: String
) {
  self.name = name
}

MemberwiseInit can provide the exact same init:

@MemberwiseInit  // 👈
public struct Person {
  public let name: String
}

Unlike Swift’s memberwise initializer, you can inspect MemberwiseInit’s initializer using Xcode by right clicking on @MemberwiseInit and the selecting “Expand Macro”.

Note Introducing an explicit init suppresses the addition of Swift’s memberwise initializer. MemberwiseInit’s initializer is always added and can coexist with your other initializers, even for types directly conforming to init-specifying protocols like Decodable and RawRepresentable.1

In contrast to Swift’s memberwise initializer, MemberwiseInit can provide an initializer up to any access level, including public. You explicitly allow it to provide a public init by marking Person with @MemberwiseInit(.public):

@MemberwiseInit(.public)  // 👈 `.public`
public struct Person {
  public let name: String
}

With this adjustment, expanding the macro yields:

public init(  // 🎉 `public`
  name: String
) {
  self.name = name
}

Suppose you then added a private member to Person:

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  private var age: Int?  // 👈 `private`
}

Now MemberwiseInit, as Swift would, provides a private init:

private init(  // 👈 `private`
  name: String,
  age: Int?
) {
  self.name = name
  self.age = age
}

The reason this init is private is foundational to understanding both Swift’s and MemberwiseInit’s memberwise initializer. By default, they both provide an initializer that will never unintentionally leak access to more restricted properties.

To publicly expose age via MemberwiseInit’s initializer, mark it with @Init(.public):

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  @Init(.public) private var age: Int?  // 👈 `@Init(.public)`
}

and now MemberwiseInit provides a public init that exposes the private age property:

public init(  // 👈 `public`
  name: String,
  age: Int?  // 👈 Exposed deliberately
) {
  self.name = name
  self.age = age
}

Let’s give age a default value:

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  @Init(.public) private var age: Int? = nil  // 👈 Default value
}

and now MemberwiseInit’s init parameter includes the default age value:

public init(
  name: String,
  age: Int? = nil  // 👈 Default value
) {
  self.name = name
  self.age = age
}

Suppose we don’t want to expose age publicly via the init. As long as age is initialized in another way (e.g. declared with a default value), we can explicitly tell MemberwiseInit to ignore it using @Init(.ignore):

@MemberwiseInit(.public)
public struct Person {
  public let name: String
  @Init(.ignore) private var age: Int? = nil  // 👈 `.ignore`
}

Now MemberwiseInit ignores the private age property and provides a public init:

public init(  // 👈 `public`, ignoring `age` property
  name: String
) {
  self.name = name
}

License

MemberwiseInit is available under the MIT license. See the LICENSE file for more info.

Footnotes

  1. Swift omits its memberwise initializer when any explicit init is present. You can do an “extension dance” to retain Swift’s memberwise init, but with imposed tradeoffs.