Skip to content

Commit

Permalink
Automatic trigger for objectWillChange publisher (#6)
Browse files Browse the repository at this point in the history
* Automatic trigger for objectWillChange publisher (implements #5)

* yield the value through the `didSet` implementation of the `wrappedValue` to respect the `behavior` property

* Dynamically check the objects type to allow the wrapper in non-`ObservableObject` types
  • Loading branch information
jlsiewert authored Oct 2, 2022
1 parent 7de68e4 commit dc258d0
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 9 deletions.
53 changes: 50 additions & 3 deletions Sources/AsyncValue/AsyncValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
//

import Foundation
#if canImport(Combine)
import Combine
#endif

/// A swift concurrency equivalent to `@Published`
///
Expand Down Expand Up @@ -63,11 +66,11 @@ import Foundation
/// // Task value: new value
/// ```
///
/// Usage with SwiftUI's `ObseravbleObject`:
/// Usage with Combine's `ObseravbleObject`:
///
/// ```swift
/// @AsyncValue var myValue = "Test" {
/// willSet { objectWillChange.send() }
/// class MyObservableOjbect: ObservableObject {
/// @AsyncValue var myValue = "Test"
/// }
/// ```
@propertyWrapper
Expand Down Expand Up @@ -113,4 +116,48 @@ public struct AsyncValue<Value: Equatable> {
self.behavior = behavior
storage = ContinuationStorage()
}

// Implementing the subscript in an extension does not seem to be picked up by the compiler
// so implement it here instead.
#if canImport(Combine)
/// Creates a new `AsyncValue` within the specified instance
/// - Parameter instance: The class in which the property is created
/// - Parameter wrappedKeyPath: `KeyPath` to the wrapped value property
/// - Parameter storageKeyPath: `KeyPath` to the enclosing instance
///
/// The default implementation of the `ObservableObject` protocol falls back to
/// the `ObservableObjectPublisher`.
/// Here, we use the static subscript implementation described in the
/// [Swift Evolution Proposal](https://github.com/apple/swift-evolution/blob/main/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type)
/// as mentioned by [John Sundell](https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/)
/// to indicate changes made to an `@AsyncValue`.
///
/// The compiler will use the static subscript whenever the enclosing type of the property wrapper
/// is a class.
///
/// Due to a limitation in the compiler ([apple/swift#54777](https://github.com/apple/swift/issues/54777))
/// only *one* subscript may be specified.
/// Therefore, the set method dynamically check if the type of the instance is an `ObservableObject`
/// that implements the default `ObservableObjectPublisher`
public static subscript<T: AnyObject>(
_enclosingInstance instance: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
) -> Value {
get {
// Return the saved wrapped value
instance[keyPath: storageKeyPath].wrappedValue
}
set {
// Dynamically call objectWillChange if available
if let instance = instance as? any ObservableObject,
let publisher = (instance.objectWillChange as any Publisher) as? ObservableObjectPublisher {
// Trigger the `ObjectWillChangePublisher`
publisher.send()
}
instance[keyPath: storageKeyPath].wrappedValue = newValue
}
}

#endif
}
9 changes: 3 additions & 6 deletions Tests/AsyncValueTests/AsyncValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@ import SwiftUI

extension AsyncValueTests {
fileprivate class TestObservableObject: ObservableObject {
var cancellable: AnyCancellable?

@AsyncValue var myValue = "Test" {
willSet { objectWillChange.send() }
}
@AsyncValue var myValue = "Test"
}

func test_observableObjectPublisher() {
Expand All @@ -81,7 +77,7 @@ extension AsyncValueTests {

var updateCount: Int = 0

sut.cancellable = sut.objectWillChange.sink {
let cancellable = sut.objectWillChange.sink {
updateCount += 1
}

Expand All @@ -96,6 +92,7 @@ extension AsyncValueTests {
// Since we subscribed after the observable object is created, the initial state of "Test".
// Cannot trigger a new value in our sink block above.
XCTAssertEqual(updateCount, 2)
cancellable.cancel()
}
}
#endif

0 comments on commit dc258d0

Please sign in to comment.