Skip to content

Instantly share code, notes, and snippets.

@jegnux
Last active October 12, 2021 22:49
Show Gist options
  • Save jegnux/79554d1b1eb200afb1dfca075b6bd478 to your computer and use it in GitHub Desktop.
Save jegnux/79554d1b1eb200afb1dfca075b6bd478 to your computer and use it in GitHub Desktop.
Alternative to @published that works with subclasses
@propertyWrapper
public struct SuperPublished<Value> {
public init(wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
public var wrappedValue: Value
public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance observed: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
) -> Value {
get {
observed[keyPath: storageKeyPath].wrappedValue
}
set {
if let publisher = observed.objectWillChange as? Combine.ObservableObjectPublisher {
publisher.send()
}
observed[keyPath: storageKeyPath].wrappedValue = newValue
}
}
}
import PlaygroundSupport
import SwiftUI
import Combine
class Root: ObservableObject {
@Published var rootPublished: Int = 0
@SuperPublished var rootSuperPublished: Int = 0
}
class Child: Root {
@Published var childPublished: Int = 0
@SuperPublished var childSuperPublished: Int = 0
}
struct MyView: View {
@ObservedObject var child = Child()
var body: some View {
VStack(alignment: .leading) {
button("rootPublished", for: \.rootPublished)
button("rootSuperPublished", for: \.rootSuperPublished)
button("childPublished", for: \.childPublished)
button("childSuperPublished", for: \.childSuperPublished)
}
}
func button(
_ title: String,
for keyPath: ReferenceWritableKeyPath<Child, Int>
) -> some View {
HStack {
Text("\(title): \(child[keyPath: keyPath])")
Button(action: { self.child[keyPath: keyPath] += 1 }) {
Text("+= 1")
}
}
}
}
PlaygroundPage.current.liveView = NSHostingView(rootView: MyView())
@atb1983
Copy link

atb1983 commented Oct 15, 2020

This is very cool. I found it very useful. Pity that Apple doesn't provide this functionality by default.

I just got one question, have you tried to unit tests a @SuperPublished property? I'm not able to do it e.g.

$childPublished // this one works
.sink {
XCTAssertEqual($0, 0)
expectation.fulfill()
}.store(in: &cancelBag)

$rootSuperPublished // $ not found, only rootSuperPublished
.sink {
XCTAssertEqual($0, 0)
expectation.fulfill()
}.store(in: &cancelBag)

Thank you

@jegnux
Copy link
Author

jegnux commented Oct 15, 2020

@atb1983 oh you're right, this property wrapper misses a projectedValue. You can add it like this:

var projectedValue: SuperPublished<Value> {
    return self
}

@jegnux
Copy link
Author

jegnux commented Oct 15, 2020

Oh actually not... This will return a SuperPublished<Value> when using $, while Published<Value> returns a publisher.
But you still should be able to do it by maintaining a CurrentValueSubject<Value>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment