It will help to consider the matter in stages. Let's start with the standard pattern (this is in a view controller):
@IBOutlet var myButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.myButton.addTarget(self, action: #selector(action), for: .touchUpInside)
}
@objc func action(sender:UIView) {
print("here")
}
What's happening here is that we step outside of Swift into Cocoa's Objective-C world. In #selector(action)
all we're doing is forming a string — in this case, "actionWithSender:"
, but the details are irrelevant. Cocoa now knows that if the button is tapped, it should send us the message "actionWithSender:"
. There is one parameter, so Cocoa passes along the sender, which is the button. That is simply how Cocoa behaves with regard to the Control target-action mechanism when the action method has one parameter; there's nothing you can do or change about that.
Now let's change the Swift declaration of the parameter type:
@IBOutlet var myButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.myButton.addTarget(self, action: #selector(action), for: .touchUpInside)
}
@objc func action(layer:CALayer) {
print("here")
}
This changes nothing as far as Objective-C is concerned. We changed the name of the function to "actionWithLayer:"
, but Objective-C knows nothing about how the parameter is typed (the "CALayer" part). It just goes right ahead and calls the method, and passes along the sender, which is still the button. You can prove that by printing the layer
parameter:
@IBOutlet var myButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.myButton.addTarget(self, action: #selector(action), for: .touchUpInside)
}
@objc func action(layer:CALayer) {
print(layer) // UIButton...
}
So now you have lied to Swift about what sort of object will be arriving as the parameter of our action
method. But your lie didn't matter because you didn't do anything with that parameter except print
it. Okay, but now let's change the transform
of layer
:
@IBOutlet var myButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.myButton.addTarget(self, action: #selector(action), for: .touchUpInside)
}
@objc func action(layer:CALayer) {
print(layer) // UIButton...
layer.transform = CATransform3DMakeRotation(.pi, 0, 0, 1)
}
Swift permits us to say that, because a CALayer has a transform
and it is a CATransform3D. But there's just one problem. This is not in fact a CALayer; it's a UIView (a button in this case). A button has a layer, but it isn't a CATransform3D (it's an affine transform instead). So Swift permits the message but it is ineffective.
Perhaps it's surprising that we don't crash, but that's probably because a button does in fact have a transform
in the first place. Having escaped from strong typing, we are able to do something incoherent and get away with it. But you'd crash if you'd say this:
@IBOutlet var myButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.myButton.addTarget(self, action: #selector(action), for: .touchUpInside)
}
@objc func action(layer:CALayer) {
print(layer) // UIButton...
layer.zPosition = 0
}
Basically you've lied to Swift (this thing is a button, not a CALayer), so we are allowed to try to set its zPosition
, but then it turns out at runtime that it is a button and has no zPosition
and we crash.
What's the moral? Don't do that! You are lying to Swift and thus eviscerating its best feature, its strong type checking. Instead, always type your sender
as Any and cast down safely. Now the cast will stop you if you get it wrong:
@IBOutlet var myButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.myButton.addTarget(self, action: #selector(action), for: .touchUpInside)
}
@objc func action(sender:Any) {
if let layer = sender as? CALayer {
layer.zPosition = 0
} else {
print("it's not a layer you silly person")
}
}
EDIT In your second version of the question, you've sent add(_:forKey:)
to the slider and it works as if you had sent it to the layer. But the slider is not a layer. What's happened is you've discovered a secret: behind the scenes, in Objective-C / Cocoa, UIView does in fact respond to addAnimation:forKey:
, as we can see by probing Cocoa with a symbolic breakpoint:

And we can discover the same fact by probing with responds
:
let slider = UIView()
(slider as AnyObject).responds(to: #selector(CALayer.add(_:forKey:))) // true!
And it happens that a UIView responds to this message by passing it along to its layer. But that doesn't change the reality: just because you say this is a CALayer, it is not! It is a UIView!! It's just coincidence that in fact UIView responds to this message and you don't crash and in fact the layer animates.