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?
jjatie
2
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.
jrose
(Jordan Rose)
4
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
Jumhyn
(Frederick Kellison-Linn)
5
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
}
}
}
jrose
(Jordan Rose)
7
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 }
}
jrose
(Jordan Rose)
9
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) }
}