Skip to content

Instantly share code, notes, and snippets.

@lslv1243
Last active May 29, 2023 21:03
Show Gist options
  • Save lslv1243/8b9aceaf6c4b895534f5214e37f04f7a to your computer and use it in GitHub Desktop.
Save lslv1243/8b9aceaf6c4b895534f5214e37f04f7a to your computer and use it in GitHub Desktop.
`@Republished` property wrapper to forward changes from nested `ObservableObject`.
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