Very strange bug in this 1 line function - swift

So I stumbled onto something really weird.
Here is a 1 line function
#objc func foo(value: Int = 1) {
print("value is \(value)")
}
I'm gonna set up a tap gesture in viewDidLoad to call that function:
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
self.view.addGestureRecognizer(tap)
Here's the handleTap method. Note how I call foo without any arguments.
#objc func handleTap() {
foo()
}
As you'd expect, it prints out a 1 every time.
Enter The Dragon 🐉
Here's where it gets weird. Let's change handleTap to only post a notification. And we'll add an observer for that notification in viewDidLoad to call foo, like so.
The notification:
extension Notification.Name {
static let didSomething = Notification.Name("didSometing")
}
The new handleTap method:
#objc func handleTap() {
NotificationCenter.default.post(name: .didSomething, object: nil)
}
And our addition to viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(foo), name: .didSomething, object: nil)
Now every time I tap, I get all kinds of values, here's what my output looks like:
value is 10748457248
value is 10748611248
value is 10748564928
value is 10748456000
value is 10748564256
value is 10748612688
value is 10748611824
value is 10748609616
value is 10748612496
value is 10748564592
Now if I change the notification observation to call a new method fooCaller (which is just a method that calls foo()), then I get all 1's.
So I figure it's something to do with obj-c, selectors and arguments, but thing is that my foo can be called without any arguments, like foo(), so I would reasonably expect #selector(foo) to have the same behaviour but it doesn't, and I don't know why.
In actuality, my function (which is represented here by foo) never took any arguments and was only called from notification observations. Then I later needed to call foo directly, passing something. So I gave it a default value so as not to disturb the current use of foo elsewhere, but it opened up the dark dimension instead.

