MemberwiseInit Swift Macro

repository·main·Indexed 41 days ago

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

A Swift Macro that enhances automatic memberwise initializers to reduce boilerplate. It allows developers to customize parameter labels, set default values for let properties, exclude specific properties, and generate public initializers. It includes the @MemberwiseInit macro for overall configuration and the @Init attribute for fine-grained property control, such as handling property wrappers like @State or marking closures as @escaping.

Tokens
5.7K
Snippets
26
Records
28
Agent score
53%

What's inside MemberwiseInit

  1. Infer types from property initialization expressions

    main

    The @MemberwiseInit macro operates at the syntax level and can infer property types from their initialization expressions. This allows you to omit explicit type annotations for many common Swift types, such as literals, ranges, arrays, dictionaries, and tuples.

    @MemberwiseInit
    struct Example {
      var count = 0  // Inferred as Int
      var string = "" // Inferred as String
      var boolTrue = true // Inferred as Bool
      var mixedDivide = 8.0 / 4 // Inferred as Double
      var arrayIntLiteral = [1, 2, 3] // Inferred as [Int]
    }
  2. Install MemberwiseInit

    main

    To use MemberwiseInit in your Swift project, follow these steps:

    Xcode Installation

    1. Go to File → Add Package Dependencies….
    2. Enter the package URL: https://github.com/gohanlon/swift-memberwise-init-macro.

    SPM Installation

    Add the package to your Package.swift dependencies:

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

    Then, add the MemberwiseInit product to all targets that require the macro:

    .product(name: "MemberwiseInit", package: "swift-memberwise-init-macro"),
    dependencies: [
      .package(url: "https://github.com/gohanlon/swift-memberwise-init-macro", from: "0.6.0")
    ]
    
    // In targets:
    .product(name: "MemberwiseInit", package: "swift-memberwise-init-macro")
  3. Expose private properties using @Init(.public)

    main

    If a struct has private properties, @MemberwiseInit(.public) will fail with a diagnostic error to prevent accidental access leaks. To deliberately include a private property in the generated initializer, mark the property with @Init(.public). This is useful when you want to allow external callers to set a value that is stored privately within the type.

    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
      @Init(.public) private var age: Int?  // Explicitly exposed in the public init
    }
  4. Set up Podman for Linux testing

    main

    The project uses Podman to run a full test matrix of 25 combinations (5 Swift versions × 5 swift-syntax versions) on Linux. Use the bin/setup-podman script to configure your environment.

    # First time, or to reconfigure (interactive)
    bin/setup-podman
    
    # Quick start (non-interactive)
    bin/setup-podman --auto
    
    # Check status
    bin/setup-podman --status
  5. Use @MemberwiseInit to generate public initializers

    main

    Swift's built-in memberwise initializers are always internal. Use the @MemberwiseInit macro to generate an initializer with a specific access level (e.g., .public).

    Unlike Swift's native behavior, adding @MemberwiseInit does not suppress the native memberwise initializer; they can coexist. This is useful for types conforming to Decodable or RawRepresentable where you want to keep the native behavior while adding a public interface.

    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
    }
  6. Exclude properties from the initializer using @Init(.ignore)

    main

    To prevent a property from appearing in the generated initializer, mark it with @Init(.ignore). This is typically used for properties that have a default value or are managed internally, ensuring the initializer remains clean and only includes relevant parameters.

    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
      @Init(.ignore) private var age: Int? = nil // age will not be a parameter in the init
    }
  7. Test specific swift-syntax versions locally

    main

    You can test specific ranges of swift-syntax locally by setting the SWIFT_SYNTAX_VERSION environment variable before running swift test.

    ```bash
    # Example for swift-syntax 600.x
    SWIFT_SYNTAX_VERSION="600.0.0..<601.0.0" swift test

    Available ranges:

    • 509.0.0..<510.0.0
    • 510.0.0..<511.0.0
    • 600.0.0..<601.0.0
    • 601.0.0..<602.0.0
    • 602.0.0..<603.0.0
    • 603.0.0..<604.0.0
    • 604.0.0-prerelease..<605.0.0
  8. Handle attributed properties (like @State) in MemberwiseInit

    main

    By default, @MemberwiseInit ignores properties with attributes (like SwiftUI's @State) to prevent common initialization pitfalls. If this causes a compiler error because a property is uninitialized, you have two options:

    1. Assign a default value: Provide a value in the declaration (e.g., @State var isOn: Bool = false).
    2. Explicitly include it: Use the @Init annotation to force the property into the initializer (e.g., @Init @State var isOn: Bool).
    // Option 1: Default value
    @MemberwiseInit
    struct MyView: View {
      @State var isOn: Bool = false
    }
    
    // Option 2: Explicitly include
    @MemberwiseInit
    struct MyView: View {
      @Init @State var isOn: Bool
    }
  9. Run the Linux test matrix with bin/test-linux

    main

    The bin/test-linux script executes the full compatibility matrix. It supports filtering by Swift version, controlling parallelism, and debugging specific failures.

    # Run full matrix with auto-detected parallelism
    make test-linux
    
    # Preview without running
    ./bin/test-linux --dry-run
    
    # Test specific combinations
    ./bin/test-linux --swift 6.0 --swift-syntax 600
    ./bin/test-linux --swift 6                    # All Swift 6.x versions
    
    # Override auto-detected parallelism
    ./bin/test-linux --parallel 4 --continue-on-error
    
    # Save logs for analysis
    ./bin/test-linux --parallel --continue-on-error --log-dir ./logs
    
    # Debug a specific failure
    ./bin/test-linux --swift 6.2 --swift-syntax 601 --verbose
    
    # Run sequentially (no parallelism)
    ./bin/test-linux --sequential
  10. Control optional defaulting with _optionalsDefaultNil

    main

    The experimental parameter _optionalsDefaultNil controls whether optional properties are automatically assigned a nil default value in the generated initializer.

    • _optionalsDefaultNil: true: All optional properties (including let optionals) will have a default value of nil in the initializer.
    • _optionalsDefaultNil: false: (Default) Ensures optional properties are not automatically defaulted to nil in public initializers.

    Note: Using @Init(default:) is generally a safer and more explicit alternative to this flag.

    @MemberwiseInit(.public, _optionalsDefaultNil: true)
    public struct User: Codable {
      public let id: Int
      public let name: String?
      public let email: String?
    }
    // Yields: public init(id: Int, name: String? = nil, email: String? = nil)
  11. Use @MemberwiseInit to generate initializers

    main

    The @MemberwiseInit macro automatically generates a memberwise initializer for a struct or class. By default, it generates an internal initializer. You can specify the access level for the generated initializer using arguments like .public or .internal.

    Key Behaviors:

    • Access Control: If you apply @MemberwiseInit(.public), the generated init will be public.
    • Property Wrappers: Properties with property wrappers (like @State) are ignored by default. To include them in the initializer, apply the @Init macro to the property.
    • Automatic Escaping: For closure types, the macro automatically adds @escaping to the initializer parameter if necessary.
    • Optionals: You can configure the macro to make optional properties default to nil in the initializer using the _optionalsDefaultNil: true argument.
    @MemberwiseInit(.public)
    public struct Person {
      public let name: String
      private var age: Int? = nil
    }
    // Generates: public init(name: String, age: Int? = nil) { ... }