-2

In the function,I can get a parameter which is UISlider.But its parameter‘s type is CALayer. The Compiler don't throw error ,it's unbeleivable . I don't know how do I get it, But it's effective.

@IBOutlet weak var vectorSlider: UISlider!

override func viewDidLoad() {
    super.viewDidLoad()
    vectorSlider.addTarget(nil, action: #selector(changeVector), for: UIControl.Event.valueChanged) // Less than Swift5,UIControlEvents.valueChanged
}

@IBAction func changeVector(layer: CALayer) {
    let transformAnimaZ = CABasicAnimation.init(keyPath: "transform.rotation.z")
    transformAnimaZ.fromValue = vectorSlider.value
    transformAnimaZ.toValue = vectorSlider.value
    let transformGroup = CAAnimationGroup()
    transformGroup.animations = [transformAnimaZ]
    transformGroup.isRemovedOnCompletion = false
    transformGroup.fillMode = CAMediaTimingFillMode.forwards // Less than Swift5,kCAFillModeForwards
    layer.add(transformGroup, forKey: "transform")
}

I want to know how to get the parameter. I only find some answer about UIControl, not CALayer. Why can I add layer animat by this way.This is a Demo's picture of SliderRotate

Albert
  • 11
  • 2

1 Answers1

2

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:

enter image description here

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I have read your answer carefully.I'm sorry to upload error code, I have update it. I have learnt a lot from your answer.But it can't resolve my doubt.Please try the new code, thank you very much! – Albert Jul 15 '19 at 01:16
  • You just need to read my answer more carefully. You cannot make a layer arrive into a control action handler. It will always be a control, such as a button or a slider. I proved it and now you proved it too – matt Jul 15 '19 at 02:10
  • I have the same conclusion as you, that why I can't understand it. The control really rotate after the slider changed value. – Albert Jul 15 '19 at 02:28
  • It's for the same reason I gave in my answer: you don't know everything about the secret features of every class. You've gotten past the compiler and send `add(_:forKey:)` to a UIView. Well, that's legal in the sense that secretly UIView does in fact respond to that, passing the message along to its layer. But it's a secret! You've peeked under the hood, that's all that's happened. No harm done. But you are not magically turning a view into a layer or anything like that. – matt Jul 15 '19 at 03:14
  • Try this in a playground: `let slider = UIView(); (slider as AnyObject).responds(to: #selector(CALayer.add(_:forKey:)))`. It comes back `true`. So this is legal in Objective-C. But it's not part of the published API. You've gone behind the back of the published API by lying to the compiler. So you found out the secret. – matt Jul 15 '19 at 03:15
  • Added that to my answer. – matt Jul 15 '19 at 03:20
  • I think I got it almost. it's deeply grateful your answer, Thank you. Because I just learn a few Objective-C knowledge, I don't understand `responds` very well. In my opinion, `UIView` and `CALayer` both follow protocol of `SCNAnimatable`, so the status return true. Do I take it? – Albert Jul 15 '19 at 06:45
  • OK. I'll take your advice. Thank you very much. – Albert Jul 15 '19 at 12:45
  • I have posted a gist https://gist.github.com/mattneub/7651413d5f06d4466df42b675ca6864b This shows you the complete list of UIView methods, public and private. Most of them are private! (That means they are secret.) If you look carefully you will see that `addAnimation:forKey:` is one of them. – matt Jul 15 '19 at 12:49
  • After a year age, I read your anwser again.I get some new infomation and I finally understand what you mean. Thank your answer. – Albert Apr 22 '21 at 08:45