Last active
November 12, 2025 06:26
-
-
Save SoundBlaster/459ae7445135c7a7cf771779b200581f to your computer and use it in GitHub Desktop.
Demo for problem of animation sublayer with explicit animations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import UIKit | |
| class SublayerDelegate: NSObject, CALayerDelegate { | |
| func animationFromSuperlayer(of layer: CALayer, forKey event: String) -> CAAnimation? { | |
| guard let superlayer = layer.superlayer else { return nil } | |
| guard let firstAnimationKey = superlayer.animationKeys()?.first else { return nil } | |
| return superlayer.animation(forKey: event) ?? superlayer.animation(forKey: firstAnimationKey) | |
| } | |
| func action(for layer: CALayer, forKey event: String) -> (any CAAction)? { | |
| print("------- \(event)") | |
| layer.superlayer?.animationKeys()?.forEach({ print($0) }) | |
| print("-------") | |
| if ["bounds", "position"].contains(event) { | |
| guard let superAnimation = animationFromSuperlayer(of: layer, forKey: event) else { | |
| return NSNull() | |
| } | |
| if let springAnimation = superAnimation as? CASpringAnimation { | |
| let animation = CASpringAnimation(perceptualDuration: springAnimation.perceptualDuration, bounce: springAnimation.bounce) | |
| // это не работает для относительных анимаций с isAdditive=true! | |
| animation.fromValue = layer.presentation()?.value(forKeyPath: event) | |
| return animation | |
| } | |
| let animation = CABasicAnimation(keyPath: event) | |
| // это не работает для относительных анимаций с isAdditive=true! | |
| // animation.fromValue = layer.presentation()?.value(forKeyPath: event) | |
| animation.duration = superAnimation.duration | |
| animation.timingFunction = superAnimation.timingFunction | |
| return animation | |
| } | |
| return NSNull() | |
| } | |
| } | |
| class MySublayer: CALayer { | |
| override init() { | |
| super.init() | |
| backgroundColor = UIColor.green.withAlphaComponent(0.5).cgColor | |
| } | |
| required init?(coder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| override init(layer: Any) { | |
| super.init(layer: layer) | |
| } | |
| open override func add(_ anim: CAAnimation, forKey key: String?) { | |
| guard let key else { return } | |
| let mark: String.Element = "-" | |
| let fixedKey = if let endIndex = key.firstIndex(of: mark) { | |
| String(key[..<endIndex]) | |
| } else { | |
| key | |
| } | |
| print("\(type(of: self)) | add animation: \(String(describing: key)), fixed: \(fixedKey)") | |
| super.add(anim, forKey: key) | |
| } | |
| } | |
| class MyLayer : CALayer { | |
| let sublayer = MySublayer() | |
| let sublayerDelegate = SublayerDelegate() | |
| override init() { | |
| super.init() | |
| backgroundColor = UIColor.blue.cgColor | |
| self.sublayer.delegate = sublayerDelegate | |
| self.addSublayer(self.sublayer) | |
| } | |
| required init?(coder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| override init(layer: Any) { | |
| super.init(layer: layer) | |
| } | |
| open override func add(_ anim: CAAnimation, forKey key: String?) { | |
| guard let key else { return } | |
| let mark: String.Element = "-" | |
| let fixedKey = if let endIndex = key.firstIndex(of: mark) { | |
| String(key[..<endIndex]) | |
| } else { | |
| key | |
| } | |
| print("\(type(of: self)) | add animation: \(String(describing: key)), fixed: \(fixedKey)") | |
| if let basicAnimation = anim as? CABasicAnimation { | |
| let currentValue = self.presentation()?.value(forKeyPath: fixedKey) | |
| print("current state: \(String(describing: currentValue)), animation.fromValue: \(String(describing: basicAnimation.fromValue))") | |
| } | |
| super.add(anim, forKey: key) | |
| // super.add(anim, forKey: fixedKey) | |
| } | |
| override func layoutSublayers() { | |
| super.layoutSublayers() | |
| self.sublayer.bounds = .init(origin: .zero, size: bounds.size) | |
| self.sublayer.position = .init(x: bounds.midX, y: bounds.midY) | |
| } | |
| } | |
| class MyView : UIView { | |
| override init(frame: CGRect) { | |
| super.init(frame: frame) | |
| self.backgroundColor = .red | |
| } | |
| required init?(coder: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| override class var layerClass: AnyClass { | |
| return MyLayer.self | |
| } | |
| func toggleSublayerHiddenState() { | |
| (self.layer as? MyLayer)?.sublayer.isHidden.toggle() | |
| } | |
| } | |
| class ViewController: UIViewController { | |
| var isBigButton: Bool = false { | |
| didSet { | |
| self.view.setNeedsLayout() | |
| } | |
| } | |
| let myView = MyView(frame: .init(origin: .zero, size: .init(width: 100, height: 100))) | |
| let myViewAnimateButton = UIButton(type: .system) | |
| let hideSublayerButton = UIButton(type: .system) | |
| override func viewDidLoad() { | |
| super.viewDidLoad() | |
| view.addSubview(myView) | |
| myViewAnimateButton.setTitle("Animate", for: .normal) | |
| myViewAnimateButton.addTarget(self, action: #selector(animate), for: .touchUpInside) | |
| myViewAnimateButton.sizeToFit() | |
| view.addSubview(myViewAnimateButton) | |
| hideSublayerButton.setTitle("Hide sublayer (green)", for: .normal) | |
| hideSublayerButton.addTarget(self, action: #selector(hideSublayer), for: .touchUpInside) | |
| hideSublayerButton.sizeToFit() | |
| view.addSubview(hideSublayerButton) | |
| } | |
| override func viewWillLayoutSubviews() { | |
| super.viewWillLayoutSubviews() | |
| myView.bounds.size = isBigButton ? .init(width: 300, height: 300) : .init(width: 100, height: 100) | |
| myView.center = view.center | |
| myViewAnimateButton.center = .init(x: view.bounds.midX, y: view.bounds.maxY - 50) | |
| hideSublayerButton.center = .init(x: view.bounds.midX, y: view.bounds.maxY - 100) | |
| } | |
| @objc func animate() { | |
| UIView.animate( | |
| withDuration: 1, | |
| delay: 0, | |
| // usingSpringWithDamping: 1.0, | |
| // initialSpringVelocity: 0.0, | |
| options: [ | |
| // .curveEaseInOut, | |
| // .beginFromCurrentState, | |
| // .layoutSubviews | |
| ], | |
| ) { | |
| self.isBigButton.toggle() | |
| self.view.layoutIfNeeded() | |
| } | |
| } | |
| @objc func hideSublayer() { | |
| myView.toggleSublayerHiddenState() | |
| UIView.animate(withDuration: 0.2) { [weak self] in | |
| guard let self else { return } | |
| self.hideSublayerButton.setTitle(self.myView.layer.sublayers?.first?.isHidden == false ? "Show sublayer (green)" : "Hide sublayer (green)", for: .normal) | |
| self.hideSublayerButton.sizeToFit() | |
| self.view.setNeedsLayout() | |
| self.view.layoutIfNeeded() | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment