Last active
May 29, 2023 21:03
-
-
Save lslv1243/8b9aceaf6c4b895534f5214e37f04f7a to your computer and use it in GitHub Desktop.
`@Republished` property wrapper to forward changes from nested `ObservableObject`.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
import SwiftUI | |
struct ContentView: View { | |
@StateObject var observable = OuterObservable() | |
var body: some View { | |
VStack { | |
Display(title: "inner", value: observable.inner.binding(\.value)) | |
Display(title: "outer", value: $observable.value) | |
} | |
.padding() | |
} | |
} | |
struct Display: View { | |
let title: String | |
@Binding var value: Int | |
var body: some View { | |
VStack { | |
Text(title) | |
.font(.headline) | |
Text("value: \(value)") | |
Button("increment") { value += 1 } | |
.buttonStyle(.bordered) | |
} | |
} | |
} | |
class OuterObservable: ObservableObject { | |
@Republished var inner = InnerObservable() | |
@Published var value = 0 | |
} | |
class InnerObservable: ObservableObject { | |
@Published var value = 0 | |
} | |
extension ObservableObject { | |
/// Creates a `Binding` to a property of an `ObservableObject` via a key path. | |
/// | |
/// This method is a workaround for the `@Republished` property wrapper, allowing a `Binding` to be | |
/// created for properties that would otherwise not trigger `objectWillChange` when accessed through | |
/// `projectedValue`. | |
func binding<Value>(_ keyPath: ReferenceWritableKeyPath<Self, Value>) -> Binding<Value> { | |
return Binding( | |
get: { self[keyPath: keyPath] }, | |
set: { self[keyPath: keyPath] = $0 } | |
) | |
} | |
} | |
/// A property wrapper that forwards the `objectWillChange` publisher of an `ObservableObject`. | |
/// | |
/// `Republished` enables SwiftUI views to respond to changes in an `ObservableObject` by | |
/// forwarding its `objectWillChange` publisher to the outer `ObservableObject`. This is beneficial | |
/// when the inner `ObservableObject` properties change, and these changes should trigger updates | |
/// in the SwiftUI view. | |
/// | |
/// Access to the wrapped value directly is intentionally disabled to ensure that the `objectWillChange` | |
/// publisher is correctly forwarded when the property is accessed. | |
@propertyWrapper | |
struct Republished<Value: ObservableObject> { | |
private class Storage { | |
let value: Value | |
var cancellable: AnyCancellable? | |
init(_ value: Value) { | |
self.value = value | |
} | |
} | |
private let storage: Storage | |
@available(*, unavailable) | |
var wrappedValue: Value { fatalError() } | |
init(wrappedValue: Value) { | |
storage = Storage(wrappedValue) | |
} | |
public static subscript<Parent: ObservableObject>( | |
_enclosingInstance instance: Parent, | |
wrapped wrappedKeyPath: KeyPath<Parent, Value>, | |
storage storageKeyPath: KeyPath<Parent, Republished<Value>> | |
) -> Value where Parent.ObjectWillChangePublisher == ObservableObjectPublisher { | |
let storage = instance[keyPath: storageKeyPath].storage | |
if storage.cancellable == nil { | |
storage.cancellable = storage.value.objectWillChange.sink { _ in | |
instance.objectWillChange.send() | |
} | |
} | |
return storage.value | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment