Search code examples
iosswiftanimationtimerdraw

Why isn't the draw method of the custom CALayer called earlier?


Issue

I trigger random animations on a selection of UIViews using a Timer. All animations work as intended but one where the draw method of a CALayer is called after the exit of the timer selector.

Detailed Description

For the sake of clarity and simplification, let me schematise the actions performed.

Cycle

All the animations I have created so far work as intended: they are a combination of CABasicAnimations on existing subviews/sublayers or new ones added to the selected view hierarchy. They are coded as an UIView extension, so that they can be called on any views, irrespectively of the view or the view controller the view is in. Well, all work except one.

I have indeed created a custom CALayer class which consists in drawing patterns on a CALayer. In an extension of this custom class, there is a method to animate those patterns (see hereafter the code). So all in all, when I reach the step/method animate selected view and run this particular animation, here is what should happen:

  • a method named animatePattern is called
  • this method adds the custom CALayer class to the selected view and then calls the animation extension of this layer

The issue: if with all the other animations, all the drawings are performed prior to the exit of the animate selected view step/method, in that particular case, the custom CALayer class draw method is called after the exit of the performAnimation method, which in turn results in the crash of the animation.

I should add that I have tried the custom CALayer class animation in a separate and simplified playground and it works well (I add the custom layer to a UIView in the UIViewController's viewDidLoad method and then I call the layer animation in the UIViewController's viewDidAppear method.)

The code

  • the method called by animate selected view step/method:

    func animatePattern(for duration: TimeInterval = 1) {
    
    let patternLayer        = PatternLayer(effectType: .dark)
    patternLayer.frame      = self.bounds
    self.layer.addSublayer(patternLayer)
    patternLayer.play(for: duration)
    
    }
    

(note that this method is in a UIView extension, therefore self here represents the UIView on which the animation has been called)

  • the simplified custom CALayer Class:

    override var bounds: CGRect {
        didSet {
            self.setNeedsDisplay()
        }
    
    
    // Initializers
    override init() {
        super.init()
        self.setNeedsDisplay()
    }
    
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    init(effectType: EffectType){
        super.init()
        // sets various properties
        self.setNeedsDisplay()
    }
    
    
    // Drawing
    override func draw(in ctx: CGContext) {
        super.draw(in: ctx)
        guard self.frame        != CGRect.zero else { return }
    
        self.masksToBounds      = true
    
        // do the drawings
    }
    
  • the animation extension of the custom CALayer class:

     func play(for duration: Double, removeAfterCompletion: RemoveAfterCompletion = .no) {
    
        guard self.bounds != CGRect.zero else {
            print("Cannot animate nil layer")
            return }
    
        CATransaction.begin()
    
        CATransaction.setCompletionBlock {
    
            if removeAfterCompletion.removeStatus {
                if case RemoveAfterCompletion.yes = removeAfterCompletion { self.fadeOutDuration = 0.0 }
                self.fadeToDisappear(duration: &self.fadeOutDuration)
            }
        }
    
    
        // perform animations
    
        CATransaction.commit()
    }
    

Attempts so far

I have tried to force draw the layer by inserting setNeedsDisplay / setNeedsLayout at various places in the code but it does not work: debugging the custom CALayer class's draw method is constantly reached after the exit of the performAnimation method, whilst it should be called when the layer's frame is modified in the animatePattern method. I must miss something quite obvious but I am currently running in circles and I'd appreciate a fresh pair of eyes on it.

Thank you in advance for taking the time to consider this issue! Best,


Solution

  • As it is frequently the case, when you take the time to write down your issue, new ideas start to pop up. In fact, I remembered that self.setNeedsDisplay() informs the system that the layer needs to be (re)drawn but it does not (re)draw it at once: it will be done during the next refresh. It seems then that the refresh occurs after the end of the cycle for that specific animation. In order to overcome this is issue, I first added a call for the display method right after the patternLayer bounds are set, and it worked, but given @Matt comment, I changed the solution by calling displayIfNeeded method instead. It works as well.

    func animatePattern(for duration: TimeInterval = 1) {
    
    let patternLayer        = PatternLayer(effectType: .dark)
    patternLayer.frame      = self.bounds
    self.layer.addSublayer(patternLayer)
    
    // asks the system to draw the layer now
    patternLayer.displayIfNeeded()
    
    patternLayer.play(for: duration)
    
    }
    

    Again, should anyone come up with another more elegant solution/explanation, please do not refrain to share!