Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a publisher from a value, since the beauty of @Observable?

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)
  }
}
like image 715
Roland Lariotte Avatar asked Sep 06 '25 03:09

Roland Lariotte


2 Answers

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)
  }
}
like image 161
Sergio Carneiro Avatar answered Sep 07 '25 22:09

Sergio Carneiro


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.

like image 25
ST K Avatar answered Sep 07 '25 23:09

ST K