KVO publisher does not send signal on property change - swift

I've just started learning Combine and am quite confused with behaviour of KVO publishers. They just do not publish any events except for the initial value.
Here is the sample code I used:
#objc class SampleClass: NSObject {
#objc var name: NSString = "1"
}
var a = SampleClass()
let kvoPublisher = a.publisher(for: \.name)
.sink(receiveCompletion: {
print("completion \($0)")
}, receiveValue: { newVal in
print("new val - \(newVal)")
})
a.name = "2"
a.name = "3"
print("Finished; publisher = \(kvoPublisher) | a.name = \(a.name)")
The console output is
new val - 1
Finished; publisher = Combine.AnyCancellable | a.name = 3
Could you please explain what am I missing here and how to fix it?
Thanks.

You also need to mark the property as dynamic in order for it to be KVO compliant. publisher(for:) only works with KVO compliant properties, since it uses KVO under the hood.
#objc class SampleClass: NSObject {
#objc dynamic var name: NSString = "1"
}
Once you do that, the KVO publisher emits the updated values as expected.
For more information on #objc vs #objc dynamic, see this Q&A.
Bear in mind that you should only use KVO publishers when interacting with code that you cannot change. When you want to observe property values of types that you control, use #Published instead.

Related

Swift Combine - Publisher works only with extension for computed variables? Why? [duplicate]

I've just started learning Combine and am quite confused with behaviour of KVO publishers. They just do not publish any events except for the initial value.
Here is the sample code I used:
#objc class SampleClass: NSObject {
#objc var name: NSString = "1"
}
var a = SampleClass()
let kvoPublisher = a.publisher(for: \.name)
.sink(receiveCompletion: {
print("completion \($0)")
}, receiveValue: { newVal in
print("new val - \(newVal)")
})
a.name = "2"
a.name = "3"
print("Finished; publisher = \(kvoPublisher) | a.name = \(a.name)")
The console output is
new val - 1
Finished; publisher = Combine.AnyCancellable | a.name = 3
Could you please explain what am I missing here and how to fix it?
Thanks.
You also need to mark the property as dynamic in order for it to be KVO compliant. publisher(for:) only works with KVO compliant properties, since it uses KVO under the hood.
#objc class SampleClass: NSObject {
#objc dynamic var name: NSString = "1"
}
Once you do that, the KVO publisher emits the updated values as expected.
For more information on #objc vs #objc dynamic, see this Q&A.
Bear in mind that you should only use KVO publishers when interacting with code that you cannot change. When you want to observe property values of types that you control, use #Published instead.

A property in a struct not being updated after being computed in willSet observer

Sorry in advance if my question comes across being stupid, I'm currently learning property observers and I've been given an example from a great swift tutorial online to determine if the code is valid, I correctly assumed it was and decided to implement it in Swift playgrounds. I don't understand why the isMillionaire property remains false despite the if statement evaluating to true.
struct BankAccount{
var name: String
var isMillionaire = false
var balance: Int {
didSet {
if balance > 1_000_000 {
isMillionaire = true
} else {
isMillionaire = false
}
}
}
}
var bankUser1 = BankAccount(name: "John Appleseed", balance: 2_000_000)
print(bankUser1.isMillionaire) //Returns false
Property observers are not called on initialisation, only when the value is set after that, that's why didSet is not being executed.
In this specific case, since isMillionaire is completely derived from balance and shouldn't be able to be updated directly, I would recommend using a computed property, so it would look like this:
var isMillionaire: Bool {
return balance > 1_000_000
}

Is there an alternative to Combine's #Published that signals a value change after it has taken place instead of before?

