How should we reconstitute property wrappers?

Let's start with what I think is the simplest reconstitution: a parameter.

@propertyWrapper struct Wrapped<Value>: ReconstitutablePropertyWrapper {
  let wrappedValue: Value
}
func ƒ<Value>(wrapped: Wrapped<Value>) -> Value {
  @Wrapped(wrapped) var value
  return value
}

To enable that, what are we supposed to do? Have every property wrapper adopt this?

/// Makes a instance of a property wrapper usable with "wrapper syntax",
/// after having been passed as an instance.
public protocol ReconstitutablePropertyWrapper { }

public extension ReconstitutablePropertyWrapper {
  init(_ wrapper: Self) {
    self = wrapper
  }
}

I think we'd all rather have the option of using it as a property wrapper again without a need to transform it back into one, though?

I believe this proposal addresses the issue you’re facing swift-evolution/0293-extend-property-wrappers-to-function-and-closure-parameters.md at main · apple/swift-evolution · GitHub

It's possible I've missed something in there over the past couple of years since Swift 5.5, but as far as I know, it looks like it might be related to what I'm asking, but it's not.

I believe this works, but I'll admit that there may be subtleties I'm unaware of.

@propertyWrapper struct Wrapped<Value> {
  let wrappedValue: Value
}
func ƒ<Value>(wrapped: Wrapped<Value>) -> Value {
  @Wrapped var value: Value // declare, but do not initialize
  _value = wrapped
  return value
}

(this is by analogy with how you initialize property wrappers directly in init)

1 Like

The one downside here is that if Wrapper declares an init() you will end up calling that in the body.

I don't like it, but it is what I do for the next part of the question: is that what's intended for storing the property wrapper for use with a type instance?

public extension Published {
  /// The stored value of a `Published`.
  /// - Note: Only useful when not having access to the enclosing class instance.
  var value: Value { Storage(self).value }

  private final class Storage {
    @Published private(set) var value: Value

    init(_ published: Published) {
      _value = published
    }
  }
}

It is definitely the correct syntax for use with an instance property; it's only locals where I don't know if there's a better way. As far as I know @jumhyn's right that there's no way to prevent the no-argument initializer from being called in this case (either the local or the instance property), unless the property is declared with let and not var. :-/

Then the last part is, what do you do when you know you know you have a property wrapper, but not what exact kind? This Rewrapper solution isn't versatile, because of the various possibilities with wrappedValue & projectedValue.

@propertyWrapper struct WrapGod🎤🤨<Wrapper: wrappedValue & projectedValue> {
  @Rewrapper<Wrapper> var wrappedValue: Wrapper.WrappedValue

  init(wrapper: Wrapper) {
    _wrappedValue = .init(wrapper)
  }
}
/// Makes an instance usable as a property wrapper.
@propertyWrapper public struct Rewrapper<Wrapper: wrappedValue & projectedValue> {
  public var wrappedValue: Wrapper.WrappedValue { wrapper.wrappedValue }
  public var projectedValue: Wrapper.ProjectedValue { wrapper.projectedValue }
  private let wrapper: Wrapper
}

public extension Rewrapper {
  init(_ wrapper: Wrapper) {
    self.wrapper = wrapper
  }
}
public protocol wrappedValue<WrappedValue> {
  associatedtype WrappedValue
  var wrappedValue: WrappedValue { get }
}

public protocol projectedValue<ProjectedValue> {
  associatedtype ProjectedValue
  var projectedValue: ProjectedValue { get }
}

We've gotten to that point where I want to ask what you're actually trying to do. Under what circumstances do you have a property wrapper being passed as a value, and you want to turn it back into a property wrapper, and you know nothing about the property wrapper otherwise? Like, there are lots of problems with property wrappers (the big one being "you can't really have one as a protocol requirement"), but I'm not sure this is one of them in practice.

2 Likes

Without reconstitution, you can end up with too much .wrappedValue.wrappedValue.etc. chaining, where the intermediate wrappers only serve to provide access to a wrapped property, and don't require any other interaction. E.g. Here's a case where one level can be removed, but not both.

