It is my understanding that CALayer.setNeedsDisplay registers the CALayer for redisplaying its content, which eventually leads to a call to CALayer.display. Multiple calls to CALayer.setNeedsDisplay in a short amount of time (lower than display refresh cycle) should "accumulate" to approximately trigger one single call of CALayer.display. The following code in fact leads to only one call of CALayer.display (full code at the end of post):
Unfortunately calling CALayer.setNeedsDisplay multiple times in this short amount of time (lower than display refresh cycle) from different threads (but still making sure it is called on the main thread since CALayer.setNeedsDisplay needs to be called from the main thread) seems to trigger CALayer.display multiple times instead of only one single time. Here is a small code excerpt how this would look like (full code at the end of post):
In my tests the code above lead to the unexpected described behavior. Tested in XCode 12.0.1 using XCode Playground. I also tested a similar code in an app running on my IPad Pro (first generation) showing the same behavior.
My questions are:
Code Block for _ in 0..<200 { let timeInterval: TimeInterval = Double(arc4random_uniform(1000)) / 40000.0 // random value between 0 and 25 ms Thread.sleep(forTimeInterval: timeInterval) testLayer.setNeedsDisplay() }
Unfortunately calling CALayer.setNeedsDisplay multiple times in this short amount of time (lower than display refresh cycle) from different threads (but still making sure it is called on the main thread since CALayer.setNeedsDisplay needs to be called from the main thread) seems to trigger CALayer.display multiple times instead of only one single time. Here is a small code excerpt how this would look like (full code at the end of post):
Code Block for _ in 0..<200 { DispatchQueue.global(qos: .userInteractive).async { let timeInterval: TimeInterval = Double(arc4random_uniform(1000)) / 40000.0 // random value between 0 and 25 ms Thread.sleep(forTimeInterval: timeInterval) DispatchQueue.main.async { testLayer.setNeedsDisplay() } } }
In my tests the code above lead to the unexpected described behavior. Tested in XCode 12.0.1 using XCode Playground. I also tested a similar code in an app running on my IPad Pro (first generation) showing the same behavior.
My questions are:
How can I make sure CALayer.display is called only once in the second case instead of multiple times?
Why does this happen?
Code Block import Foundation import UIKit import PlaygroundSupport /// class for conveniently measuring timespans class TimeTicToc { static var lastTime: TimeInterval? @discardableResult static func timeSinceLastCall(_ functionName: String? = nil) -> TimeInterval? { let currentTime: TimeInterval = Date().timeIntervalSince1970 var timeInterval: TimeInterval? if let lastCallTime = TimeTicToc.lastTime { let timeDiff = (currentTime - lastCallTime) * 1000.0 if let functionName = functionName { print("time since last call of \(functionName) = \(timeDiff) (ms), which corresponds to \(1000.0 / timeDiff) Hz") } else { print("time since last call = \(timeDiff), which corresponds to \(1000.0 / timeDiff) Hz") } timeInterval = timeDiff } TimeTicToc.lastTime = currentTime return timeInterval } } /// class derived from CALayer to expose display function class TestLayer: CALayer { override func display() { super.display() TimeTicToc.timeSinceLastCall("display") } } // setup liveView let liveView = UIView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 200.0, height: 200.0))) liveView.backgroundColor = UIColor.red // setup testLayer as sublayer of liveView let testLayer = TestLayer() testLayer.frame = CGRect(origin: CGPoint.zero, size: 0.5 * liveView.frame.size) testLayer.backgroundColor = UIColor.green.cgColor liveView.layer.addSublayer(testLayer) PlaygroundPage.current.liveView = liveView // execute setNeedsDisplay, enqueued from multiple threads with different very short delays (smaller than display refresh cicle) for _ in 0..<200 { DispatchQueue.global(qos: .userInteractive).async { let timeInterval: TimeInterval = Double(arc4random_uniform(1000)) / 40000.0 // random value between 0 and 25 ms Thread.sleep(forTimeInterval: timeInterval) DispatchQueue.main.async { testLayer.setNeedsDisplay() } } }