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?
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)
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.
/// 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.
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) }
}