gohanlon / swift-memberwise-init-macro

Swift Macro for enhanced automatic inits.
MIT License
122 stars 8 forks source link

Add fluent property wrapper initialization #17

Closed gohanlon closed 1 year ago

gohanlon commented 1 year ago

With #13's addition of @Init(assignee:type:), MemberwiseInit's @Init became expressive enough to support property-wrapped members[^1].

But, MemberwiseInit should provide a cleaner way to initialize properties wrapped by property wrappers. Further, @Init has become burdened by some seldomly needed parameters and is now Xcode autocomplete-unfriendly. We can improve the situation somewhat.

Here are the new macro definitions:

// An escape hatch that embraces the "template" nature of the macro and directly exposes it's configuration.
// 6 arguments
public macro InitRaw(
  _ accessLevel: AccessLevelConfig? = nil,
  assignee: String? = nil,
  default: Any? = nil,  // forward looking
  escaping: Bool? = nil,
  label: String? = nil,
  type: Any.Type? = nil
) = …

// To simplify common usage, forgo 'assignee' and 'type'.
// 4 arguments
public macro Init(
  _ accessLevel: AccessLevelConfig? = nil,
  default: Any? = nil,  // forward looking
  escaping: Bool? = nil,
  label: String? = nil
) = …

// Use the 'assignee' of 'self._\(#propertyName)'.
// 5 arguments
public macro InitWrapper(
  _ accessLevel: AccessLevelConfig? = nil,
  default: Any? = nil,  // forward looking
  escaping: Bool? = nil,,
  label: String? = nil,
  type: Any.Type  // NB: 'type' is required because it can't be inferred, and will always be different than that of the wrapped property
) = …

Example:

import SwiftUI

@propertyWrapper
struct Logged<Value> {
  var wrappedValue: Value {
    didSet {
      print("Logged: \(wrappedValue)")
    }
  }

  init(wrappedValue: Value) {
    self.wrappedValue = wrappedValue
  }
}

// NB: Some property wrappers require initialization of the property
// wrapper itself, hence `@InitWrapper`. Here, we want Logged to be
// initialized without triggering its side effects (logging).

@MemberwiseInit(.public)
public struct CounterView: View {
  // @Logged: InitWrapper
  @InitWrapper(.public, default: "Blob", type: Logged<String>)
  @Logged
  var name1: String

  // @Binding: InitWrapper
  @InitWrapper(.public, type: Binding<Int>)
  @Binding
  var count1: Int

  // @Logged: InitRaw
  @InitRaw(.public, assignee: "self._name1", default: "Blob", type: Logged<String>)
  @Logged
  var name2: String

  // @Binding: InitRaw
  @InitRaw(.public, assignee: "self._count1", type: Binding<Int>)
  @Binding
  var count2: Int

  // @State
  @Init(.public)
  @State var isOn = false

  var body: some View { … }
}

[^1]: I think @Init(assignee:type:) provides complete (if awkward) support for initializing property wrappers. If I'm wrong, I'd love to hear about it.

swift-memberwise-init-macro version information

0.2.0