There is sort of a bug, in the sense that, in my opinion, your code should not compile. And it does indeed, as you suspect, have to do with the tricky interface between Objective-C and Swift.
Let's look at the declaration of addObserver in Objective-C:
- (void)addObserver:(id)observer selector:(SEL)aSelector
name:(NSNotificationName)aName object:(id)anObject;
According to the docs, the selector must refer to a method that has
one and only one argument (an instance of NSNotification)
That's because the NSNotification can contain important information, such as its userInfo, that you might need to receive.
In other words, your foo, if it is to be the method called by the notification center to let you know of the notification, should have this signature:
#objc func foo(_ notification: Notification) {
And if it did, you'd be able to do stuff like this:
#objc func foo(_ notification: Notification) {
print(notification) // name = didSomething, object = nil, userInfo = nil
}
Great. But your foo does not have that signature; it types its parameter as Int instead. This is not identically what you have, but it might as well be:
#objc func foo(_ value: Int) {
Now, in my opinion, that should not be permitted. If you declare your foo that way, then when the time comes to use it as the selector...
NotificationCenter.default.addObserver(self, selector: #selector(foo), name: .didSomething, object: nil)
...the compiler should complain: "No, you can't do that, foo has the wrong type as its parameter!" But it doesn't.
So the Notification object arrives and is interpreted as an Int in accordance with its memory location — and that is what you are printing.

Your console log print these numbers because of this line:
NotificationCenter.default.addObserver(self, selector: #selector(foo), name: .didSomething, object: nil)
This function is passing the Notification as a parameter which causes these logs. Modify your function to receive the notification and handle it.
#objc func foo(notification: Notification) {
print("received notification: \(notification)")
}
Try passing the foo function as parameter for selector in tapGestureRecognizer, like this:
let tap = UITapGestureRecognizer(target: self, action: #selector(foo))
view.addGestureRecognizer(tap)
You'll see a similar result it's because because here the UITapGestureRecognizer is being passed to the foo function.

Related

Selector function not being entered when observer is added

In my first view controller, I post the notification with the following code:
NotificationCenter.default.post(name: Notification.Name("date"), object: formattedDate)
I then "receive" the notification in a second view controller with the following code:
func receiveNotification () {
NotificationCenter.default.addObserver(self, selector: #selector(self.didGetTheDate(_:)), name: NSNotification.Name("date"), object: nil)
}
#objc
func didGetTheDate(_ notification: Notification) {
print("In did get date")
date = notification.object as! String
}
However, the function "didGetTheDate" never gets called.
I have triple checked that the function "receiveNotification" gets called as I have added print statements to check this.
Can somebody please help me with this.
NSNotificacionCenter is a variation of the Observer Pattern, you can read about it here
This means that you will have to register an Observer before posting any notification. If you post anything before that, the NSNotificationCenter will look at the observer for name and will see that nobody is listening for it, thus nothing will happen.

NotificationCenter not always calls observer when specified object is a String

I have been trying to check why my observer is sometimes called and sometimes isn't when specifying a String object to filter out notifications.
My original code:
NotificationCenter.default.addObserver(self, selector: #selector(update), name: .pathContentsChanged, object: self.folder.path) // Where path is something like "/user/folder"
#objc func update()
{
// Not always called
}
My current workaround, which confirms that the notification object does match self.folder.path:
NotificationCenter.default.addObserver(self, selector: #selector(pathContentsChanged(_:)), name: .pathContentsChanged, object: nil) // No object specified anymore
#objc func pathContentsChanged(_ notification: Notification)
{
// This does get called every time
guard let path = notification.object as? String,
path == self.folder.path else
{
return
}
self.update() // notification.object and self.folder.path do match
}
Is it inherent at how Strings work on Swift? Is it because notification.object could be a bridged NSString?
NotificationCenter filters notification per-object comparing objects by-reference, and not by isEqual as you'd might expect. So posting notification with different strings (even equal by content) might not result your observer to be filtered out.
In general, String is not that object to be used in notification.object (due to its nature, constants, bridged, etc.)... Most usual scenario is to have some your custom object (that owns workflow) to be the originator of notification (so be notification object).
And if you want to have strings in object then yes, your observers have to be non filtered 'object: nil' and act conditionally inside handler.

Is removing a NotificationCenter observer that was created with closure syntax by name adequate?

I have a few notifications that were created using block / trailing closure syntax which look like this:
NotificationCenter.default.addObserver(forName: .NSManagedObjectContextObjectsDidChange, object: moc, queue: nil) { note in
// implementation
}
Which I was later removing by name, like this:
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: moc)
My Question
Is this adequate? Or do I absolutely need to save the NSObjectProtocol to it's own property and remove that property with the following syntax?
NotificationCenter.default.removeObserver(didChangeNotification)
You absolutely need to store the return value in a property and remove that later on.
From https://developer.apple.com/reference/foundation/nsnotificationcenter/1411723-addobserverforname:
Return Value
An opaque object to act as the observer.
When you call any one of the removeObserver methods, the first parameter is the observer to remove. When you set up a block to respond to a notification, self is not the observer, NSNotificationCenter creates its own observer object behind the scenes and returns it to you.
Note: as of iOS 9, you are no longer required to call removeObserver from dealloc/deinit, as that will happen automatically when the observer goes away. So, if you're only targeting iOS 9, this may all just work, but if you're not retaining the returned observer at all, the notification could be removed before you expect it to be. Better safe than sorry.
To add to #Dave's answer, it looks like documentation isn't always 100% accurate. According to this article by Ole Begemann there is a contradiction in the doc and self-removing magic was not happening as of iOS 11.2 in his test app.
So that the answer is still "Yes, one needs to remove that observer manually" (and yes, self is not the observer, the result of addObserver() method is the observer).
Here an example with code, for how a correct implementation looks like:
Declare the variable that gets returned when you add the observer in your class A (the receiver of the notification or observer):
private var fetchTripsNotification: NSObjectProtocol?
In your init method add yourself as an observer:
init() {
fetchTripsNotification = NotificationCenter.default.addObserver(forName: .needsToFetchTrips, object: nil, queue: nil) { [weak self] _ in
guard let `self` = self else {
return
}
self.fetchTrips()
}
}
In the deinit method of your class, make sure to remove the observer:
deinit { NotificationCenter.default.removeObserver(fetchTripsNotification as Any) }
In your class B (the poster of the notification) trigger the notification like usually:
NotificationCenter.default.post(name: .needsToFetchTrips, object: nil)

how to use selector: with function that throws exception in Swfit

I have an SKScene that makes itself an observer of a notification named " showPhotoForMoodNotification" with an associated selector called : "eventListenerDidReceiveNotification:" .
The eventListenerDidReceiveNotification is declared as a function that can throw and exception as follows:
func eventListenerDidReceiveNotification(notif:NSNotification) throws { }
But I noticed that when the notification is received by the SKScene, the compiler doesn't associate the signature of this "eventListenerDidReceiveNotification" method with the signature of the "selector" in the addObserver called, which looks like thisL
NSNotificationCenter.defaultCenter().addObserver(self, selector: "eventListenerDidReceiveNotification:", name: "showPhotoForMoodNotification", object: nil)
The error i get is this:
So, my guess is that the "throws" part of the method's signature is not compatible with the "selector" part of the nsnotification "addObserver" call, because if I eliminate the "throws" part from the "eventListenerDidReceiveNotification" method declaration, things work.
So do I have to add anything more to the addObserver "selector" part to describe this method as a method that throws an exception?
thanks
Possible answer here. BTW, in Swift 2.2 (actually, i don't know what version you are using) there is new syntax for selectors which is recommended way to use it. (IBAction connected to button TouchUpInside event in storyboard)
Actually, i just tested this code and it worked:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(test(_:)), name: "TestNotification", object: nil)
}
#objc private func test(notification: NSNotification) throws {
print("notification")
}
#IBAction private func fireNotification() {
NSNotificationCenter.defaultCenter().postNotificationName("TestNotification", object: nil)
}
IIRC, Swift methods like
func f(x: T) throws -> U
Are viewed in Objective C as
- (nullable U *)fWithX:(T *)x error:(NSError **)errorPtr;
So you may try adding that error: part in your selector.
EDIT:
And
func f() throws -> U
Becomes
- (nullable U *)fAndReturnError:(NSError **)errorPtr;

Observer never called

I have two functions
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
NSNotificationCenter.defaultCenter().addObserverForName("personalDataDidLoad", object: self, queue: NSOperationQueue.mainQueue()) {_ in
print("Received notification")
self.showPersonalData()
}
loadPersonalData()
}
func loadPersonalData() {
//load data
print("personal data loaded")
NSNotificationCenter.defaultCenter().postNotificationName("personalDataDidLoad", object: nil)
but for some reason this outputs
personal data loaded
instead of the expected
personal data loaded
Received notification
I'm probably missing something obvious, but I don't see it right now....
I also tried addObserver with selector: "showPersonalData:" but this throws an unrecognized selector exception..
The problem is with the 2nd parameter in postNotificationName and addObserverForName: object. When you add an observer, and pass a non-nil object value, this means that the observer block will run when a notification comes from that object, and from that object only.
However, when you fire the notification, you do object: nil. So your notification is ignored.
On the other hand, passing a nil value for object means, "I want to receive this notification regardless of who sends it".
So you need to make sure the object value is the same in both places: either self or nil.
Is there a reason you need to use addObserverForName(_:object:queue:usingBlock:)?
Try this instead:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, "personalDataDidLoadNotification:", name: "personalDataDidLoad" object: nil)
loadPersonalData()
}
func loadPersonalData() {
//load data
print("personal data loaded")
NSNotificationCenter.defaultCenter().postNotificationName("personalDataDidLoad", object: nil)
}
func personalDataDidLoadNotification(notification: NSNotification) {
print("notification recieved")
}
Another answer to the title question (but not this example) but will hopefully help others in the situation I have been in for the last 3 hours:
Make sure your notificationcenter observer is added inside a class that has a persisted instance. I created the observer inside a class that was called as e.g. MyClass().setupNotification inside a local method of another class.
This meant the observer was immediately deleted and didnt persist against any instance.
Schoolboy error - but hope this helps others searching for this.