We can know a lot, just, sometimes, not which one is being used, precisely. E.g. here's a case where the generalization is over only two wrappers.

final class Object: ObservableObject { }
@ObservedObject.Collection var observed: [Object]
@StateObject.Collection var state = [Object]()
import SwiftUI

public extension ObservableObjectCollection {
  /// A `Collection` of `ObservableObject`s that invalidate a view
  /// when changes are made to their `Published` properties.
  @propertyWrapper struct DynamicProperty<
    DynamicProperty: SwiftUI.DynamicProperty
      & wrappedValue<ObservableObjectCollection>
      & projectedValue<ObservedObject<DynamicProperty.WrappedValue>.Wrapper>
  > {
    @MainActor public var wrappedValue: Objects {
      get { objects.wrappedValue }
      nonmutating set { objects.wrappedValue = newValue }
    }

    @MainActor public var projectedValue: ObservedObject<DynamicProperty.WrappedValue>.Wrapper {
      $objects
    }

    @Rewrapper<DynamicProperty> private var objects: DynamicProperty.WrappedValue
  }
}

// MARK: - private
private extension ObservableObjectCollection.DynamicProperty {
  private init(property: DynamicProperty) {
    _objects = .init(property)
  }
}

// MARK: - SwiftUI.DynamicProperty
extension ObservableObjectCollection.DynamicProperty: SwiftUI.DynamicProperty { }

// MARK: - ObservedObject
public extension ObservedObject {
  typealias Collection<Objects> = ObservableObjectCollection<Objects>.DynamicProperty<Self>
  where ObjectType == ObservableObjectCollection<Objects>
}

@available(
  swift, deprecated: 5.8,
  message: "`where` clause will not compile with generic syntax instead"
)
extension ObservableObjectCollection.DynamicProperty where DynamicProperty == ObservedObject<ObservableObjectCollection> {
  public init(wrappedValue: Objects) {
    self.init(
      property: .init(
        wrappedValue: .init(wrappedValue: wrappedValue)
      )
    )
  }
}

extension ObservedObject: wrappedValue & projectedValue { }

// MARK: - StateObject
public extension StateObject {
  typealias Collection<Objects> = ObservableObjectCollection<Objects>.DynamicProperty<Self>
  where ObjectType == ObservableObjectCollection<Objects>
}

@available(
  swift, deprecated: 5.8,
  message: "`where` clause will not compile with generic syntax instead"
)
public extension ObservableObjectCollection.DynamicProperty where DynamicProperty == StateObject<ObservableObjectCollection> {
  init(wrappedValue: Objects) {
    self.init(
      property: .init(
        wrappedValue: .init(wrappedValue: wrappedValue)
      )
    )
  }
}

extension StateObject: wrappedValue & projectedValue { }
The rest of the code to support that ⬆️
import Combine

/// `ObservableObject`s  which all forward their `objectWillChange` through a parent.
@propertyWrapper public final class ObservableObjectCollection<Objects: Collection>: ObservableObject
where
  Objects.Element: ObservableObject,
  Objects.Element.ObjectWillChangePublisher == ObservableObjectPublisher
{
  public init(wrappedValue: Objects) {
    self.wrappedValue = wrappedValue
    assignCancellable()
  }

  @Published public var wrappedValue: Objects {
    didSet { assignCancellable() }
  }

  private var cancellable: AnyCancellable!
}

// MARK: - public
public extension ObservableObjectCollection {
  func assignCancellable() {
    cancellable = wrappedValue.map(\.objectWillChange).merged.subscribe(objectWillChange)
  }
}
import Combine

/// The simplest way to forward one `objectWillChange` through another
/// is to make `ObservableObjectPublisher` be a `Subject`,
/// so that `Publisher.subscribe` can be used with it.
extension ObservableObjectPublisher: Subject {
  public func send(subscription: any Subscription) {
    subscription.request(.unlimited)
  }

  public func send(_: Void) {
    send()
  }

  public func send(completion _: Subscribers.Completion<Never>) { }
}
import Combine

public extension Sequence where Element: Publisher {
  var merged: Publishers.MergeMany<Element> { .init(self) }
}