I would like to use Combine's #Published attribute to respond to changes in a property, but it seems that it signals before the change to the property has taken place, like a willSet observer. The following code:
import Combine
class A {
#Published var foo = false
}
let a = A()
let fooSink = a.$foo.dropFirst().sink { _ in // `dropFirst()` is to ignore the initial value
print("foo is now \(a.foo)")
}
a.foo = true
outputs:
foo is now false
I'd like the sink to run after the property has changed like a didSet observer so that foo would be true at that point. Is there an alternative publisher that signals then, or a way of making #Published work like that?
There is a thread on the Swift forums for this issue. Reasons of why they made the decision to fire signals on "willSet" and not "didSet" explained by Tony_Parker
We (and SwiftUI) chose willChange because it has some advantages over
didChange:
It enables snapshotting the state of the object (since you
have access to both the old and new value, via the current value of
the property and the value you receive). This is important for
SwiftUI's performance, but has other applications.
"will" notifications are easier to coalesce at a low level, because you can
skip further notifications until some other event (e.g., a run loop
spin). Combine makes this coalescing straightforward with operators
like removeDuplicates, although I do think we need a few more grouping
operators to help with things like run loop integration.
It's easier to make the mistake of getting a half-modified object with did,
because one change is finished but another may not be done yet.
I do not intuitively understand that I'm getting willSend event instead of didSet, when I receive a value. It does not seem like a convenient solution for me. For example, what do you do, when in ViewController you receiving a "new items event" from ViewModel, and should reload your table/collection? In table view's numberOfRowsInSection and cellForRowAt methods you can't access new items with self.viewModel.item[x] because it's not set yet. In this case, you have to create a redundant state variable just for the caching of the new values within receiveValue: block.
Maybe it's good for SwiftUI inner mechanisms, but IMHO, not so obvious and convenient for other usecases.
User clayellis in the thread above proposed solution which I'm using:
Publisher+didSet.swift
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
Now I can use it like this and get didSet value:
self.viewModel.$items.didSet.sink { [weak self] (models) in
self?.updateData()
}.store(in: &self.subscriptions)
I'm not sure if it is stable for future Combine updates, though.
UPD: Worth to mention that it can possibly cause bugs (races) if you set value from a different thread than the main.
Original topic link: https://forums.swift.org/t/is-this-a-bug-in-published/31292/37?page=2
You can write your own custom property wrapper:
import Combine
#propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Further to Eluss's good explanation, I'll add some code that works. You need to create your own PassthroughSubject to make a publisher, and use the property observer didSet to send changes after the change has taken place.
import Combine
class A {
public var fooDidChange = PassthroughSubject<Void, Never>()
var foo = false { didSet { fooDidChange.send() } }
}
let a = A()
let fooSink = a.fooDidChange.sink { _ in
print("foo is now \(a.foo)")
}
a.foo = true
Before the introduction of ObservableObject SwiftUI used to work the way that you specify - it would notify you after the change has been made. The change to willChange was made intentionally and is probably caused by some optimizations, so using ObservableObjsect with #Published will always notify you before the changed by design. Of course you could decide not to use the #Published property wrapper and implement the notifications yourself in a didChange callback and send them via objectWillChange property, but this would be against the convention and might cause issues with updating views. (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) and it's done automatically when used with #Published.
If you need the sink for something else than ui updates, then I would implement another publisher and not go agains the ObservableObject convention.
Another alternative is to just use a CurrentValueSubject instead of a member variable with the #Published attribute. So for example, the following:
#Published public var foo: Int = 10
would become:
public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)
This obviously has some disadvantages, not least of which is that you need to access the value as object.foo.value instead of just object.foo. It does give you the behavior you're looking for, however.

KVO for dependent key paths does not work properly for Swift class

