Skip to content

Commit

Permalink
Optional support
Browse files Browse the repository at this point in the history
You can now do `@PublishedObject var something: Example?`
  • Loading branch information
Amzd committed May 30, 2021
1 parent 02aa30e commit e61bfde
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 10 deletions.
42 changes: 32 additions & 10 deletions Sources/PublishedObject/PublishedObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,39 @@ import Foundation
/// Just like @Published this sends willSet events to the enclosing ObservableObject's ObjectWillChangePublisher
/// but unlike @Published it also sends the wrapped value's published changes on to the enclosing ObservableObject
@propertyWrapper @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct PublishedObject<Value: ObservableObject> where Value.ObjectWillChangePublisher == ObservableObjectPublisher {
public struct PublishedObject<Value> {

public init(wrappedValue: Value) {
public init(wrappedValue: Value) where Value: ObservableObject, Value.ObjectWillChangePublisher == ObservableObjectPublisher {
self.wrappedValue = wrappedValue
self.cancellable = nil
_startListening = { futureSelf, wrappedValue in
let publisher = futureSelf._projectedValue
let parent = futureSelf.parent
futureSelf.cancellable = wrappedValue.objectWillChange.sink { [parent] in
parent.objectWillChange?()
DispatchQueue.main.async {
publisher.send(wrappedValue)
}
}
publisher.send(wrappedValue)
}
startListening(to: wrappedValue)
}

public init<V>(wrappedValue: V?) where V? == Value, V: ObservableObject, V.ObjectWillChangePublisher == ObservableObjectPublisher {
self.wrappedValue = wrappedValue
self.cancellable = nil
_startListening = { futureSelf, wrappedValue in
let publisher = futureSelf._projectedValue
let parent = futureSelf.parent
futureSelf.cancellable = wrappedValue?.objectWillChange.sink { [parent] in
parent.objectWillChange?()
DispatchQueue.main.async {
publisher.send(wrappedValue)
}
}
publisher.send(wrappedValue)
}
startListening(to: wrappedValue)
}

Expand Down Expand Up @@ -60,15 +88,9 @@ public struct PublishedObject<Value: ObservableObject> where Value.ObjectWillCha
}
}

private var _startListening: (inout Self, _ toValue: Value) -> Void
private mutating func startListening(to wrappedValue: Value) {
let publisher = _projectedValue
cancellable = wrappedValue.objectWillChange.sink { [parent] in
parent.objectWillChange?()
DispatchQueue.main.async {
publisher.send(wrappedValue)
}
}
publisher.send(wrappedValue)
_startListening(&self, wrappedValue)
}

public typealias Publisher = AnyPublisher<Value, Never>
Expand Down
102 changes: 102 additions & 0 deletions Tests/PublishedObjectTests/PublishedObjectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import XCTest
import Combine
@testable import PublishedObject

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
class Outer: ObservableObject {
@PublishedObject var innerPublishedObject: Inner
@Published var innerPublished: Inner
Expand All @@ -12,6 +13,7 @@ class Outer: ObservableObject {
}
}

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
class Inner: ObservableObject {
@Published var value: Int

Expand All @@ -20,6 +22,18 @@ class Inner: ObservableObject {
}
}

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
class OuterOptional: ObservableObject {
@PublishedObject var innerPublishedObject: Inner?
@Published var innerPublished: Inner?

init(_ value: Int?) {
self.innerPublishedObject = value.map(Inner.init)
self.innerPublished = value.map(Inner.init)
}
}

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final class PublishedObjectTests: XCTestCase {
var cancellables: Set<AnyCancellable> = []

Expand Down Expand Up @@ -115,9 +129,97 @@ final class PublishedObjectTests: XCTestCase {
outer.innerPublished.value = 3
wait(for: [exp5], timeout: 0.1)
}

func testOptionalWithValue() throws {
let outer = OuterOptional(1)

// Setting property on Inner from the optional init (This will only send an update when using @PublishedObject)

let exp5 = XCTestExpectation(description: "outer.objectWillChange will be called")
outer.objectWillChange.first().sink { exp5.fulfill() } .store(in: &cancellables)
outer.innerPublishedObject?.value = 3
wait(for: [exp5], timeout: 0.1)

let exp6 = XCTestExpectation(description: "outer.objectWillChange will NOT be called")
exp6.isInverted = true
outer.objectWillChange.first().sink { exp6.fulfill() } .store(in: &cancellables)
outer.innerPublished?.value = 3
wait(for: [exp6], timeout: 0.1)

// Setting property on Outer (This will send an update with either @Published or @PublishedObject)

let exp1 = XCTestExpectation(description: "outer.objectWillChange will be called")
outer.objectWillChange.first().sink { exp1.fulfill() } .store(in: &cancellables)
outer.innerPublishedObject = Inner(2)
wait(for: [exp1], timeout: 0.1)

let exp2 = XCTestExpectation(description: "outer.objectWillChange will be called")
outer.objectWillChange.first().sink { exp2.fulfill() } .store(in: &cancellables)
outer.innerPublished = Inner(2)
wait(for: [exp2], timeout: 0.1)

// Setting property on Inner (This will only send an update when using @PublishedObject)

let exp3 = XCTestExpectation(description: "outer.objectWillChange will be called")
outer.objectWillChange.first().sink { exp3.fulfill() } .store(in: &cancellables)
outer.innerPublishedObject?.value = 3
wait(for: [exp3], timeout: 0.1)

let exp4 = XCTestExpectation(description: "outer.objectWillChange will NOT be called")
exp4.isInverted = true
outer.objectWillChange.first().sink { exp4.fulfill() } .store(in: &cancellables)
outer.innerPublished?.value = 3
wait(for: [exp4], timeout: 0.1)
}

func testOptionalWithoutValue() throws {
let outer = OuterOptional(nil)

// Setting property on Inner while it is nil
// (this should never call objectWillChange because the Inner obj is not there so nothing is changed)

let exp5 = XCTestExpectation(description: "uhhhhm")
exp5.isInverted = true
outer.objectWillChange.first().sink { exp5.fulfill() } .store(in: &cancellables)
outer.innerPublishedObject?.value = 3
wait(for: [exp5], timeout: 0.1)

let exp6 = XCTestExpectation(description: "uhhhhm")
exp6.isInverted = true
outer.objectWillChange.first().sink { exp6.fulfill() } .store(in: &cancellables)
outer.innerPublished?.value = 3
wait(for: [exp6], timeout: 0.1)

// Setting property on Outer (This will send an update with either @Published or @PublishedObject)

let exp1 = XCTestExpectation(description: "outer.objectWillChange will be called")
outer.objectWillChange.first().sink { exp1.fulfill() } .store(in: &cancellables)
outer.innerPublishedObject = Inner(2)
wait(for: [exp1], timeout: 0.1)

let exp2 = XCTestExpectation(description: "outer.objectWillChange will be called")
outer.objectWillChange.first().sink { exp2.fulfill() } .store(in: &cancellables)
outer.innerPublished = Inner(2)
wait(for: [exp2], timeout: 0.1)

// Setting property on Inner (This will only send an update when using @PublishedObject)

let exp3 = XCTestExpectation(description: "outer.objectWillChange will be called")
outer.objectWillChange.first().sink { exp3.fulfill() } .store(in: &cancellables)
outer.innerPublishedObject?.value = 3
wait(for: [exp3], timeout: 0.1)

let exp4 = XCTestExpectation(description: "outer.objectWillChange will NOT be called")
exp4.isInverted = true
outer.objectWillChange.first().sink { exp4.fulfill() } .store(in: &cancellables)
outer.innerPublished?.value = 3
wait(for: [exp4], timeout: 0.1)
}

static var allTests = [
("testObjectWillChange", testObjectWillChange),
("testProjectedValue", testProjectedValue),
("testOptionalWithValue", testOptionalWithValue),
("testOptionalWithoutValue", testOptionalWithoutValue),
]
}

0 comments on commit e61bfde

Please sign in to comment.