Files
rive-ios/Source/RiveView.swift
dskuza 08532a7036 Add visionOS and tvOS support to Apple runtime
This pull request adds support for both visionOS and tvOS to the Apple (colloquially referred to as iOS) runtime.

It should _not_ be a breaking change, since the only major API change is an internal one (see `RenderContext` below). I believe we should be able to make this a minor release. Developers who have subclassed `RiveView` or `RiveRendererView` should not see any changes, unless they were explicitly expecting this view to be `MTKView`, which is fully unavailable on visionOS (hence our recreation - see `RiveMTKView` below.

## Premake Scripts

The premake scripts were updated to add a few new variants for iOS:
- xros (visionOS devices; named after the internal sdk)
- xrsimulator (visionOS simulator; named after the internal sdk)
- appletvos (tvOS devices; named after the internal sdk)
- appletvsimulator (tvOS simulators; named after the internal sdk)

The majority of the work here is copy/pasting existing code, and just adding additional filters when these new options are used, primarily used to target the new SDKs / minimums for those SDKs.

## Shaders

Shaders are pre-compiled for visionOS and tvOS separately, and the correct shaders are then used later-on at compile time.

## Build scripts

Build scripts were updated to support building the new libraries, targeting the new devices, using the new options above. Additionally, they have to point to new output files.

The `build_framework` script has been updated to build the new libraries to add to the final output `xcframework`.

## Project

Example targets for both visionOS and tvOS, since these truly are the "native" apps, rather than just iPad-on-your-device. These use a new `streaming` riv by the creative team.

The tvOS example app adds additional support for remote control, since that behavior can be utilized in multiple ways during development; that is, we don't add any "default" behavior for remote controls. The visionOS app, on the other hand, works out-of-the-box with no additional changes.

## RenderContext

`RenderContext` is an internal type; it's forward-declared, so it's unusable outside of the scope of internal development. There have been some "breaking" changes here - the API has been updated to, instead of passing in `MTKView` around, using `id<RiveMetalDrawableView>`. This had to be changed, regardless, since visionOS does not have `MTKView`. The choice to use a protocol was because it forces a little more explicit initialization across platforms, rather than having a parent class that acts as an abstract class, but isn't abstract because it still needs some default values, but those values are different based on device and API availability, etc. We could've passed around `RiveMTKView` as the type, but with a protocol, there's a possibility of being able to use a type that isn't exactly a view, but might want to still act against the drawing process. Personal choice, really.

## RiveRendererView

`RiveRendererView` is now a subclass of `RiveMTKView`. `RiveMTKView`'s superclass depends on the device:
- On visionOS, this is a `UIView` with an underlying `CAMetalLayer`
- On all other platforms, `MTKView`

This new class conforms to `RiveMetalDrawableView`, which allows it to be passed to `RenderContext` types.

### RiveMTKView (visionOS)

`RiveMTKView` on visionOS is a subclass of `UIView` that is backed by a `CAMetalLayer`, providing the necessary properties of `RiveMetalDrawableView` (compile-time safety here, baby). This is quite a simple recreation of the default `MTKView`, since that type is not available on visionOS (thanks, Apple).

## Other things

Additional compile-time checks for platform OS have been added to make sure each new platform compiles with the correct APIs that can be shared, or otherwise newly implemented.

Diffs=
6f70a0e803 Add visionOS and tvOS support to Apple runtime (#8107)

Co-authored-by: David Skuza <david@rive.app>
2024-12-11 23:37:59 +00:00

778 lines
29 KiB
Swift

//
// RiveView.swift
// RiveRuntime
//
// Created by Zachary Duncan on 3/23/22.
// Copyright © 2022 Rive. All rights reserved.
//
import Foundation
open class RiveView: RiveRendererView {
struct Constants {
static let layoutScaleFactorAutomatic: Double = -1
}
// MARK: Configuration
internal weak var riveModel: RiveModel?
internal var fit: RiveFit = .contain { didSet { needsDisplay() } }
internal var alignment: RiveAlignment = .center { didSet { needsDisplay() } }
/// The scale factor to apply when using the `layout` fit. By default, this value is -1, where Rive will determine
/// the correct scale for your device.To override this default behavior, set this value to a value greater than 0. This value should
/// only be set at the view model level and passed into this view.
/// - Note: If the scale factor <= 0, nothing will be drawn.
internal var layoutScaleFactor: Double = RiveView.Constants.layoutScaleFactorAutomatic { didSet { needsDisplay() } }
/// The internally calculated layout scale to use if a scale is not set by the developer (i.e layoutScaleFactor == -1)
/// Defaults to the "legacy" methods, which will be overridden
/// by window handlers in this view when the window changes.
private lazy var _layoutScaleFactor: Double = {
#if os(iOS) || os(visionOS) || os(tvOS)
return self.traitCollection.displayScale
#else
guard let scale = NSScreen.main?.backingScaleFactor else { return 1 }
return scale
#endif
}() {
didSet { needsDisplay() }
}
/// Sets whether or not the Rive view should forward Rive listener touch / click events to any next responders.
/// When true, touch / click events will be forwarded to any next responder(s).
/// When false, only the Rive view will handle touch / click events, and will not forward
/// to any next responder(s). Defaults to `false`, as to preserve pre-existing runtime functionality.
/// - Note: On iOS, this is handled separately from `isExclusiveTouch`.
internal var forwardsListenerEvents: Bool = false
// MARK: Render Loop
internal private(set) var isPlaying: Bool = false
private var lastTime: CFTimeInterval = 0
private var displayLinkProxy: DisplayLinkProxy?
private var eventQueue = EventQueue()
// MARK: Delegates
@objc public weak var playerDelegate: RivePlayerDelegate?
public weak var stateMachineDelegate: RiveStateMachineDelegate?
// MARK: Debug
private var fpsCounter: FPSCounterView? = nil
/// Shows or hides the FPS counter on this RiveView
public var showFPS: Bool = RiveView.showFPSCounters { didSet { setFPSCounterVisibility() } }
/// Shows or hides the FPS counters on all RiveViews
public static var showFPSCounters = false
open override var bounds: CGRect {
didSet {
redrawIfNecessary()
}
}
open override var frame: CGRect {
didSet {
redrawIfNecessary()
}
}
private var orientationObserver: (any NSObjectProtocol)?
private var screenObserver: (any NSObjectProtocol)?
/// Minimalist constructor, call `.configure` to customize the `RiveView` later.
public init() {
super.init(frame: .zero)
commonInit()
}
public convenience init(model: RiveModel, autoPlay: Bool = true) {
self.init()
commonInit()
try! setModel(model, autoPlay: autoPlay)
}
#if os(visionOS)
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
#else
required public init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
#endif
private func commonInit() {
#if os(iOS) || os(visionOS) || os(tvOS)
if #available(iOS 17, tvOS 17, visionOS 1, *) {
registerForTraitChanges([UITraitHorizontalSizeClass.self, UITraitVerticalSizeClass.self]) { [weak self] (_: UITraitEnvironment, traitCollection: UITraitCollection) in
guard let self else { return }
self.redrawIfNecessary()
}
}
if #available(iOS 17, tvOS 17, visionOS 1, *) {
registerForTraitChanges([UITraitDisplayScale.self]) { [weak self] (_: UITraitEnvironment, traitCollection: UITraitCollection) in
guard let self else { return }
self._layoutScaleFactor = self.traitCollection.displayScale
}
}
#endif
#if os(iOS)
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
orientationObserver = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [weak self] _ in
guard let self else { return }
self.redrawIfNecessary()
}
#endif
#if os(macOS)
screenObserver = NotificationCenter.default.addObserver(
forName: NSWindow.didChangeScreenNotification,
object: nil,
queue: nil) { [weak self] _ in
guard let self, let scale = window?.screen?.backingScaleFactor else { return }
_layoutScaleFactor = scale
}
#endif
}
deinit {
stopTimer()
#if os(iOS)
UIDevice.current.endGeneratingDeviceOrientationNotifications()
if let observer = orientationObserver {
NotificationCenter.default.removeObserver(observer as Any)
}
#endif
#if os(macOS)
if let observer = screenObserver {
NotificationCenter.default.removeObserver(observer as Any)
}
#endif
orientationObserver = nil
screenObserver = nil
}
private func needsDisplay() {
#if os(iOS) || os(visionOS) || os(tvOS)
setNeedsDisplay()
#else
needsDisplay=true
#endif
}
#if os(iOS) || os(tvOS)
open override func didMoveToWindow() {
super.didMoveToWindow()
guard let scale = window?.windowScene?.screen.scale else { return }
_layoutScaleFactor = scale
}
#elseif os(visionOS)
open override func didMoveToWindow() {
super.didMoveToWindow()
let scale = traitCollection.displayScale
_layoutScaleFactor = scale
}
#else
open override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
guard let scale = window?.screen?.backingScaleFactor else { return }
_layoutScaleFactor = scale
}
#endif
/// This resets the view with the new model. Useful when the `RiveView` was initialized without one.
open func setModel(_ model: RiveModel, autoPlay: Bool = true) throws {
stopTimer()
isPlaying = false
riveModel = model
#if os(iOS) || os(visionOS) || os(tvOS)
isOpaque = false
#else
layer?.isOpaque=false
#endif
if autoPlay {
play()
} else {
advance(delta: 0)
}
setFPSCounterVisibility()
}
#if os(iOS) || os(visionOS) || os(tvOS)
/// Hints to underlying CADisplayLink the preferred FPS to run at
/// - Parameters:
/// - preferredFramesPerSecond: Integer number of seconds to set preferred FPS at
open func setPreferredFramesPerSecond(preferredFramesPerSecond: Int) {
if let displayLink = displayLinkProxy?.displayLink {
displayLink.preferredFramesPerSecond = preferredFramesPerSecond
}
}
/// Hints to underlying CADisplayLink the preferred frame rate range
/// - Parameters:
/// - preferredFrameRateRange: Frame rate range to set
@available(iOS 15.0, *)
open func setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange) {
if let displayLink = displayLinkProxy?.displayLink {
displayLink.preferredFrameRateRange = preferredFrameRateRange
}
}
#endif
// MARK: - Controls
/// Starts the render loop
internal func play() {
RiveLogger.log(view: self, event: .play)
eventQueue.add {
self.playerDelegate?.player(playedWithModel: self.riveModel)
}
isPlaying = true
startTimer()
}
/// Asks the render loop to stop on the next cycle
internal func pause() {
RiveLogger.log(view: self, event: .pause)
if isPlaying {
eventQueue.add {
self.playerDelegate?.player(pausedWithModel: self.riveModel)
}
isPlaying = false
}
}
/// Asks the render loop to stop on the next cycle
internal func stop() {
RiveLogger.log(view: self, event: .stop)
playerDelegate?.player(stoppedWithModel: riveModel)
isPlaying = false
reset()
}
internal func reset() {
RiveLogger.log(view: self, event: .reset)
lastTime = 0
if !isPlaying {
advance(delta: 0)
}
}
// MARK: - Render Loop
private func startTimer() {
if displayLinkProxy == nil {
displayLinkProxy = DisplayLinkProxy(
handle: { [weak self] in
self?.tick()
},
to: .main,
forMode: .common
)
}
#if os(iOS) || os(visionOS)
if displayLinkProxy?.displayLink?.isPaused == true {
displayLinkProxy?.displayLink?.isPaused = false
}
#endif
}
private func stopTimer() {
displayLinkProxy?.invalidate()
displayLinkProxy = nil
lastTime = 0
fpsCounter?.stopped()
}
private func timestamp() -> CFTimeInterval {
#if os(iOS) || os(visionOS) || os(tvOS)
return displayLinkProxy?.displayLink?.targetTimestamp ?? Date().timeIntervalSince1970
#else
return Date().timeIntervalSince1970
#endif
}
/// Start a redraw:
/// - determine the elapsed time
/// - advance the artbaord, which will invalidate the display.
/// - if the artboard has come to a stop, stop.
@objc fileprivate func tick() {
guard displayLinkProxy?.displayLink != nil else {
stopTimer()
return
}
let timestamp = timestamp()
// last time needs to be set on the first tick
if lastTime == 0 {
lastTime = timestamp
}
// Calculate the time elapsed between ticks
let elapsedTime = timestamp - lastTime
#if os(iOS) || os(visionOS) || os(tvOS)
fpsCounter?.didDrawFrame(timestamp: timestamp)
#else
fpsCounter?.elapsed(time: elapsedTime)
#endif
lastTime = timestamp
advance(delta: elapsedTime)
if !isPlaying {
stopTimer()
}
}
/// Advances the Artboard and either a StateMachine or an Animation.
/// Also fires any remaining events in the queue.
///
/// - Parameter delta: elapsed seconds since the last advance
@objc open func advance(delta: Double) {
let wasPlaying = isPlaying
eventQueue.fireAll()
if let stateMachine = riveModel?.stateMachine {
let firedEventCount = stateMachine.reportedEventCount()
if (firedEventCount > 0) {
for i in 0..<firedEventCount {
let event = stateMachine.reportedEvent(at: i)
RiveLogger.log(view: self, event: .eventReceived(event.name()))
stateMachineDelegate?.onRiveEventReceived?(onRiveEvent: event)
}
}
isPlaying = stateMachine.advance(by: delta) && wasPlaying
if let delegate = stateMachineDelegate {
stateMachine.stateChanges().forEach { delegate.stateMachine?(stateMachine, didChangeState: $0) }
}
} else if let animation = riveModel?.animation {
isPlaying = animation.advance(by: delta) && wasPlaying
if isPlaying {
if animation.didLoop() {
playerDelegate?.player(loopedWithModel: riveModel, type: Int(animation.loop()))
}
}
}
if !isPlaying {
stopTimer()
// This will be true when coming to a hault automatically
if wasPlaying {
RiveLogger.log(view: self, event: .pause)
playerDelegate?.player(pausedWithModel: riveModel)
}
}
RiveLogger.log(view: self, event: .advance(delta))
playerDelegate?.player(didAdvanceby: delta, riveModel: riveModel)
// Trigger a redraw
needsDisplay()
}
/// This is called in the middle of drawRect. Override this method to implement
/// custom draw logic
override open func drawRive(_ rect: CGRect, size: CGSize) {
// This prevents breaking when loading RiveFile async
guard let artboard = riveModel?.artboard else { return }
let scale = layoutScaleFactor == RiveView.Constants.layoutScaleFactorAutomatic ? _layoutScaleFactor : layoutScaleFactor
RiveLogger.log(view: self, event: .drawing(size))
let newFrame = CGRect(origin: rect.origin, size: size)
if (fit == RiveFit.layout) {
if scale <= 0 {
RiveLogger.log(view: self, event: .error("Cannot draw with a scale factor of \(scale)"))
return
}
artboard.setWidth(Double(newFrame.width) / scale);
artboard.setHeight(Double(newFrame.height) / scale);
} else {
artboard.resetArtboardSize();
}
align(with: newFrame, contentRect: artboard.bounds(), alignment: alignment, fit: fit, scaleFactor: scale)
draw(with: artboard)
}
// MARK: - UITraitCollection
#if os(iOS)
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #unavailable(iOS 17) {
if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass
|| traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass {
redrawIfNecessary()
}
if traitCollection.displayScale != previousTraitCollection?.displayScale {
_layoutScaleFactor = traitCollection.displayScale
}
}
}
#endif
// MARK: - UIResponder
#if os(iOS) || os(visionOS) || os(tvOS)
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
handleTouch(touch, delegate: stateMachineDelegate?.touchBegan) { stateMachine, location in
let result = stateMachine.touchBegan(atLocation: location)
RiveLogger.log(view: self, event: .touchBegan(location))
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .began)
}
}
if forwardsListenerEvents == true {
super.touchesBegan(touches, with: event)
}
}
open override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
handleTouch(touch, delegate: stateMachineDelegate?.touchMoved) { stateMachine, location in
RiveLogger.log(view: self, event: .touchMoved(location))
let result = stateMachine.touchMoved(atLocation: location)
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .moved)
}
}
if forwardsListenerEvents == true {
super.touchesMoved(touches, with: event)
}
}
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
handleTouch(touch, delegate: stateMachineDelegate?.touchEnded) { stateMachine, location in
RiveLogger.log(view: self, event: .touchEnded(location))
let result = stateMachine.touchEnded(atLocation: location)
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .ended)
}
}
if forwardsListenerEvents == true {
super.touchesEnded(touches, with: event)
}
}
open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
handleTouch(touch, delegate: stateMachineDelegate?.touchCancelled) { stateMachine, location in
RiveLogger.log(view: self, event: .touchCancelled(location))
let result = stateMachine.touchCancelled(atLocation: location)
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .cancelled)
}
}
if forwardsListenerEvents == true {
super.touchesCancelled(touches, with: event)
}
}
/// Sends incoming touch event to all playing `RiveStateMachineInstance`'s
/// - Parameters:
/// - touch: The `CGPoint` where the touch occurred in `RiveView` coordinate space
/// - delegateAction: The delegate callback that should be triggered by this touch event
/// - stateMachineAction: Param1: A playing `RiveStateMachineInstance`, Param2: `CGPoint`
/// location where touch occurred in `artboard` coordinate space
private func handleTouch(
_ touch: UITouch,
delegate delegateAction: ((RiveArtboard?, CGPoint)->Void)?,
stateMachineAction: (RiveStateMachineInstance, CGPoint)->Void
) {
guard let artboard = riveModel?.artboard else { return }
guard let stateMachine = riveModel?.stateMachine else { return }
let location = touch.location(in: self)
let artboardLocation = artboardLocation(
fromTouchLocation: location,
inArtboard: artboard.bounds(),
fit: fit,
alignment: alignment
)
stateMachineAction(stateMachine, artboardLocation)
play()
// We send back the touch location in UIView coordinates because
// users cannot query or manually control the coordinates of elements
// in the Artboard. So that information would be of no use.
delegateAction?(artboard, location)
}
#else
open override func mouseDown(with event: NSEvent) {
handleTouch(event, delegate: stateMachineDelegate?.touchBegan) { stateMachine, location in
RiveLogger.log(view: self, event: .touchBegan(location))
let result = stateMachine.touchBegan(atLocation: location)
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .began)
}
}
if forwardsListenerEvents == true {
super.mouseDown(with: event)
}
}
open override func mouseMoved(with event: NSEvent) {
handleTouch(event, delegate: stateMachineDelegate?.touchMoved) { stateMachine, location in
RiveLogger.log(view: self, event: .touchMoved(location))
let result = stateMachine.touchMoved(atLocation: location)
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .moved)
}
}
if forwardsListenerEvents == true {
super.mouseMoved(with: event)
}
}
open override func mouseDragged(with event: NSEvent) {
handleTouch(event, delegate: stateMachineDelegate?.touchMoved) { stateMachine, location in
RiveLogger.log(view: self, event: .touchMoved(location))
let result = stateMachine.touchMoved(atLocation: location)
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .moved)
}
}
if forwardsListenerEvents == true {
super.mouseDragged(with: event)
}
}
open override func mouseUp(with event: NSEvent) {
handleTouch(event, delegate: stateMachineDelegate?.touchEnded) { stateMachine, location in
RiveLogger.log(view: self, event: .touchEnded(location))
let result = stateMachine.touchEnded(atLocation: location)
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .ended)
}
}
if forwardsListenerEvents == true {
super.mouseUp(with: event)
}
}
open override func mouseExited(with event: NSEvent) {
handleTouch(event, delegate: stateMachineDelegate?.touchCancelled) { stateMachine, location in
RiveLogger.log(view: self, event: .touchCancelled(location))
let result = stateMachine.touchCancelled(atLocation: location)
if let stateMachine = riveModel?.stateMachine {
stateMachineDelegate?.stateMachine?(stateMachine, didReceiveHitResult: result, from: .cancelled)
}
}
if forwardsListenerEvents == true {
super.mouseExited(with: event)
}
}
open override func updateTrackingAreas() {
addTrackingArea(
NSTrackingArea(
rect: self.bounds,
options: [.mouseEnteredAndExited, .mouseMoved, .activeInKeyWindow],
owner: self,
userInfo: nil
)
)
}
/// Sends incoming touch event to all playing `RiveStateMachineInstance`'s
/// - Parameters:
/// - touch: The `CGPoint` where the touch occurred in `RiveView` coordinate space
/// - delegateAction: The delegate callback that should be triggered by this touch event
/// - stateMachineAction: Param1: A playing `RiveStateMachineInstance`, Param2: `CGPoint`
/// location where touch occurred in `artboard` coordinate space
private func handleTouch(
_ event: NSEvent,
delegate delegateAction: ((RiveArtboard?, CGPoint)->Void)?,
stateMachineAction: (RiveStateMachineInstance, CGPoint)->Void
) {
guard let artboard = riveModel?.artboard else { return }
guard let stateMachine = riveModel?.stateMachine else { return }
let location = convert(event.locationInWindow, from: nil)
// This is conforms the point to UIView coordinates which the
// RiveRendererView expects in its artboardLocation method
let locationFlippedY = CGPoint(x: location.x, y: frame.height - location.y)
let artboardLocation = artboardLocation(
fromTouchLocation: locationFlippedY,
inArtboard: artboard.bounds(),
fit: fit,
alignment: alignment
)
stateMachineAction(stateMachine, artboardLocation)
play()
// We send back the touch location in NSView coordinates because
// users cannot query or manually control the coordinates of elements
// in the Artboard. So that information would be of no use.
delegateAction?(artboard, location)
}
#endif
// MARK: - Debug
private func setFPSCounterVisibility() {
// Create a new counter view
if showFPS && fpsCounter == nil {
fpsCounter = FPSCounterView()
addSubview(fpsCounter!)
}
if !showFPS {
fpsCounter?.removeFromSuperview()
fpsCounter = nil
}
}
private func redrawIfNecessary() {
if isPlaying == false {
needsDisplay()
}
}
}
/// An enum of possible touch or mouse events when interacting with an animation.
@objc public enum RiveTouchEvent: Int {
/// The touch event that occurs when a touch or mouse button click occurs.
case began
/// The touch event that occurs when a touch or mouse is dragged.
case moved
/// The touch event that occurs when a touch or mouse button is lifted.
case ended
/// The touch event that occurs when a touch or mouse click is cancelled.
case cancelled
}
@objc public protocol RiveStateMachineDelegate: AnyObject {
@objc optional func touchBegan(onArtboard artboard: RiveArtboard?, atLocation location: CGPoint)
@objc optional func touchMoved(onArtboard artboard: RiveArtboard?, atLocation location: CGPoint)
@objc optional func touchEnded(onArtboard artboard: RiveArtboard?, atLocation location: CGPoint)
@objc optional func touchCancelled(onArtboard artboard: RiveArtboard?, atLocation location: CGPoint)
@objc optional func stateMachine(_ stateMachine: RiveStateMachineInstance, receivedInput input: StateMachineInput)
@objc optional func stateMachine(_ stateMachine: RiveStateMachineInstance, didChangeState stateName: String)
@objc optional func stateMachine(_ stateMachine: RiveStateMachineInstance, didReceiveHitResult hitResult: RiveHitResult, from event: RiveTouchEvent)
@objc optional func onRiveEventReceived(onRiveEvent riveEvent: RiveEvent)
}
@objc public protocol RivePlayerDelegate: AnyObject {
func player(playedWithModel riveModel: RiveModel?)
func player(pausedWithModel riveModel: RiveModel?)
func player(loopedWithModel riveModel: RiveModel?, type: Int)
func player(stoppedWithModel riveModel: RiveModel?)
func player(didAdvanceby seconds: Double, riveModel: RiveModel?)
}
#if os(iOS) || os(visionOS) || os(tvOS)
fileprivate class DisplayLinkProxy {
var displayLink: CADisplayLink?
var handle: (() -> Void)?
private var runloop: RunLoop
private var mode: RunLoop.Mode
init(handle: (() -> Void)?, to runloop: RunLoop, forMode mode: RunLoop.Mode) {
self.handle = handle
self.runloop = runloop
self.mode = mode
displayLink = CADisplayLink(target: self, selector: #selector(updateHandle))
displayLink?.add(to: runloop, forMode: mode)
}
@objc func updateHandle() {
handle?()
}
func invalidate() {
displayLink?.remove(from: runloop, forMode: mode)
displayLink?.invalidate()
displayLink = nil
}
}
#else
fileprivate class DisplayLinkProxy {
var displayLink: CVDisplayLink?
init?(handle: (() -> Void)!, to runloop: RunLoop, forMode mode: RunLoop.Mode) {
//ignore runloop/formode
let error = CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
if error != kCVReturnSuccess { return nil }
CVDisplayLinkSetOutputHandler(displayLink!) { dl, ts, tsDisplay, _, _ in
DispatchQueue.main.async {
handle()
}
return kCVReturnSuccess
}
CVDisplayLinkStart(displayLink!)
}
func invalidate() {
if let displayLink = displayLink {
CVDisplayLinkStop(displayLink)
self.displayLink = nil
}
}
}
#endif
/// Tracks a queue of events that haven't been fired yet. We do this so that we're not calling delegates and modifying state
/// while a view is updating (e.g. being initialized, as we autoplay and fire play events during the view's init otherwise
class EventQueue {
var events: [() -> Void] = []
func add(_ event: @escaping () -> Void) {
events.append(event)
}
func fireAll() {
events.forEach { $0() }
events.removeAll()
}
}