I'm trying to write a wrapper around URLSessionTask in Swift. According to the documentation
All task properties support key-value observing.
So I want to keep this behavior and make all the properties on my wrapper also KVO-compliant (usually delegating to the wrapped task) and fully accessible to Objective-C. I'll describe what I'm doing with one property, but I basically want to do the same thing for all properties.
Let's take the property state of URLSessionTask. I create my wrapper like this:
#objc(MyURLSessionTask)
public class TaskWrapper: NSObject {
#objc public internal(set) var underlyingTask: URLSessionTask?
#objc dynamic public var state: URLSessionTask.State {
return underlyingTask?.state ?? backupState
}
// the state to be used when we don't have an underlyingTask
#objc dynamic private var backupState: URLSessionTask.State = .suspended
#objc public func resume() {
if let task = underlyingTask {
task.resume()
return
}
dispatchOnBackgroundQueue {
let task:URLSessionTask = constructTask()
task.resume()
self.underlyingTask = task
}
}
}
I added #objc to the properties so they are available to be called from Objective-C. And I added dynamic to the properties so they will be called via message-passing/the runtime even from Swift, to make sure the correct KVO-Notifications can be generated by NSObject. This is supposed to be enough according to Apple's KVO chapter in the "Using Swift with Cocoa and Objective-C" book.
I then implemented the static class methods necessary to tell KVO about dependent key paths:
// MARK: KVO Support
extension TaskWrapper {
#objc static var keyPathsForValuesAffectingState:Set<String> {
let keypaths:Set<String> = [
#keyPath(TaskWrapper.backupState),
#keyPath(TaskWrapper.underlyingTask.state)
]
return keypaths
}
}
Then I wrote a unit test to check whether the notifications are called correctly:
var swiftKVOObserver:NSKeyValueObservation?
func testStateObservation() {
let taskWrapper = TaskWrapper()
let objcKVOExpectation = keyValueObservingExpectation(for: taskWrapper, keyPath: #keyPath(TaskWrapper.state), handler: nil)
let swiftKVOExpectation = expectation(description: "Expect Swift KVO call for `state`-change")
swiftKVOObserver = taskWrapper.observe(\.state) { (_, _) in
swiftKVOExpectation.fulfill()
}
// this should trigger both KVO versions
taskWrapper.underlyingTask = URLSession(configuration: .default).dataTask(with: url)
self.wait(for: [swiftKVOExpectation, objcKVOExpectation], timeout: 0.1)
}
When I run it, the test crashes with an NSInternalInconsistencyException:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot remove an observer <_XCKVOExpectationImplementation 0x60000009d6a0> for the key path "underlyingTask.state" from < MyURLSessionTask 0x6000002a1440>, most likely because the value for the key "underlyingTask" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the MyURLSessionTask class.'
But by making the underlyingTask-property #objc and dynamic, the Objective-C runtime should ensure that this notification is sent, even when the task is changed from Swift, right?
I can make the test work correctly by sending the KVO-notifications for the underlyingTask manually like this:
#objc public internal(set) var underlyingTask: URLSessionTask? {
willSet {
willChangeValue(for: \.underlyingTask)
}
didSet {
didChangeValue(for: \.underlyingTask)
}
}
But I'd much rather avoid having to implement this for every property and would prefer to use the existing keyPathsForValuesAffecting<Key> methods. Am I missing something to make this work? Or should it work and this is a bug?
Property underlyingTask isn't dynamic.

Simple observable struct with RxSwift?

I'm trying to come up with a simple observable object in Swift and thought to use RxSwift. I couldn't find a simple example to do something like this:
protocol PropertyObservable {
typealias PropertyType
var propertyChanged: Event<(PropertyType, Any)> { get }
}
class Car: PropertyObservable {
typealias PropertyType = CarProperty
let propertyChanged = Event<(CarProperty, Any)>()
dynamic var miles: Int = 0 {
didSet {
propertyChanged.raise(.Miles, oldValue as Any)
}
}
dynamic var name: String = "Turbo" {
didSet {
propertyChanged.raise(.Name, oldValue as Any)
}
}
}
The above is pure Swift solution for observables from this blog post; I really like how it's a protocol-based solution and not invasive. In my case, I have an object in my project where each property is set asynchronously under the hood (bluetooth device). So I need to observe/subscribe to the changes instead of getting/setting the properties in real-time.
I keep hearing RxSwift will do just that and more. However, I can't find a simple example to match above and beginning to think RxSwift is overkill for my need? Thanks for any help.
Easiest way to quickly make this observable with RxSwift would probably be to use the RxSwift class Variable (all code here is untested off the top of my head):
import RxSwift
class Car {
var miles = Variable<Int>(0)
var name = Variable<String>("Turbo")
}
This enables you to observe the values by subscribing to them:
let disposeBag = DisposeBag()
let car = Car
car.name.asObservable()
.subscribeNext { name in print("Car name changed to \(name)") }
.addToDisposeBag(disposeBag) // Make sure the subscription disappears at some point.
Now you've lost the old value in each event. There are of course numerous ways to solve this, the RxSwifty way would probably be to add a scan operation to your element sequence, which works a lot like reduce does on a normal Array:
car.name.asObservable()
.scan(seed: ("", car.name.value)) { (lastEvent, newElement) in
let (_, oldElement) = lastEvent
return (oldElement, newElement)
}
.subscribeNext { (old, new) in print("Car name changed from \(old) to \(new)") }
.addToDisposeBag(disposeBag)