SwiftUI - KV Observe completion from Combine does not get triggered

Viewed 920

I am trying to build a VOIP app using lib called VailerSIPLib. As the library was built using Obj-C and heavily using NotificationCenter to to publish the changes the active states all over the place.

I currently at the CallView part of the project, I can manage to start, end, reject calls. However, I need to implement connectionStatus in the view which will give information about the call like duration, "connecting..", "disconnected", "ringing" etc.

The below code is all in CallViewModel: ObservableObject;

Variables:

var activeCall: VSLCall!
@Published var connectionStatus: String = ""

Initializer:

override init(){
        super.init()
        NotificationCenter.default.addObserver(self, selector: #selector(self.listen(_:)), name: Notification.Name.VSLCallStateChanged, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.buildCallView(_:)), name: Notification.Name.CallKitProviderDelegateInboundCallAccepted, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.buildCallView(_:)), name: Notification.Name.CallKitProviderDelegateOutboundCallStarted, object: nil)
    }

Methods:

func setCall(_ call: VSLCall) {
    self.activeCall = call
    self.activeCall.observe(\.callStateText) { (asd, change) in
        print("observing")
        print("\(String(describing: change.oldValue)) to \(String(describing: change.newValue)) for \(call.callId)")
    } 
}

@objc func listen(_ notification: Notification) {
       if let _ = self.activeCall {
           print(self.activeCall.callStateText)
       }    
}

@objc func buildCallView(_ notification: Notification) {
    print("inbound call")
    self.isOnCall = true 
}

Problem:

It prints out every thing except the completionBlock in setCall(_:). listen(_:) function validates that the state of the activeCall is changing and I would want to use that directly, however it does not work correct all the time. It should be triggered when the call is answered with callState value of .confirmed but sometime it does. This how I will know that it is time start the timer.

Other point is, in the example project of the VialerSIPLib they used self.activeCall.addObserver(_:) and it works fine. The problem for that is it throws a runtime error at the method something like didObservedValueChange(_:) and logs An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.

Finally there is yellow warning at the activeCall.observe(_:) says

Result of call to 'observe(_:options:changeHandler:)' is unused which I could not find anything related to it.

1 Answers

Finally there is yellow warning at the activeCall.observe(_:) says

Result of call to 'observe(_:options:changeHandler:)'

This is telling you what the problem is. The observe(_:options:changeHandler:) method is only incompletely documented. It returns an object of type NSKeyValueObservation which represents your registration as a key-value observer. You need to save this object, because when the NSKeyValueObservation is destroyed, it unregisters you. So you need to add a property to CallViewModel to store it:

class CallViewModel: ObservableObject {
    private var callStateTextObservation: NSKeyValueObservation?
    ...

And then you need to store the observation:

func setCall(_ call: VSLCall) {
    activeCall = call
    callStateTextObservation = activeCall.observe(\.callStateText) { _, change in
        print("observing")
        print("\(String(describing: change.oldValue)) to \(String(describing: change.newValue)) for \(call.callId)")
    } 
}

You could choose to use the Combine API for KVO instead, although it is even less documented than the Foundation API. You get a Publisher whose output is each new value of the observed property. It works like this:

class CallViewModel: ObservableObject {
    private var callStateTextTicket: AnyCancellable?
    ...

    func setCall(_ call: VSLCall) {
        activeCall = call
        callStateTextTicket = self.activeCall.publisher(for: \.callStateText, options: [])
            .sink { print("callId: \(call.callId), callStateText: \($0)") }
    }

There's no specific reason to use the Combine API in your sample code, but in general a Publisher is more flexible than an NSKeyValueObservation because Combine provides so many ways to operate on Publishers.


Your error with addObserver(_:forKeyPath:options:context:) happens because that is a much older API. It was added to NSObject long before Swift was invented. In fact, it was added before Objective-C even had blocks (closures). When you use that method, all notifications are sent to the observeValue(forKeyPath:of:change:context:) method of the observer. If you don't implement the observeValue method, the default implementation in NSObject receives the notification and raises an exception.

Related