Skip to content

Instantly share code, notes, and snippets.

@SoundBlaster
Last active November 12, 2025 06:26
Show Gist options
  • Select an option

  • Save SoundBlaster/459ae7445135c7a7cf771779b200581f to your computer and use it in GitHub Desktop.

Select an option

Save SoundBlaster/459ae7445135c7a7cf771779b200581f to your computer and use it in GitHub Desktop.
Demo for problem of animation sublayer with explicit animations
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