I have an object which I want to send throughout multiple listeners/subscribers, so I was checking out Combine and I saw 2 different kind of publishers, namely NotificationCenter.Publisher and PassThroughSubject. I am confused why anyone would use a NotificationCenter.Publisher over PassThroughSubject.
I came up with the code below, demonstrating both ways. To summarize:
NotificationCenter.Publisher needs to have a Notification.Name static property
Isn't really that typesafe (since I can post a different kind of object for the same Notification.Name/different publisher for the same Notification.Name)
Posting a new value needs to be done on NotificationCenter.default (not the publisher itself)
An explicit downcast to the used type in the map closure
In what scenarios someone will use NotificationCenter.Publisher over PassThroughSubject?
import UIKit
import Combine
let passThroughSubjectPublisher = PassthroughSubject<String, Never>()
let notificationCenterPublisher = NotificationCenter.default.publisher(for: .name).map { $0.object as! String }
extension Notification.Name {
static let name = Notification.Name(rawValue: "someName")
}
class PassThroughSubjectPublisherSubscriber {
init() {
passThroughSubjectPublisher.sink { (_) in
// Process
}
}
}
class NotificationCenterPublisherSubscriber {
init() {
notificationCenterPublisher.sink { (_) in
// Process
}
}
}
class PassThroughSubjectPublisherSinker {
init() {
passThroughSubjectPublisher.send("Henlo!")
}
}
class NotificationCenterPublisherSinker {
init() {
NotificationCenter.default.post(name: .name, object: "Henlo!")
}
}
If you have to use a 3rd party framework that uses NotificationCenter.
NotificationCenter can be thought of as a first generation message passing system, while Combine is second generation. It has runtime overhead and requires casting the objects you can store in Notifications. Personally I would never use NotificationCenter when building an iOS 13 framework, but you do need to use it to access a lot of iOS notifications that are only published there. Basically in my personal projects I’m going to treat it as read only unless absolutely necessary.
Related
I am working on a SwiftUI project, where I use the MVVM-architecture.
When changing a View-model object property from the SwiftUI view, it causes a memory conflict crash in the view-model object.
The error is of the type: Simultaneous accesses to 0x600003591b48, but modification requires exclusive access.
In steps, here is what happens:
View-model property is changed from view
View-model property changes model property
Model property notifies about changes
View-model receives change notification
View-model access model object
Crash occur due to memory conflict
Relevant code snippets are seen below. Xcode project is a standard SwiftUI project.
The error will happen, after first clicking the add button, and then the modify button.
If the "update" code is moved into the "receiveValue" closure, the error will not occur. Likewise, the error will not occur, if the View-model class is made non-generic.
To my best knowledge, the code is all-right, so I suspect it is a compiler problem. But I am not sure.
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var item: ViewModel<Model> = ViewModel<Model>()
var body: some View {
VStack {
Button("Add", action: { item.add(model:Model()) })
Button("Modify", action: { item.selected.toggle() })
}
}
}
protocol ModelType {
var objectDidChange: ObservableObjectPublisher { get }
var selected: Bool { get set }
}
class Model: ModelType {
let objectDidChange = ObservableObjectPublisher()
var selected = false {
didSet {
objectDidChange.send()
}
}
}
class ViewModel<Model:ModelType>: ObservableObject {
var selected = false {
didSet {
model.selected = selected
}
}
func add(model: Model) {
self.model = model
cancellable = model.objectDidChange.sink(receiveValue: { _ in
self.update()
})
}
private var model: Model! = nil
private var cancellable: AnyCancellable? = nil
func update() {
// Crash log: Simultaneous accesses to 0x600003591b48, but modification requires exclusive access.
print("update \(model.selected)")
}
}
Short version: require AnyObject for ModelType.
Long version:
You're trying to read from self.model while you're in the middle of setting self.model. When you say "If the "update" code is moved into the "receiveValue" closure, the error will not occur," this isn't quite correct. I expect what you mean is you wrote this:
cancellable = model.objectDidChange.sink(receiveValue: { _ in
print("update \(model.selected)")
})
And that worked, but that's completely different code. model in this case is the local variable, not the property self.model. You'll get the same crash if you write it this way:
cancellable = model.objectDidChange.sink(receiveValue: { _ in
print("update \(self.model.selected)")
})
The path that gets you here is:
ViewModel.selected.didSet
WRITE to Model.selected <---
Model.selected.didSet
(observer-closure)
ViewModel.update
READ from ViewModel.model <---
This is a read and write to the same value, and that violates exclusive access. Note that the "value" in question is "the entire ViewModel value," not ViewModel.selected. You can show this by changing the update function to:
print("update \(model!)")
You'll get the same crash.
So why does this work when you take out the generic? Because this particularly strict version of exclusivity only applies to value types (like structs). It doesn't apply to classes. So when this is concrete, Swift knows viewModel is a class, and that's ok. (The why behind this difference a bit complex, so I suggest reading the proposal that explains it.)
When you make this generic, Swift has to be very cautious. It doesn't know that Model is a class, so it applies stricter rules. You can fix this by promising that it's a class:
protocol ModelType: AnyObject { ... }
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.
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.
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.
I have a case where I am using a 3rd party library and I would like to make it into an Observable. Appropriately, the library is designed around delegates as one would expect so I am wrapping it. The library performs an async operation and calls it's delegate with the results when it completes.
I definitely want to take advantage of the cold nature of the observable and only start the operation when someone subscribes. I have a solution that works, I just don't know if it's deeply flawed and I am missing some important understanding of RxSwift or perhaps there is a simpler way to achieve the same goal.
public final class RxLibBridge: LibDelegate{
let lib = Lib()
let _source = PublishSubject<[LibResult]>()
public init(){
lib.delegate = self
}
public func asObservable() -> Observable<[LibResult]>{
// create a cold observable to start
// the Lib's async operation on subscribe.
return Observable<Void>.create{
observer in
self.lib.startOperation()
// emit and complete
observer.onNext(())
observer.onCompleted()
return Disposables.create()
}
// convert the `Void` observable into an observable from the
// PublishSubject
.flatMapLatest{self._source}
}
// the lib's completion delegate method
public func lib(_ lib: Lib, didFinishWithResult results: [LibResult]) {
// grab the PublishSubject, emit the result and complete
let observer = _source.asObserver()
observer.onNext(results)
observer.onCompleted()
}
}
So my question is: Is this an appropriate pattern in Rx? Again, it works:
RxLibBridge()
.asObservable()
.subscribe(...)
Just because it works though doesn't mean I have not fundamentally misunderstood the proper way to work with this situation.
I know there is a way in RxSwift to handle something like this:
https://medium.com/#maxofeden/rxswift-migrate-delegates-to-beautiful-observables-3e606a863048#.rksg2ckpj
https://samritchie.net/2016/05/12/rxswift-delegateproxy-with-required-methods/
I tried this approach but it looks like the API changed since 2015. Namely, in the example links above proxyForObject cannot be found when adding the rx_delegate method in the extension.
Additionally, this approach appears to favor pure Objective-C [UIKit/AppKit] APIs. In my attempt to follow the linked example, I was editing the source of the 3rd party lib to make the delegate method optional and exposing it to #objc. The lib's delegate is required and I would rather not have to fork the lib to make the modifications.
This SO answer provided the updated API for the 2 links above:
Can not use proxyForObject function in DelegateProxyType (rxSwift)
So after digging some more, it looks like this will do the trick with a required delegate method, updated for RxSwift 3.3.1. This is using their DelegateProxy system.
import RxSwift
import RxCocoa
import Lib
public final class RxLibDelegate: DelegateProxy, LibDelegate, DelegateProxyType{
let _subject = PublishSubject<[LibResult]>()
public static func currentDelegateFor(_ object: AnyObject) -> AnyObject?{
let target = object as! Lib
return target.delegate
}
public static func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
let target = object as! Lib
target.delegate = delegate as? LibDelegate
}
public func lib(_ lib: Lib, didFinishWithResult results: [LibResult]) {
_subject.onNext(results)
_subject.onCompleted()
}
}
extension Lib{
public var rx_delegate: DelegateProxy{
// `proxyForDelegate` moved as compared to examples at:
// https://samritchie.net/2016/05/12/rxswift-delegateproxy-with-required-methods/
// https://medium.com/#maxofeden/rxswift-migrate-delegates-to-beautiful-observables-3e606a863048#.rksg2ckpj
return RxLibDelegate.proxyForObject(self)
}
public var rx_libResults: Observable<[LibResult]> {
// `proxyForDelegate` moved as compared to examples at:
// https://samritchie.net/2016/05/12/rxswift-delegateproxy-with-required-methods/
// https://medium.com/#maxofeden/rxswift-migrate-delegates-to-beautiful-observables-3e606a863048#.rksg2ckpj
let proxy = RxLibDelegate.proxyForObject(self)
return proxy._subject
}
}
That's about 28 LOC. My original "wrapper" (see updated version below) but I don't know if it's the best is 21 LOC; 6 of 1 half dozen of the other?
In my particular case I only have 1 delegate method to worry about. If you were working with some functionality that had multiple delegates I think the DelegateProxy + extension methods would be a lot more practical and the better choice in that case.
Regarding my original trial wrapping thing using that Void observable, it appears it's totally acceptable to alter the stream with flatMapLatest as evidenced here re: Sending continual events while a button is pressed:
https://stackoverflow.com/a/39123102/1060314
import RxSwift
import RxCocoa
let button = submitButton.rx_controlEvent([.TouchDown])
button
.flatMapLatest { _ in
Observable<Int64>.interval(0.1, scheduler: MainScheduler.instance)
.takeUntil(self.submitButton.rx_controlEvent([.TouchUpInside]))
}
.subscribeNext{ x in print("BOOM \(x)") }
.addDisposableTo(disposeBag)
//prints BOOM 0 BOOM 1 BOOM 2 BOOM 3 BOOM 4 BOOM 5 for every 0.1 seconds
Note that a new Observable is returned from flatMapLatest. The author cites the RxSwift slack channel, so I assume it is at least acceptable to do.
Here's an updated version of my wrapper version that I think might be a bit cleaner:
import RxSwift
public final class RxLibBridge: LibDelegate{
let lib = Lib()
let _source = PublishSubject<[LibResult]>()
public init(){
lib.delegate = self
}
public func asObservable() -> Observable<[LibResult]>{
// create a cold observable to start
// the Lib's async operation on subscribe.
return Observable.just(())
.do(onNext: {
self.lib.startOperation()
})
.flatMapLatest{self._source}
}
// the lib's completion delegate method
public func lib(_ lib: Lib, didFinishWithResult results: [LibResult]) {
// grab the PublishSubject, emit the result and complete
_source.onNext(results)
_source.onCompleted()
}
}
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)