2

I would like to wait for the run loop to run and the screen to be rendered 50 times before performing an operation.

Is it necessary to use CAMediaTiming and a counter for that? Is there a way to hook into the NSRunLoop directly? Can I achieve this using 50 nested DispatchQueue.async calls like so?

import Dispatch

func wait(ticks: UInt, queue: DispatchQueue = DispatchQueue.main, _ handler: @escaping () -> Void) {
    var ticks = ticks

    func predicate() {
        queue.async {
            ticks -= 1
            if ticks < 1 {
                handler()
                return
            }
            queue.async(execute: predicate)
        }
    }

    predicate()
}

EDIT: if anyone is wondering, the snippet does work and it performs very well when we're talking about apps run loop.

Isaaс Weisberg
  • 2,296
  • 2
  • 12
  • 28
  • Why 50 times? Keep in mind that some devices (iPad Pros with “ProMotion”) have a variable screen refresh rate. – rob mayoff May 27 '19 at 18:12

2 Answers2

2

You can use CADisplayLink, which is called every screen update.

let displayLink = CADisplayLink(target: self, selector: #selector(drawFunction(displayLink:)))
displayLink.add(to: .main, forMode: .commonModes)

In drawFunction (or predicate, etc.) we can subtract from ticks. When they reach 0, we've hit the 50th frame and invalidate displayLink.

var ticks = 50
@objc private func drawFunction(displayLink: CADisplayLink) {
    doSomething()
    ticks -= 1
    if ticks == 0 {
        displayLink.invalidate()
        displayLink = nil
        return
    }
}

CADisplayLink can also provide the amount of time between frames. A similar discussion can be found here. If you're concerned about absolute accuracy here, you could calculate the time between frames. From the docs:

The duration property provides the amount of time between frames at the maximumFramesPerSecond. To calculate the actual frame duration, use targetTimestamp - timestamp. You can use this value in your application to calculate the frame rate of the display, the approximate time that the next frame will be displayed, and to adjust the drawing behavior so that the next frame is prepared in time to be displayed.

jake
  • 1,226
  • 8
  • 14
  • Yeah, I'll mark as the right answer. Shame that it forces to rely on CoreAnimation and thus I won't be able to use it on Linux, but alright – Isaaс Weisberg May 28 '19 at 06:42
0

I think better solution to use CADisplayLink instead of Dispatch. Here is an example:

    class Waiter {
        let handler: () -> Void
        let ticksLimit: UInt
        var ticks: UInt = 0
        var displayLink: CADisplayLink?

        init(ticks: UInt, handler: @escaping () -> Void) {
            self.handler = handler
            self.ticksLimit = ticks
        }

        func wait() {
            createDisplayLink()
        }

        private func createDisplayLink() {
            displayLink = CADisplayLink(target: self,
                                      selector: #selector(step))
            displayLink?.add(to: .current,
                        forMode: RunLoop.Mode.default)
        }

        @objc private func step(displaylink: CADisplayLink) {
            print(ticks)
            if ticks >= ticksLimit {
                displayLink?.invalidate()
                handler()
            }
            ticks += 1
        }
    }

Here is an example of usage:

    let waiter = Waiter(ticks: 50) {
        print("Handled")
    }
    waiter.wait()
Anton Vlasov
  • 1,372
  • 1
  • 10
  • 18