Is there a better way of getting the published value synchronously?
class C {
@Published var value: Int = 123
func foo() {
let v = getValue($value)
print(v)
}
func getValue(_ v: Published<Int>.Publisher) -> Int? {
var val: Int? = nil
_ = v.sink {
val = $0
}
return val // 123
}
}
let c = C()
c.foo()
The lines:
_ = v.sink {
val = $0
}
don't feel right to me: is there a guarantee that in case of ready publishers (like Int = 123) the "sink" call closure is called synchronously? (by that I mean "during duration of the sink call itself). Or is there a more direct way of doing it?
import SwiftUI
import Combine
class BaseModel {
@Published var someVal: Int = 123
}
class SubModel: ModelWithSubscribers, ObservableObject {
@Published var someVal: Int = 0
private var baseModel = BaseModel()
var cancellables = Set<AnyCancellable>()
init() {
baseModel.$someVal
.receive(on: DispatchQueue.main)
.sink {
if self.someVal != $0 {
self.someVal = $0
}
}
.store(in: &cancellables)
}
}
where base model change is replicated in the sub model. This is ok-ish (aside from sort of truth duplication) but I'd like to get rid of the boilerplate in the initialiser. With this helper:
protocol ModelWithSubscribers: AnyObject {
var cancellables: Set<AnyCancellable> { get set }
func bind<T: Publisher>(_ publisher: T, to binding: Binding<T.Output>) where T.Output: Equatable
}
extension ModelWithSubscribers {
func bind<T: Publisher>(_ publisher: T, to binding: Binding<T.Output>) where T.Output: Equatable {
publisher
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { error in
fatalError("\(error)")
}, receiveValue: { output in
if binding.wrappedValue != output {
binding.wrappedValue = output
}
})
.store(in: &cancellables)
}
}
I managed to reduce that boilerplate down to this line:
protocol ModelWithSubscribers: AnyObject {
var cancellables: Set<AnyCancellable> { get set }
func bind<Root, T: Publisher>(_ publisher: T, to keyPath: ReferenceWritableKeyPath<Root, T.Output>) where T.Failure == Never, T.Output: Equatable, Self == Root
// 🟨 Same-type requirement makes generic parameters 'Root' and 'Self' equivalent; this is an error in Swift 6
}
extension ModelWithSubscribers {
func bind<Root, T: Publisher>(_ publisher: T, to keyPath: ReferenceWritableKeyPath<Root, T.Output>) where T.Failure == Never, T.Output: Equatable, Self == Root {
publisher
.receive(on: DispatchQueue.main)
.sink(receiveValue: { output in
if self[keyPath: keyPath] != output {
self[keyPath: keyPath] = output
}
})
.store(in: &cancellables)
}
}
Usage:
bind(baseModel.$someVal, to: \.someVal)
Can fix the warning by charging Root to Self:
protocol ModelWithSubscribers: AnyObject {
var cancellables: Set<AnyCancellable> { get set }
func bind<T: Publisher>(_ publisher: T, to keyPath: ReferenceWritableKeyPath<Self, T.Output>) where T.Failure == Never, T.Output: Equatable
}
but then I have to make "SubModel" class final as getting this error:
class SubModel: ModelWithSubscribers, ObservableObject { // 🛑 Protocol 'ModelWithSubscribers' requirement 'bind(_:to:)' cannot be satisfied by a non-final class ('SubModel') because it uses 'Self' in a non-parameter, non-result type position
Not sure if making this class final is ok or bad for me - will cross that bridge when get to it.
Sorry that was red herring: initially I tried to pass a variable like so: "$someValue" - but that didn't give me the wanted binding. I supposed that I could have used a published type instead hence the original question, but looks like that was the wrong approach to the problem; found some working key-path based solution above.
That example with the key paths does have a reference cycle in the sink btw; it captures self which is then stored as a handle in the cancelables. So you might want to find some nice way to break that if that is meaningful to your object graph.