That’s a really good observation.
There are two patterns at work, one of which feels weird and magical, but it’s not.
The protocol methods - methods often declared by a type using the delegation or data source design patterns - already have pre-defined signatures. When a delegating object wants to send a method that it listed in the protocol, it uses the -respondsToSelector method to ask its target “do you implement this method?” before calling it. But it only asks for that one, specific method signature. If it’s not implemented, it doesn’t get called.
The strange behavior of the target-action design pattern - the pattern used by NSTimer, UIButton, and many others - comes down to how the messages are actually sent. When you configure the timer, you gave it a selector. The timer expects a selector that, in turn, takes an argument of type NSTimer *, as indicated in Apple’s NSTimer documentation.
When the timer gets around to calling your method, it uses code that looks like this:
That is, the timer calls on your object the performSelector method, telling your object to run the method you’d previously provided (in your case, updateLastTime:), and the timer passes itself as the argument to your method via the withObject: argument above. But what happens if you told the timer to use a selector that doesn’t take any arguments? Then when the timer calls performSelector:withObject: on your object (because the timer fired), your object doesn’t know what to do with the withObject: argument and simply ignores it. The result is that you can technically provide a selector with the wrong number of arguments and everything still seems to work. The selector you provide must still match the method that you actually implement, but this can take more or less arguments than what the NSTimer expected because of this quirk of performSelector:withObject: - that it quietly ignores extra arguments.
Why does performSelector:withObject: even exist? It’s one of a family of methods implemented in NSObject that exists for exactly this purpose - to allow an object to call a method whose name isn’t known until the program is running. When Apple wrote NSTimer, they didn’t know what method name you’d provide for the timer’s target. Telling an object to performSelector: is like saying “I’m pretty sure you have a method called ‘someMethod:’. Run it. And if I’m wrong and you don’t have a method with that name, just crash.” This is what the timer does - “I’m pretty sure you have a method called updateLastTime:. Please run it, with this argument: (self, the timer). If you don’t actually have that method, just crash.” So when you implement a method (and pass the selector) that takes no arguments - updateLastTime instead of updateLastTime: (the difference being the colon, which indicates an expected argument) - that’s when this “feature” of performSelector: causes everything to still work.
It’s very weird. This is a corner of Objective-C that you can get through most of your career without trying to really understand, because it’s not one we (as 3rd party developers) are really meant to be using. There’s a bit more on this in the book’s last couple chapters about KVO and the Objective-C Runtime.