Reputation: 3502
Following the Foo2
class example, I used to have my object as an ObservableObject
, I could set a @Published
value and listen to their changes the way I would do it with Combine.
Now that I am using the @Observable
macro, shown on the Foo1
example, I can not create a Combine like pipeline.
Is there a way to listen to the @Observable
macro values the way I used when using the @Published
ones from an ObservableObject
object?
@Observable class Foo1 {
var isEnabled = false
init() {
isEnabled
.map { $0 } // Error: Value of type 'Bool' has no member 'map'
}
}
class Foo2: ObservableObject {
@Published var isEnabled = false
var cancellables = Set<AnyCancellable>()
init() {
$isEnabled
.map { $0 } // Works like a charm
.sink { print($0.description) }
.store(in: &cancellables)
}
}
Upvotes: 27
Views: 2874
Reputation: 3342
As an additional alternative to @Sérgio Carneiro’s approach, you can use AsyncStream to achieve similar behavior to CurrentValueSubject.
@Observable
class Foo1 {
var isEnabled = false {
didSet { continuation.yield(isEnabled) }
}
private let stream: AsyncStream<Bool>
private let continuation: AsyncStream<Bool>.Continuation
init() {
(stream, continuation) = AsyncStream<Bool>.makeStream()
continuation.yield(isEnabled) // Emit the initial value
Task {
for await value in stream.map({ $0 }) {
print(value)
}
}
}
}
Unlike CurrentValueSubject, AsyncStream does not retain the last value, so to ensure subscribers receive the current state upon subscription, we explicitly emit the initial value.
Additionally, if you need Combine-like operators (map
, filter
, removeDuplicates
, etc.), you can complement this approach with Apple's AsyncAlgorithms package.
Upvotes: 0
Reputation: 159
You can use property wrappers in @Observable
by ignoring observation.
@Observable
class Fool {
var cancellables = Set<AnyCancellable>()
@ObservationIgnored
@Published var isEnabled: Bool = false
init() {
$isEnabled
.map { $0 }
.sink { print( $0 ) }
.store(in: &cancellables)
}
}
However, in this case, SwiftUI views will not track isEnabled
anymore, so the UI will not be updated. This is okay if you don't need UI updates, but in most cases, you still want SwiftUI to track value changes to update the views.
Here's an alternative approach:
@Observable
class Fool {
var cancellables = Set<AnyCancellable>()
var isEnabled: Bool = false { didSet { isEnabled$ = isEnabled } }
@ObservationIgnored
@Published var isEnabled$: Bool = false
init() {
$isEnabled$
.removeDuplicates() // Prevent infinite loop
.sink { self.isEnabled = $0; print($0) }
.store(in: &cancellables)
}
}
Expanding on @Sérgio Carneiro's answer, this approach ensures changes are reflected on both sides.
Upvotes: 4
Reputation: 3966
This seems to be a limitation, and Apple doesn't seem to mention this pattern anywhere, most likely because in most of the cases you should react to these values in views using onChange()
.
To work around this limitation, you can create a similar publisher as the one created by @Published
using CurrentValueSubject:
import Combine
@Observable class Foo1 {
var isEnabled = false {
didSet { isEnabled$.send(isEnabled) }
}
var isEnabled$ = CurrentValueSubject<Bool, Never>(false)
var cancellables = Set<AnyCancellable>()
init() {
isEnabled$
.map { $0 }
.sink { print($0.description) }
.store(in: &cancellables)
}
}
Upvotes: 13