Search code examples
iosswiftanimationdraw

Constantly animated view in Swift


I'm creating a subclass of AVPlayerController, that observes the player and handles player's states. If AVPlayerItemStatus is .failed, I add a custom UIView over the player's frame. At present, the important parts of my custom view's code looks like this:

class NoiseView: UIView {

    ...

    var timer: Timer?
    func animate() {
        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
    }

    func stopAnimating() {
        timer?.invalidate()
    }

    @objc func timerAction() {
        self.setNeedsDisplay()
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        let context = UIGraphicsGetCurrentContext()!
        let h = rect.height
        let w = rect.width

        var val: CGFloat!
        var color: UIColor!

        for i in 0...Int(w-1) {
            for j in 0...Int(h-1) {
                val = .random
                color = UIColor(red: val, green: val, blue: val, alpha: 1.0)

                context.setFillColor(color.cgColor)
                context.fill(CGRect(x: i, y: j, width: 1, height: 1))
            }
        }

        context.flush()
    } 
}

I'm calling the method animate() from other ViewController that keeps the NoiseView object.

The thing is, it's working as it's supposed to work (view is animating and creating a white noise) but the app starts to work slowly. What should be a proper approach to redraw the view without causing such a performance drop?

By the way, the drop happened with 0.1s interval (10 FPS). What I want to accomplish is having a constant white noise with at least 30 FPS to look like a legit tv noise.


Solution

  • There are a number of strategies you can try to optimize the drawing code, but to me the most obvious one is that you should pre-calculate the CGRects you need outside the drawing code.

    The loops you're running require w^h iterations for each frame of animation to calculate all the CGRects you need. That's a lot, and totally unnecessary, because the CGRects will always be the same so long as the width and height are unchanged. Instead, you should do something like this:

    var cachedRects = [CGRect]()
    override var frame: CGRect {
        didSet {
            calculateRects()
        }
    }
    
    func calculateRects() {
        cachedRects = []
        let w = frame.width
        let h = frame.height
        for i in 0...Int(w-1) {
            for j in 0...Int(h-1) {
                let rect = CGRect(x: i, y: j, width: 1, height: 1)
                cachedRects += [rect]
            }
        }
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        let context = UIGraphicsGetCurrentContext()!
        var val: CGFloat!
        var color: UIColor!
        for rect in cachedRects {
            val = .random
            color = UIColor(red: val, green: val, blue: val, alpha: 1.0)
            context.setFillColor(color.cgColor)
            context.fill(rect)
        }
        context.flush()
    }
    

    By pre-caching the rects, you only have to do (w * h) number of iterations, which is a vast improvement.

    If that isn't enough to improve performance, you can further optimize using similar pre-caching strategies, like instead of randomizing the each pixel, pre-calculate tiles of random colors outside the draw code, and then randomly assemble them in drawRect(). If the randomizer is what's the performance problem, this would cut down on the amount of work it would have to.

    The key strategy is to try and minimize the amount of work your drawRect() method has to do, because it run on each frame of animation.

    Good luck!