Skip to content

Commit 72f0f1c

Browse files
committed
CurrentValuePublisher: Refactored @published support
Refs #2
1 parent 3a59299 commit 72f0f1c

2 files changed

Lines changed: 108 additions & 36 deletions

File tree

Sources/CombineExtensions/CurrentValuePublisher/CurrentValuePublisher+Published.swift

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@ import Combine
22

33
extension CurrentValuePublisher {
44

5-
/// Creates a `CurrentValuePublisher` from a `@Published` property’s publisher.
5+
/// Initializes a `CurrentValuePublisher` from a `@Published` property’s publisher.
66
///
7-
/// The resulting publisher emits the initial value of the `@Published` property, followed
8-
/// by all subsequent values.
7+
/// Captures the current value of the `@Published` property and emits all future updates.
8+
/// Useful when bridging a `@Published` property to APIs expecting a `CurrentValuePublisher`.
99
///
10-
/// - Parameter publisher: A publisher associated with a `@Published` property.
10+
/// - Parameter publisher: The publisher exposed by a `@Published` property via its projected
11+
/// value, typically accessed via the `$` prefix.
1112
public convenience init(
1213
_ publisher: Published<Output>.Publisher
1314
) where Failure == Never {
1415
var initialValue: Output!
1516

16-
// `Published.Publisher`, similarly to `CurrentValueSubject` and ultimately also
17-
// `CurrentValuePublisher`, sends its current value to a subscriber upon subscription.
18-
// We leverage this behavior for obtaining the current value with a short-lived
19-
// subscription and skip it in the upstream publisher.
17+
// Ideally, we would access the current value of a `@Published` property directly, but
18+
// there is no API to achieve that. Instead, we rely on the fact that `Published.Publisher`
19+
// emits its current value upon subscription—similar to `CurrentValueSubject`
20+
// and `CurrentValuePublisher`. We use a short-lived subscription to capture that value,
21+
// then drop it from the upstream.
2022
_ = publisher
2123
.first()
2224
.sink { initialValue = $0 }
@@ -31,28 +33,37 @@ extension CurrentValuePublisher {
3133

3234
extension Published {
3335

34-
/// Creates a `@Published` property wrapper that reflects the values of a `CurrentValuePublisher`.
36+
/// Initializes a `@Published` property wrapper backed by a `CurrentValuePublisher`.
3537
///
36-
/// This is typically used to bind values from a `CurrentValuePublisher` to a `@Published`
37-
/// property in the initializer of an observable object. The property wrapper has to be
38-
/// assigned via `self._property = Published(publisher)`.
38+
/// Useful for binding a `CurrentValuePublisher` to a `@Published` property inside
39+
/// an observable object’s initializer. The wrapper must be assigned directly to the backing
40+
/// storage, e.g. `self._property = Published(publisher)`.
3941
///
40-
/// - Parameter publisher: A `CurrentValuePublisher` whose values the property wrapper
41-
/// will reflect.
42-
public init(_ publisher: CurrentValuePublisher<Publisher.Output, Publisher.Failure>) {
42+
/// While it is technically possible to mutate such a `@Published` property, doing so is
43+
/// discouraged—any assigned value will be overwritten by the next emission from the upstream
44+
/// publisher. To prevent accidental writes, the property should typically be declared with
45+
/// `private(set)` or limited through access control.
46+
///
47+
/// - Parameter publisher: The `CurrentValuePublisher` whose values will drive the `@Published`
48+
/// property.
49+
public init(_ publisher: CurrentValuePublisher<Value, Never>) {
4350
self.init(initialValue: publisher.value)
44-
publisher.assign(to: &projectedValue)
51+
52+
publisher
53+
.dropFirst()
54+
.assign(to: &self.projectedValue)
4555
}
4656

47-
/// Creates a `@Published` property wrapper that reflects the values of a `CurrentValueSubject`.
48-
///
49-
/// This is typically used to bind values from a `CurrentValueSubject` to a `@Published`
50-
/// property in the initializer of an observable object. The property wrapper has to be
51-
/// assigned via `self._property = Published(subject)`.
52-
///
53-
/// - Parameter publisher: A `CurrentValueSubject` whose values the property wrapper
54-
/// will reflect.
55-
public init(_ subject: CurrentValueSubject<Publisher.Output, Publisher.Failure>) {
57+
}
58+
59+
extension Published {
60+
61+
@available(*, deprecated, message: """
62+
No longer supported. Explicitly convert the subject to a CurrentValuePublisher instead. \
63+
This initializer creates a one-way binding only—updates to the property do not propagate \
64+
back to the subject, which can lead to confusion.
65+
""")
66+
public init(_ subject: CurrentValueSubject<Value, Never>) {
5667
self.init(CurrentValuePublisher(subject))
5768
}
5869

Tests/CombineExtensionsTests/CurrentValuePublisher/CurrentValuePublisherTests.swift

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,11 @@ class CurrentValuePublisherTests: XCTestCase {
270270
}
271271

272272
// ============================================================================ //
273-
// MARK: - Published
273+
// MARK: - @Published → CurrentValuePublisher
274274
// ============================================================================ //
275275

276276
func testPublishedToCurrentValuePublisher_accessingValue() {
277-
let object = MutableObservableObject(initialValue: "initial")
277+
let object = OwnedObservableObject(initialValue: "initial")
278278
let publisher = CurrentValuePublisher(object.$value)
279279

280280
XCTAssertEqual(publisher.value, "initial")
@@ -290,7 +290,7 @@ class CurrentValuePublisherTests: XCTestCase {
290290
var cancellables = Set<AnyCancellable>()
291291
var values = [String]()
292292

293-
let object = MutableObservableObject(initialValue: "initial")
293+
let object = OwnedObservableObject(initialValue: "initial")
294294

295295
CurrentValuePublisher(object.$value)
296296
.sink { values.append($0) }
@@ -309,7 +309,7 @@ class CurrentValuePublisherTests: XCTestCase {
309309
var cancellables = Set<AnyCancellable>()
310310
var values = [String]()
311311

312-
let object = MutableObservableObject(initialValue: "initial")
312+
let object = OwnedObservableObject(initialValue: "initial")
313313

314314
CurrentValuePublisher(object.$value)
315315
.sink { values.append($0) }
@@ -321,16 +321,19 @@ class CurrentValuePublisherTests: XCTestCase {
321321
XCTAssertEqual(values, ["initial", "second"])
322322

323323
cancellables.forEach { $0.cancel() }
324-
XCTAssertEqual(values, ["initial", "second"])
325324

326325
object.value = "third"
327326
XCTAssertEqual(values, ["initial", "second"])
328327
}
329328

329+
// ============================================================================ //
330+
// MARK: - CurrentValuePublisher → @Published
331+
// ============================================================================ //
332+
330333
func testCurrentValuePublisherToPublished_accessingValue() {
331334
let subject = CurrentValueSubject<String, Never>("initial")
332335
let publisher = CurrentValuePublisher<String, Never>(subject)
333-
let object = ImmutableObservableObject(publisher: publisher)
336+
let object = BackedObservableObject(publisher: publisher)
334337

335338
XCTAssertEqual(object.value, "initial")
336339

@@ -341,20 +344,78 @@ class CurrentValuePublisherTests: XCTestCase {
341344
XCTAssertEqual(object.value, "third")
342345
}
343346

347+
func testCurrentValuePublisherToPublished_receivingValues() {
348+
var cancellables = Set<AnyCancellable>()
349+
var values = [String]()
350+
351+
let subject = CurrentValueSubject<String, Never>("initial")
352+
let publisher = CurrentValuePublisher<String, Never>(subject)
353+
let object = BackedObservableObject(publisher: publisher)
354+
355+
object.$value
356+
.sink { values.append($0) }
357+
.store(in: &cancellables)
358+
359+
XCTAssertEqual(values, ["initial"])
360+
361+
subject.value = "second"
362+
XCTAssertEqual(values, ["initial", "second"])
363+
364+
subject.value = "third"
365+
XCTAssertEqual(values, ["initial", "second", "third"])
366+
}
367+
368+
func testCurrentValuePublisherToPublished_localOverride() {
369+
let subject = CurrentValueSubject<String, Never>("initial")
370+
let publisher = CurrentValuePublisher<String, Never>(subject)
371+
let object = BackedObservableObject(publisher: publisher)
372+
373+
XCTAssertEqual(object.value, "initial")
374+
375+
object.value = "override"
376+
XCTAssertEqual(object.value, "override")
377+
378+
subject.value = "second"
379+
XCTAssertEqual(object.value, "second")
380+
}
381+
344382
func testCurrentValuePublisherToPublished_completionFinished() {
345383
let subject = CurrentValueSubject<String, Never>("initial")
346384
let publisher = CurrentValuePublisher<String, Never>(subject)
347-
let object = ImmutableObservableObject(publisher: publisher)
385+
let object = BackedObservableObject(publisher: publisher)
348386

349387
XCTAssertEqual(object.value, "initial")
350388

351-
subject.send("second")
389+
subject.value = "second"
352390
XCTAssertEqual(object.value, "second")
353391

354392
subject.send(completion: .finished)
355393
XCTAssertEqual(object.value, "second")
356394
}
357395

396+
func testCurrentValuePublisherToPublished_cancellation() {
397+
var cancellables = Set<AnyCancellable>()
398+
var values = [String]()
399+
400+
let subject = CurrentValueSubject<String, Never>("initial")
401+
let publisher = CurrentValuePublisher<String, Never>(subject)
402+
403+
let object = BackedObservableObject(publisher: publisher)
404+
405+
object.$value
406+
.sink { values.append($0) }
407+
.store(in: &cancellables)
408+
409+
XCTAssertEqual(values, ["initial"])
410+
411+
subject.value = "second"
412+
XCTAssertEqual(values, ["initial", "second"])
413+
414+
cancellables.forEach { $0.cancel() }
415+
subject.value = "third"
416+
XCTAssertEqual(values, ["initial", "second"])
417+
}
418+
358419
// ============================================================================ //
359420
// MARK: - Map Operator
360421
// ============================================================================ //
@@ -944,7 +1005,7 @@ class CurrentValuePublisherTests: XCTestCase {
9441005

9451006
fileprivate struct TestError: Error, Equatable {}
9461007

947-
fileprivate class MutableObservableObject: ObservableObject {
1008+
fileprivate class OwnedObservableObject: ObservableObject {
9481009

9491010
fileprivate init(initialValue: String) {
9501011
self.value = initialValue
@@ -955,13 +1016,13 @@ fileprivate class MutableObservableObject: ObservableObject {
9551016

9561017
}
9571018

958-
fileprivate class ImmutableObservableObject {
1019+
fileprivate class BackedObservableObject {
9591020

9601021
fileprivate init(publisher: CurrentValuePublisher<String, Never>) {
9611022
self._value = Published(publisher)
9621023
}
9631024

9641025
@Published
965-
fileprivate private(set) var value: String
1026+
fileprivate var value: String
9661027

9671028
}

0 commit comments

Comments
 (0)