I'm trying to make a property wrapper similar to Combine
's Published
one(for my project needs), but with ability to modify the wrapped property by sending a value to a publisher, stored in projectedValue
, like this:
// in class
@PublishedMutable var foo = "foo"
$foo.send("bar")
// ...
Here's the code of the property wrapper:
@propertyWrapper
struct PublishedMutable<Value> {
static subscript<T: ObservableObject>(
_enclosingInstance instance: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
) -> Value {
get {
instance[keyPath: storageKeyPath].storage
}
set {
let publisher = instance.objectWillChange
// This assumption is definitely not safe to make in
// production code, but it's fine for this demo purpose:
(publisher as? ObservableObjectPublisher)?.send()
instance[keyPath: storageKeyPath].storage = newValue
}
}
@available(*, unavailable,
message: "@PublishedMutable can only be applied to classes"
)
var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
private var storage: Value{
get { publisher.value }
set { publisher.send(newValue) }
}
typealias Publisher = CurrentValueSubject<Value, Never>
var projectedValue: Publisher { self.publisher }
private var publisher: Publisher
init(initialValue: Value) {
self.publisher = Publisher(initialValue)
}
init(wrappedValue: Value) {
self.init(initialValue: wrappedValue)
}
}
And here's the playground code that I used to test its features:
class AmogusComic: ObservableObject {
@PublishedMutable var currentPageContent = "S O S"
private var cancellables: Set<AnyCancellable> = []
func connect() {
$currentPageContent.sink { newPage in
print(newPage)
}
.store(in: &cancellables)
objectWillChange.sink { _ in
print("Update")
}
.store(in: &cancellables)
}
}
let amogusComic = AmogusComic()
DispatchQueue.main.async {
// connect publishers and print initial content. prints "S O S"
amogusComic.connect()
// change the value of current page via setting a property, the amogusComic class will reactively print "S U S" after
amogusComic.currentPageContent = "S U S"
// take a property publisher and send new value to publisher, the amogusComic class will reactively print "A M O G U S" after
amogusComic.$currentPageContent.send("A M O G U S")
}
PlaygroundPage.current.needsIndefiniteExecution = true
Just modifying the value both by modifying a property directly and sending a value to publisher work perfectly.
But I also want to call objectWillChange.send()
anytime the value changes
The problem is that it only gets called when modifying the wrapped property directly, which is not correct behavior.
The test code outputs this
S O S
Update
S U S
A M O G U S
when I think it should output this
S O S
Update
S U S
Update
A M O G U S
As you can see in code above, I used _enclosingInstance
static subscript to get the ObservableObject
and call objectWillChange
, but it turns out that Swift calls that subscript only when modifying a property directly (amogusComic.currentPageContent = "S U S"
)
Getting Mirror(reflecting: self).superclassMirror
also doesn't work, superclassMirror
always returns nil
Can I somehow get the ObservableObject
outside of _enclosingInstance
static subscript? So I could call objectWillChange
also when sending a value to the publisher
No. We don't have access to a static subscript equivalent of projectedValue
. It's Apple's secret, how a Published
subscribes to its parent object's objectWillChange
, without the need for its getter or setter to be called first.
You can only recreate (and you have) Published
's projectedValue
if the class you use it with is not an ObservableObject
.
That said, there's no need to, if your intention is just to have a send
method.
final class Object: ObservableObject {
@Published var published = ""
}
let object = Object()
object.objectWillChange.sink { print("objectWillChange") }
object.$published.send("new value")
// "objectWillChange"
object.published // "new value"
public extension Published.Publisher {
mutating func send(_ value: Value) {
Just(value).assign(to: &self)
}
}