Created
November 10, 2019 00:14
-
-
Save JoshuaSullivan/969934d8ae5193611ceb6559e2fff937 to your computer and use it in GitHub Desktop.
This gist demonstrates how we can wrap CADisplayLink to get frame refresh events via a published stream rather than the traditional `target:selector:` methodology.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import Combine | |
/// This class wraps a `CADisplayLink` and exposes its callback as a Combine Publisher. | |
/// | |
class CombineDisplayLink { | |
/// An object that includes timing information about the screen refresh. | |
/// | |
struct Tick { | |
static let zero = Tick(index: 0, duration: 0, timestamp: 0, targetTimestamp: 0, intervalSinceLastTick: 0) | |
/// Indicates how many ticks this `CombineDisplayLink` has sent. | |
let index: Int | |
/// The ideal time interval between screen refresh updates. | |
/// - Warning: This does not reflect the **actual** time interval since | |
/// the last tick. Check `intervalSinceLastTick` for that. | |
let duration: CFTimeInterval | |
/// The time value associated with the previous tick. | |
let timestamp: CFTimeInterval | |
/// The time value associated with the current tick. | |
let targetTimestamp: CFTimeInterval | |
/// Returns the time in seconds since the last tick was dispatched. | |
let intervalSinceLastTick: CFTimeInterval | |
} | |
/// The publisher of `Tick` events. | |
@Published var tick: Tick = .zero | |
/// Put a placeholder link in the value so it doesn't need to be optional. | |
private var link = CADisplayLink() | |
/// Track the number of ticks. | |
private var index: Int = 0 | |
/// Create a new `CombineDisplayLink`. New links will start in a paused state. | |
init() { | |
link = CADisplayLink(target: self, selector: #selector(handleTick(link:))) | |
link.add(to: RunLoop.main, forMode: .common) | |
link.isPaused = true | |
} | |
deinit { | |
// Clean up the display link. | |
link.invalidate() | |
} | |
/// Transform the display link callbacks into `Tick` objects and | |
/// send them out via the publisher. | |
/// | |
@objc private func handleTick(link: CADisplayLink) { | |
let interval = link.targetTimestamp - link.timestamp | |
index += 1 | |
self.tick = Tick(index: index, | |
duration: link.duration, | |
timestamp: link.timestamp, | |
targetTimestamp: link.targetTimestamp, | |
intervalSinceLastTick: interval) | |
} | |
/// The preferred frame rate for tick events. | |
/// | |
var preferredFramesPerSecond: Int { | |
get { link.preferredFramesPerSecond } | |
set { link.preferredFramesPerSecond = newValue } | |
} | |
/// A Boolean value that states whether the display link publishes | |
/// `Tick` objects on its publisher when a display refresh occurs. | |
/// | |
var isPaused: Bool { | |
get { link.isPaused } | |
set { link.isPaused = newValue } | |
} | |
/// Shuts down a display link. Once this method is called, | |
/// it will never produce another value. | |
func stop() { | |
link.invalidate() | |
} | |
} | |
let link = CombineDisplayLink() | |
let cancelable = link.$tick.sink { tick in | |
print("[\(tick.index)] Elapsed time since last tick: \(tick.intervalSinceLastTick)") | |
} | |
link.preferredFramesPerSecond = 30 | |
link.isPaused = false | |
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { | |
link.stop() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment