Skip to content

Commit e61bfde

Browse files
committed
Optional support
You can now do `@PublishedObject var something: Example?`
1 parent 02aa30e commit e61bfde

File tree

2 files changed

+134
-10
lines changed

2 files changed

+134
-10
lines changed

Sources/PublishedObject/PublishedObject.swift

+32-10
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,39 @@ import Foundation
77
/// Just like @Published this sends willSet events to the enclosing ObservableObject's ObjectWillChangePublisher
88
/// but unlike @Published it also sends the wrapped value's published changes on to the enclosing ObservableObject
99
@propertyWrapper @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
10-
public struct PublishedObject<Value: ObservableObject> where Value.ObjectWillChangePublisher == ObservableObjectPublisher {
10+
public struct PublishedObject<Value> {
1111

12-
public init(wrappedValue: Value) {
12+
public init(wrappedValue: Value) where Value: ObservableObject, Value.ObjectWillChangePublisher == ObservableObjectPublisher {
13+
self.wrappedValue = wrappedValue
14+
self.cancellable = nil
15+
_startListening = { futureSelf, wrappedValue in
16+
let publisher = futureSelf._projectedValue
17+
let parent = futureSelf.parent
18+
futureSelf.cancellable = wrappedValue.objectWillChange.sink { [parent] in
19+
parent.objectWillChange?()
20+
DispatchQueue.main.async {
21+
publisher.send(wrappedValue)
22+
}
23+
}
24+
publisher.send(wrappedValue)
25+
}
26+
startListening(to: wrappedValue)
27+
}
28+
29+
public init<V>(wrappedValue: V?) where V? == Value, V: ObservableObject, V.ObjectWillChangePublisher == ObservableObjectPublisher {
1330
self.wrappedValue = wrappedValue
1431
self.cancellable = nil
32+
_startListening = { futureSelf, wrappedValue in
33+
let publisher = futureSelf._projectedValue
34+
let parent = futureSelf.parent
35+
futureSelf.cancellable = wrappedValue?.objectWillChange.sink { [parent] in
36+
parent.objectWillChange?()
37+
DispatchQueue.main.async {
38+
publisher.send(wrappedValue)
39+
}
40+
}
41+
publisher.send(wrappedValue)
42+
}
1543
startListening(to: wrappedValue)
1644
}
1745

@@ -60,15 +88,9 @@ public struct PublishedObject<Value: ObservableObject> where Value.ObjectWillCha
6088
}
6189
}
6290

91+
private var _startListening: (inout Self, _ toValue: Value) -> Void
6392
private mutating func startListening(to wrappedValue: Value) {
64-
let publisher = _projectedValue
65-
cancellable = wrappedValue.objectWillChange.sink { [parent] in
66-
parent.objectWillChange?()
67-
DispatchQueue.main.async {
68-
publisher.send(wrappedValue)
69-
}
70-
}
71-
publisher.send(wrappedValue)
93+
_startListening(&self, wrappedValue)
7294
}
7395

7496
public typealias Publisher = AnyPublisher<Value, Never>

Tests/PublishedObjectTests/PublishedObjectTests.swift

+102
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import XCTest
22
import Combine
33
@testable import PublishedObject
44

5+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
56
class Outer: ObservableObject {
67
@PublishedObject var innerPublishedObject: Inner
78
@Published var innerPublished: Inner
@@ -12,6 +13,7 @@ class Outer: ObservableObject {
1213
}
1314
}
1415

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

@@ -20,6 +22,18 @@ class Inner: ObservableObject {
2022
}
2123
}
2224

25+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
26+
class OuterOptional: ObservableObject {
27+
@PublishedObject var innerPublishedObject: Inner?
28+
@Published var innerPublished: Inner?
29+
30+
init(_ value: Int?) {
31+
self.innerPublishedObject = value.map(Inner.init)
32+
self.innerPublished = value.map(Inner.init)
33+
}
34+
}
35+
36+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
2337
final class PublishedObjectTests: XCTestCase {
2438
var cancellables: Set<AnyCancellable> = []
2539

@@ -115,9 +129,97 @@ final class PublishedObjectTests: XCTestCase {
115129
outer.innerPublished.value = 3
116130
wait(for: [exp5], timeout: 0.1)
117131
}
132+
133+
func testOptionalWithValue() throws {
134+
let outer = OuterOptional(1)
135+
136+
// Setting property on Inner from the optional init (This will only send an update when using @PublishedObject)
137+
138+
let exp5 = XCTestExpectation(description: "outer.objectWillChange will be called")
139+
outer.objectWillChange.first().sink { exp5.fulfill() } .store(in: &cancellables)
140+
outer.innerPublishedObject?.value = 3
141+
wait(for: [exp5], timeout: 0.1)
142+
143+
let exp6 = XCTestExpectation(description: "outer.objectWillChange will NOT be called")
144+
exp6.isInverted = true
145+
outer.objectWillChange.first().sink { exp6.fulfill() } .store(in: &cancellables)
146+
outer.innerPublished?.value = 3
147+
wait(for: [exp6], timeout: 0.1)
148+
149+
// Setting property on Outer (This will send an update with either @Published or @PublishedObject)
150+
151+
let exp1 = XCTestExpectation(description: "outer.objectWillChange will be called")
152+
outer.objectWillChange.first().sink { exp1.fulfill() } .store(in: &cancellables)
153+
outer.innerPublishedObject = Inner(2)
154+
wait(for: [exp1], timeout: 0.1)
155+
156+
let exp2 = XCTestExpectation(description: "outer.objectWillChange will be called")
157+
outer.objectWillChange.first().sink { exp2.fulfill() } .store(in: &cancellables)
158+
outer.innerPublished = Inner(2)
159+
wait(for: [exp2], timeout: 0.1)
160+
161+
// Setting property on Inner (This will only send an update when using @PublishedObject)
162+
163+
let exp3 = XCTestExpectation(description: "outer.objectWillChange will be called")
164+
outer.objectWillChange.first().sink { exp3.fulfill() } .store(in: &cancellables)
165+
outer.innerPublishedObject?.value = 3
166+
wait(for: [exp3], timeout: 0.1)
167+
168+
let exp4 = XCTestExpectation(description: "outer.objectWillChange will NOT be called")
169+
exp4.isInverted = true
170+
outer.objectWillChange.first().sink { exp4.fulfill() } .store(in: &cancellables)
171+
outer.innerPublished?.value = 3
172+
wait(for: [exp4], timeout: 0.1)
173+
}
174+
175+
func testOptionalWithoutValue() throws {
176+
let outer = OuterOptional(nil)
177+
178+
// Setting property on Inner while it is nil
179+
// (this should never call objectWillChange because the Inner obj is not there so nothing is changed)
180+
181+
let exp5 = XCTestExpectation(description: "uhhhhm")
182+
exp5.isInverted = true
183+
outer.objectWillChange.first().sink { exp5.fulfill() } .store(in: &cancellables)
184+
outer.innerPublishedObject?.value = 3
185+
wait(for: [exp5], timeout: 0.1)
186+
187+
let exp6 = XCTestExpectation(description: "uhhhhm")
188+
exp6.isInverted = true
189+
outer.objectWillChange.first().sink { exp6.fulfill() } .store(in: &cancellables)
190+
outer.innerPublished?.value = 3
191+
wait(for: [exp6], timeout: 0.1)
192+
193+
// Setting property on Outer (This will send an update with either @Published or @PublishedObject)
194+
195+
let exp1 = XCTestExpectation(description: "outer.objectWillChange will be called")
196+
outer.objectWillChange.first().sink { exp1.fulfill() } .store(in: &cancellables)
197+
outer.innerPublishedObject = Inner(2)
198+
wait(for: [exp1], timeout: 0.1)
199+
200+
let exp2 = XCTestExpectation(description: "outer.objectWillChange will be called")
201+
outer.objectWillChange.first().sink { exp2.fulfill() } .store(in: &cancellables)
202+
outer.innerPublished = Inner(2)
203+
wait(for: [exp2], timeout: 0.1)
204+
205+
// Setting property on Inner (This will only send an update when using @PublishedObject)
206+
207+
let exp3 = XCTestExpectation(description: "outer.objectWillChange will be called")
208+
outer.objectWillChange.first().sink { exp3.fulfill() } .store(in: &cancellables)
209+
outer.innerPublishedObject?.value = 3
210+
wait(for: [exp3], timeout: 0.1)
211+
212+
let exp4 = XCTestExpectation(description: "outer.objectWillChange will NOT be called")
213+
exp4.isInverted = true
214+
outer.objectWillChange.first().sink { exp4.fulfill() } .store(in: &cancellables)
215+
outer.innerPublished?.value = 3
216+
wait(for: [exp4], timeout: 0.1)
217+
}
118218

119219
static var allTests = [
120220
("testObjectWillChange", testObjectWillChange),
121221
("testProjectedValue", testProjectedValue),
222+
("testOptionalWithValue", testOptionalWithValue),
223+
("testOptionalWithoutValue", testOptionalWithoutValue),
122224
]
123225
}

0 commit comments

Comments
 (0)