mirror of
https://github.com/rive-app/rive-ios.git
synced 2026-01-18 17:11:28 +01:00
725 lines
28 KiB
Swift
725 lines
28 KiB
Swift
//
|
|
// RiveViewModel.swift
|
|
// RiveRuntime
|
|
//
|
|
// Created by Zachary Duncan on 3/24/22.
|
|
// Copyright © 2022 Rive. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Combine
|
|
|
|
/// An object used for controlling a RiveView. For most common Rive files you should only need
|
|
/// to interact with a `RiveViewModel` object.
|
|
///
|
|
/// - Usage:
|
|
/// - You should initialize with either an Animation name or a StateMachine name, but not both.
|
|
/// Only one will be used and if both are given the StateMachine will be used.
|
|
/// - Default StateMachine or Animation from the file can be used by leaving their parameters nil
|
|
/// - Examples:
|
|
///
|
|
/// ```
|
|
/// // SwiftUI Example
|
|
/// struct Animation: View {
|
|
/// var body: some View {
|
|
/// RiveViewModel(fileName: "cool_rive_file").view()
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// ```
|
|
/// // UIKit Example
|
|
/// class AnimationViewController: UIViewController {
|
|
/// @IBOutlet weak var riveView: RiveView!
|
|
/// var viewModel = RiveViewModel(fileName: "cool_rive_file")
|
|
///
|
|
/// override func viewDidLoad() {
|
|
/// viewModel.setView(riveView)
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
@objc open class RiveViewModel: NSObject, ObservableObject, RiveFileDelegate, RiveStateMachineDelegate, RivePlayerDelegate{
|
|
/// The default layout scale factor that allows for the scale factor to be determined by Rive.
|
|
@objc public static let layoutScaleFactorAutomatic: Double = RiveView.Constants.layoutScaleFactorAutomatic
|
|
|
|
// TODO: could be a weak ref, need to look at this in more detail.
|
|
open private(set) var riveView: RiveView?
|
|
private var defaultModel: RiveModelBuffer!
|
|
|
|
@objc public init(
|
|
_ model: RiveModel,
|
|
stateMachineName: String?,
|
|
fit: RiveFit = .contain,
|
|
alignment: RiveAlignment = .center,
|
|
autoPlay: Bool = true,
|
|
artboardName: String? = nil
|
|
) {
|
|
self.fit = fit
|
|
self.alignment = alignment
|
|
self.autoPlay = autoPlay
|
|
super.init()
|
|
riveModel = model
|
|
sharedInit(artboardName: artboardName, stateMachineName: stateMachineName, animationName: nil)
|
|
}
|
|
|
|
@objc public init(
|
|
_ model: RiveModel,
|
|
animationName: String? = nil,
|
|
fit: RiveFit = .contain,
|
|
alignment: RiveAlignment = .center,
|
|
autoPlay: Bool = true,
|
|
artboardName: String? = nil
|
|
) {
|
|
self.fit = fit
|
|
self.alignment = alignment
|
|
self.autoPlay = autoPlay
|
|
super.init()
|
|
riveModel = model
|
|
sharedInit(artboardName: artboardName, stateMachineName: nil, animationName: animationName)
|
|
}
|
|
|
|
@objc public init(
|
|
fileName: String,
|
|
extension: String = ".riv",
|
|
in bundle: Bundle = .main,
|
|
stateMachineName: String?,
|
|
fit: RiveFit = .contain,
|
|
alignment: RiveAlignment = .center,
|
|
autoPlay: Bool = true,
|
|
artboardName: String? = nil,
|
|
loadCdn: Bool = true,
|
|
customLoader: LoadAsset? = nil
|
|
) {
|
|
self.fit = fit
|
|
self.alignment = alignment
|
|
self.autoPlay = autoPlay
|
|
super.init()
|
|
riveModel = try! RiveModel(fileName: fileName, extension: `extension`, in: bundle, loadCdn: loadCdn, customLoader:customLoader)
|
|
sharedInit(artboardName: artboardName, stateMachineName: stateMachineName, animationName: nil)
|
|
}
|
|
|
|
public init(
|
|
fileName: String,
|
|
extension: String = ".riv",
|
|
in bundle: Bundle = .main,
|
|
animationName: String? = nil,
|
|
fit: RiveFit = .contain,
|
|
alignment: RiveAlignment = .center,
|
|
autoPlay: Bool = true,
|
|
artboardName: String? = nil,
|
|
preferredFramesPerSecond: Int? = nil,
|
|
loadCdn: Bool = true,
|
|
customLoader: LoadAsset? = nil
|
|
) {
|
|
self.fit = fit
|
|
self.alignment = alignment
|
|
self.autoPlay = autoPlay
|
|
super.init()
|
|
riveModel = try! RiveModel(fileName: fileName, extension: `extension`, in: bundle, loadCdn: loadCdn, customLoader:customLoader)
|
|
sharedInit(artboardName: artboardName, stateMachineName: nil, animationName: animationName)
|
|
}
|
|
|
|
@objc public init(
|
|
webURL: String,
|
|
stateMachineName: String?,
|
|
fit: RiveFit = .contain,
|
|
alignment: RiveAlignment = .center,
|
|
autoPlay: Bool = true,
|
|
loadCdn: Bool = true,
|
|
artboardName: String? = nil
|
|
) {
|
|
self.fit = fit
|
|
self.alignment = alignment
|
|
self.autoPlay = autoPlay
|
|
super.init()
|
|
riveModel = RiveModel(webURL: webURL, delegate: self, loadCdn: loadCdn)
|
|
defaultModel = RiveModelBuffer(artboardName: artboardName, stateMachineName: stateMachineName, animationName: nil)
|
|
}
|
|
|
|
@objc public init(
|
|
webURL: String,
|
|
animationName: String? = nil,
|
|
fit: RiveFit = .contain,
|
|
alignment: RiveAlignment = .center,
|
|
autoPlay: Bool = true,
|
|
loadCdn: Bool = true,
|
|
artboardName: String? = nil
|
|
) {
|
|
self.fit = fit
|
|
self.alignment = alignment
|
|
self.autoPlay = autoPlay
|
|
super.init()
|
|
riveModel = RiveModel(webURL: webURL, delegate: self, loadCdn: loadCdn)
|
|
defaultModel = RiveModelBuffer(artboardName: artboardName, stateMachineName: nil, animationName: animationName)
|
|
}
|
|
|
|
private func sharedInit(artboardName: String?, stateMachineName: String?, animationName: String?) {
|
|
try! configureModel(artboardName: artboardName, stateMachineName: stateMachineName, animationName: animationName)
|
|
|
|
defaultModel = RiveModelBuffer(
|
|
artboardName: artboardName,
|
|
stateMachineName: stateMachineName,
|
|
animationName: animationName
|
|
)
|
|
|
|
try! riveView?.setModel(riveModel!, autoPlay: autoPlay)
|
|
}
|
|
|
|
// MARK: - RiveView
|
|
|
|
@objc open private(set) var riveModel: RiveModel? {
|
|
didSet {
|
|
if let model = riveModel {
|
|
try! riveView?.setModel(model, autoPlay: autoPlay)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc open var isPlaying: Bool { riveView?.isPlaying ?? false }
|
|
|
|
@objc open var autoPlay: Bool
|
|
|
|
@objc open var fit: RiveFit = .contain {
|
|
didSet { riveView?.fit = fit }
|
|
}
|
|
|
|
@objc open var alignment: RiveAlignment = .center {
|
|
didSet { riveView?.alignment = alignment }
|
|
}
|
|
|
|
/// 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.
|
|
/// - Note: If the scale factor <= 0, nothing will be drawn.
|
|
@objc open var layoutScaleFactor: Double = layoutScaleFactorAutomatic {
|
|
didSet { riveView?.layoutScaleFactor = layoutScaleFactor }
|
|
}
|
|
|
|
/// Sets whether or not the current 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`.
|
|
@objc open var forwardsListenerEvents: Bool = false {
|
|
didSet { riveView?.forwardsListenerEvents = forwardsListenerEvents }
|
|
}
|
|
|
|
#if os(macOS)
|
|
/// Hints to underlying CADisplayLink in RiveView (if created) the preferred FPS to run at
|
|
/// For more, see: https://developer.apple.com/documentation/quartzcore/cadisplaylink/1648421-preferredframespersecond
|
|
/// - Parameters:
|
|
/// - preferredFramesPerSecond: Integer number of seconds to set preferred FPS at
|
|
@available(macOS 14, *)
|
|
@objc(setPreferredFramesPerSecond:)
|
|
public func setPreferredFramesPerSecond(preferredFramesPerSecond: Int) {
|
|
riveView?.setPreferredFramesPerSecond(preferredFramesPerSecond: preferredFramesPerSecond)
|
|
}
|
|
|
|
/// Hints to underlying CADisplayLink in RiveView (if created) the preferred frame rate range
|
|
/// For more, see: https://developer.apple.com/documentation/quartzcore/cadisplaylink/3875343-preferredframeraterange
|
|
/// - Parameters:
|
|
/// - preferredFrameRateRange: Frame rate range to set
|
|
@available(macOS 14, *)
|
|
@objc(setPreferredFrameRateRange)
|
|
public func setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange) {
|
|
riveView?.setPreferredFrameRateRange(preferredFrameRateRange: preferredFrameRateRange)
|
|
}
|
|
#else
|
|
/// Hints to underlying CADisplayLink in RiveView (if created) the preferred FPS to run at
|
|
/// For more, see: https://developer.apple.com/documentation/quartzcore/cadisplaylink/1648421-preferredframespersecond
|
|
/// - Parameters:
|
|
/// - preferredFramesPerSecond: Integer number of seconds to set preferred FPS at
|
|
@objc(setPreferredFramesPerSecond:)
|
|
open func setPreferredFramesPerSecond(preferredFramesPerSecond: Int) {
|
|
riveView?.setPreferredFramesPerSecond(preferredFramesPerSecond: preferredFramesPerSecond)
|
|
}
|
|
|
|
/// Hints to underlying CADisplayLink in RiveView (if created) the preferred frame rate range
|
|
/// For more, see: https://developer.apple.com/documentation/quartzcore/cadisplaylink/3875343-preferredframeraterange
|
|
/// - Parameters:
|
|
/// - preferredFrameRateRange: Frame rate range to set
|
|
@available(iOS 15.0, tvOS 15.0, visionOS 1.0, *)
|
|
@objc(setPreferredFrameRateRange:)
|
|
open func setPreferredFrameRateRange(preferredFrameRateRange: CAFrameRateRange) {
|
|
riveView?.setPreferredFrameRateRange(preferredFrameRateRange: preferredFrameRateRange)
|
|
}
|
|
#endif
|
|
|
|
/// Starts the active Animation or StateMachine from it's last position. It will start
|
|
/// from the beginning if the active Animation has ended or a new one is provided.
|
|
/// - Parameters:
|
|
/// - animationName: The name of a new Animation to play on the current Artboard
|
|
/// - loop: The loop mode for the active Animation
|
|
@objc open func play(animationName: String? = nil, loop: RiveLoop = .autoLoop, direction: RiveDirection = .autoDirection) {
|
|
if let name = animationName {
|
|
try! riveModel?.setAnimation(name)
|
|
}
|
|
|
|
if let animation = riveModel?.animation {
|
|
if loop != .autoLoop {
|
|
animation.loop(Int32(loop.rawValue))
|
|
}
|
|
|
|
if direction == .forwards {
|
|
animation.direction(1)
|
|
} else if direction == .backwards {
|
|
animation.direction(-1)
|
|
}
|
|
|
|
if animation.hasEnded() {
|
|
// Restarts Animation from beginning
|
|
animation.setTime(0)
|
|
}
|
|
}
|
|
|
|
// We're not checking if a StateMachine is "ended" or "ExitState"
|
|
// But we may want to in the future to enable restarting it by playing again
|
|
RiveLogger.log(viewModel: self, event: .play)
|
|
riveView?.play()
|
|
}
|
|
|
|
/// Halts the active Animation or StateMachine and will resume from it's current position when next played
|
|
@objc open func pause() {
|
|
RiveLogger.log(viewModel: self, event: .pause)
|
|
riveView?.pause()
|
|
}
|
|
|
|
/// Halts the active Animation or StateMachine and sets it at its starting position
|
|
@objc open func stop() {
|
|
RiveLogger.log(viewModel: self, event: .stop)
|
|
resetCurrentModel()
|
|
riveView?.stop()
|
|
}
|
|
|
|
/// Sets the active Animation or StateMachine back to their starting position
|
|
@objc open func reset() {
|
|
RiveLogger.log(viewModel: self, event: .reset)
|
|
resetCurrentModel()
|
|
riveView?.reset()
|
|
}
|
|
|
|
// MARK: - RiveModel
|
|
|
|
/// Instantiates elements in the model needed to play in a `RiveView`
|
|
@objc open func configureModel(artboardName: String? = nil, stateMachineName: String? = nil, animationName: String? = nil) throws {
|
|
guard let model = riveModel else {
|
|
let errorMessage = "Cannot configure nil RiveModel"
|
|
RiveLogger.log(viewModel: self, event: .fatalError(errorMessage))
|
|
fatalError(errorMessage)
|
|
}
|
|
|
|
model.animation = nil
|
|
model.stateMachine = nil
|
|
|
|
if let name = artboardName {
|
|
try model.setArtboard(name)
|
|
} else {
|
|
// Keep current Artboard if there is one
|
|
if model.artboard == nil {
|
|
// Set default Artboard if not
|
|
try model.setArtboard()
|
|
}
|
|
}
|
|
|
|
if let name = stateMachineName {
|
|
try model.setStateMachine(name)
|
|
}
|
|
else if let name = animationName {
|
|
try model.setAnimation(name)
|
|
}
|
|
|
|
// Find defaults
|
|
else {
|
|
// Attempts to set a default StateMachine first
|
|
if ((try? model.setStateMachine()) == nil) {
|
|
// If it fails, attempts a default Animation
|
|
try model.setAnimation()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Puts the active Animation or StateMachine back to their starting position
|
|
private func resetCurrentModel() {
|
|
guard let model = riveModel else {
|
|
let errorMessage = "Current model is nil"
|
|
RiveLogger.log(viewModel: self, event: .fatalError(errorMessage))
|
|
fatalError(errorMessage)
|
|
}
|
|
try! configureModel(
|
|
artboardName: model.artboard.name(),
|
|
stateMachineName: model.stateMachine?.name(),
|
|
animationName: model.animation?.name()
|
|
)
|
|
}
|
|
|
|
/// Sets the Artboard, StateMachine or Animation back to the first one given to the RiveViewModel
|
|
@objc open func resetToDefaultModel() {
|
|
try! configureModel(
|
|
artboardName: defaultModel.artboardName,
|
|
stateMachineName: defaultModel.stateMachineName,
|
|
animationName: defaultModel.animationName
|
|
)
|
|
}
|
|
|
|
|
|
/// Provide the active StateMachine a `Trigger` input
|
|
/// - Parameter inputName: The name of a `Trigger` input on the active StateMachine
|
|
@objc open func triggerInput(_ inputName: String) {
|
|
RiveLogger.log(viewModel: self, event: .triggerInput(inputName, nil))
|
|
riveModel?.stateMachine?.getTrigger(inputName).fire()
|
|
play()
|
|
}
|
|
|
|
/// Provide the active StateMachine a `Boolean` input
|
|
/// - Parameters:
|
|
/// - inputName: The name of a `Boolean` input on the active StateMachine
|
|
/// - value: A Bool value for the input
|
|
@objc(setBooleanInput::) open func setInput(_ inputName: String, value: Bool) {
|
|
RiveLogger.log(viewModel: self, event: .booleanInput(inputName, nil, value))
|
|
riveModel?.stateMachine?.getBool(inputName).setValue(value)
|
|
play()
|
|
}
|
|
|
|
/// Returns the current boolean input by name. Get its value by calling `.value` on the returned object.
|
|
/// - Parameter name: The name of the input
|
|
/// - Returns: The boolean input if it exists. Returns `nil` if the input cannot be found.
|
|
@objc open func boolInput(named name: String) -> RiveSMIBool?
|
|
{
|
|
guard let input = riveModel?.stateMachine?.getBool(name) else {
|
|
RiveLogger.log(viewModel: self, event: .error("Cannot find bool input named \(name)"))
|
|
return nil
|
|
}
|
|
return input
|
|
}
|
|
|
|
/// Provide the active StateMachine a `Number` input
|
|
/// - Parameters:
|
|
/// - inputName: The name of a `Number` input on the active StateMachine
|
|
/// - value: A Float value for the input.
|
|
@objc(setFloatInput::) open func setInput(_ inputName: String, value: Float) {
|
|
RiveLogger.log(viewModel: self, event: .floatInput(inputName, nil, value))
|
|
riveModel?.stateMachine?.getNumber(inputName).setValue(value)
|
|
play()
|
|
}
|
|
|
|
/// Provide the active StateMachine a `Number` input
|
|
/// - Parameters:
|
|
/// - inputName: The name of a `Number` input on the active StateMachine
|
|
/// - value: A Double value for the input
|
|
@objc(setDoubleInput::) open func setInput(_ inputName: String, value: Double) {
|
|
RiveLogger.log(viewModel: self, event: .doubleInput(inputName, nil, value))
|
|
setInput(inputName, value: Float(value))
|
|
}
|
|
|
|
/// Returns the current number input by name. Get its value by calling `.value` on the returned object.
|
|
/// - Parameter name: The name of the input
|
|
/// - Returns: The number input if it exists. Returns `nil` if the input cannot be found.
|
|
@objc open func numberInput(named name: String) -> RiveSMINumber?
|
|
{
|
|
guard let input = riveModel?.stateMachine?.getNumber(name) else {
|
|
RiveLogger.log(viewModel: self, event: .error("Cannot find number input named \(name)"))
|
|
return nil
|
|
}
|
|
return input
|
|
}
|
|
|
|
/// Provide the specified nested Artboard with a `Trigger` input
|
|
/// - Parameters:
|
|
/// - inputName: The name of a `Trigger` input on the active StateMachine
|
|
/// - path: A String representing the path to the nested artboard delimited by "/" (ie. "Nested" or "Level1/Level2/Level3")
|
|
@objc open func triggerInput(_ inputName: String, path: String) {
|
|
RiveLogger.log(viewModel: self, event: .triggerInput(inputName, path))
|
|
riveModel?.artboard?.getTrigger(inputName, path: path).fire()
|
|
play()
|
|
}
|
|
|
|
/// Provide the specified nested Artboard with a `Boolean` input
|
|
/// - Parameters:
|
|
/// - inputName: The name of a `Boolean` input on the active StateMachine
|
|
/// - value: A Bool value for the input
|
|
/// - path: A String representing the path to the nested artboard delimited by "/" (ie. "Nested" or "Level1/Level2/Level3")
|
|
@objc(setBooleanInput:value:atPath:)
|
|
open func setInput(_ inputName: String, value: Bool, path: String) {
|
|
RiveLogger.log(viewModel: self, event: .booleanInput(inputName, path, value))
|
|
riveModel?.artboard?.getBool(inputName, path: path).setValue(value)
|
|
play()
|
|
}
|
|
|
|
/// Provide the specified nested Artboard with a `Number` input
|
|
/// - Parameters:
|
|
/// - inputName: The name of a `Number` input on the active StateMachine
|
|
/// - value: A Float value for the input
|
|
/// - path: A String representing the path to the nested artboard delimited by "/" (ie. "Nested" or "Level1/Level2/Level3")
|
|
@objc(setFloatInput:value:atPath:)
|
|
open func setInput(_ inputName: String, value: Float, path: String) {
|
|
RiveLogger.log(viewModel: self, event: .floatInput(inputName, path, value))
|
|
riveModel?.artboard?.getNumber(inputName, path: path).setValue(value);
|
|
play()
|
|
}
|
|
|
|
/// Provide the specified nested Artboard with a `Number` input
|
|
/// - Parameters:
|
|
/// - inputName: The name of a `Number` input on the active StateMachine
|
|
/// - value: A Double value for the input
|
|
/// - path: A String representing the path to the nested artboard delimited by "/" (ie. "Nested" or "Level1/Level2/Level3")
|
|
@objc(setDoubleInput:value:atPath:)
|
|
open func setInput(_ inputName: String, value: Double, path: String) {
|
|
RiveLogger.log(viewModel: self, event: .doubleInput(inputName, path, value))
|
|
setInput(inputName, value: Float(value), path: path)
|
|
}
|
|
|
|
/// Get a text value from a specified text run
|
|
/// - Parameters:
|
|
/// - textRunName: The name of a `Text Run` on the active Artboard
|
|
/// - Returns: String text value of the specified text run if applicable
|
|
@objc open func getTextRunValue(_ textRunName: String) -> String? {
|
|
#if WITH_RIVE_TEXT
|
|
if let textRun = riveModel?.artboard?.textRun(textRunName) {
|
|
return textRun.text()
|
|
}
|
|
#endif
|
|
return nil
|
|
}
|
|
|
|
/// Get a text value from a specified text run
|
|
/// - Parameters:
|
|
/// - textRunName: The name of a `Text Run` on the active Artboard
|
|
/// - path: The path to the nested text run.
|
|
/// - Returns: String text value of the specified text run if applicable
|
|
@objc open func getTextRunValue(_ textRunName: String, path: String) -> String? {
|
|
#if WITH_RIVE_TEXT
|
|
if let textRun = riveModel?.artboard?.textRun(textRunName, path: path) {
|
|
return textRun.text()
|
|
}
|
|
#endif
|
|
return nil
|
|
}
|
|
|
|
/// Set a text value for a specified text run
|
|
/// - Parameters:
|
|
/// - textRunName: The name of a `Text Run` on the active Artboard
|
|
/// - textValue: A String value for the text run
|
|
@objc open func setTextRunValue(_ textRunName: String, textValue: String) throws {
|
|
#if WITH_RIVE_TEXT
|
|
if let textRun = riveModel?.artboard?.textRun(textRunName) {
|
|
RiveLogger.log(viewModel: self, event: .textRun(textRunName, nil, textValue))
|
|
textRun.setText(textValue)
|
|
|
|
if isPlaying == false {
|
|
riveView?.advance(delta: 0)
|
|
}
|
|
} else {
|
|
let errorMessage = "Could not set text value on text run: \(textRunName) as the text run could not be found from the active artboard"
|
|
RiveLogger.log(viewModel: self, event: .error(errorMessage))
|
|
throw RiveError.textValueRunError(errorMessage)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// Set a text value for a specified text run
|
|
/// - Parameters:
|
|
/// - textRunName: The name of a `Text Run` on the active Artboard
|
|
/// - path: The path to the nested text run.
|
|
/// - textValue: A String value for the text run
|
|
/// - Note: If the specified path is empty, the parent artboard will be used to find the text run.
|
|
@objc open func setTextRunValue(_ textRunName: String, path: String, textValue: String) throws {
|
|
#if WITH_RIVE_TEXT
|
|
if let textRun = riveModel?.artboard?.textRun(textRunName, path: path) {
|
|
RiveLogger.log(viewModel: self, event: .textRun(textRunName, path, textValue))
|
|
textRun.setText(textValue)
|
|
|
|
if isPlaying == false {
|
|
riveView?.advance(delta: 0)
|
|
}
|
|
} else {
|
|
let errorMessage = "Could not set text value on text run: \(textRunName) as the text run could not be found from the active artboard"
|
|
RiveLogger.log(viewModel: self, event: .error(errorMessage))
|
|
throw RiveError.textValueRunError(errorMessage)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// TODO: Replace this with a more robust structure of the file's contents
|
|
@objc open func artboardNames() -> [String] {
|
|
return riveModel?.riveFile.artboardNames() ?? []
|
|
}
|
|
|
|
// MARK: - SwiftUI Helpers
|
|
|
|
/// Makes a new `RiveView` for the instance property with data from model which will
|
|
/// replace any previous `RiveView`. This is called when first drawing a `RiveViewRepresentable`.
|
|
/// - Returns: Reference to the new view that the `RiveViewModel` will be maintaining
|
|
@objc open func createRiveView() -> RiveView {
|
|
let view: RiveView
|
|
|
|
if let model = riveModel {
|
|
view = RiveView(model: model, autoPlay: autoPlay)
|
|
} else {
|
|
view = RiveView()
|
|
}
|
|
|
|
registerView(view)
|
|
|
|
return view
|
|
}
|
|
|
|
@objc(setRiveView:)
|
|
open func setRiveView(view:RiveView)
|
|
{
|
|
registerView(view)
|
|
}
|
|
|
|
/// Gives updated layout values to the provided `RiveView`. This is called in
|
|
/// the process of re-displaying `RiveViewRepresentable`.
|
|
/// - Parameter view: the `RiveView` that will be updated
|
|
@objc open func update(view: RiveView) {
|
|
view.fit = fit
|
|
view.alignment = alignment
|
|
view.layoutScaleFactor = layoutScaleFactor
|
|
view.forwardsListenerEvents = forwardsListenerEvents
|
|
}
|
|
|
|
/// Assigns the provided `RiveView` to the riveView property. This is called when
|
|
/// creating a `RiveViewRepresentable` in the `.view()` method for SwiftUI and when
|
|
/// adding to the view hierarchy with `.createRiveView()` in UIKit.
|
|
///
|
|
/// - Parameter view: the `RiveView` that this `RiveViewModel` will maintain
|
|
fileprivate func registerView(_ view: RiveView) {
|
|
riveView = view
|
|
riveView!.playerDelegate = self
|
|
riveView!.stateMachineDelegate = self
|
|
riveView!.fit = fit
|
|
riveView!.alignment = alignment
|
|
riveView!.layoutScaleFactor = layoutScaleFactor
|
|
riveView!.forwardsListenerEvents = forwardsListenerEvents
|
|
}
|
|
|
|
/// Stops maintaining a connection to any `RiveView`
|
|
@objc open func deregisterView() {
|
|
riveView = nil
|
|
}
|
|
|
|
/// This can be added to the body of a SwiftUI `View`
|
|
open func view() -> AnyView {
|
|
return AnyView(RiveViewRepresentable(viewModel: self))
|
|
}
|
|
|
|
// MARK: - UIKit Helper
|
|
|
|
/// This can be used to connect with and configure an `RiveView` that was created elsewhere.
|
|
/// Does not need to be called when updating an already configured `RiveView`. Useful for
|
|
/// attaching views created in a `UIViewController` or Storyboard.
|
|
/// - Parameter view: the `RiveView` that this `RiveViewModel` will maintain
|
|
@objc open func setView(_ view: RiveView) {
|
|
registerView(view)
|
|
try! riveView!.setModel(riveModel!, autoPlay: autoPlay)
|
|
}
|
|
|
|
// MARK: - RiveFile Delegate
|
|
|
|
/// Needed for when resetting to defaults or the RiveViewModel is initialized with a webURL so
|
|
/// we are then able make a RiveModel when the RiveFile is finished downloading
|
|
private struct RiveModelBuffer {
|
|
var artboardName: String?
|
|
var stateMachineName: String?
|
|
var animationName: String?
|
|
}
|
|
|
|
/// Called by RiveFile when it finishes downloading an asset asynchronously
|
|
@objc open func riveFileDidLoad(_ riveFile: RiveFile) throws {
|
|
riveModel = RiveModel(riveFile: riveFile)
|
|
|
|
sharedInit(
|
|
artboardName: defaultModel.artboardName,
|
|
stateMachineName: defaultModel.stateMachineName,
|
|
animationName: defaultModel.animationName
|
|
)
|
|
}
|
|
|
|
// MARK: - RivePlayer Delegate
|
|
|
|
@objc open func player(playedWithModel riveModel: RiveModel?) { }
|
|
@objc open func player(pausedWithModel riveModel: RiveModel?) { }
|
|
@objc open func player(loopedWithModel riveModel: RiveModel?, type: Int) { }
|
|
@objc open func player(stoppedWithModel riveModel: RiveModel?) { }
|
|
@objc open func player(didAdvanceby seconds: Double, riveModel: RiveModel?) { }
|
|
|
|
enum RiveError: Error {
|
|
case textValueRunError(_ message: String)
|
|
}
|
|
}
|
|
|
|
#if os(iOS) || os(visionOS) || os(tvOS)
|
|
/// This makes a SwiftUI digestable view from an `RiveViewModel` and its `RiveView`
|
|
public struct RiveViewRepresentable: UIViewRepresentable {
|
|
let viewModel: RiveViewModel
|
|
|
|
public init(viewModel: RiveViewModel) {
|
|
self.viewModel = viewModel
|
|
}
|
|
|
|
/// Constructs the view
|
|
public func makeUIView(context: Context) -> RiveView {
|
|
return viewModel.createRiveView()
|
|
}
|
|
|
|
public func updateUIView(_ view: RiveView, context: UIViewRepresentableContext<RiveViewRepresentable>) {
|
|
viewModel.update(view: view)
|
|
}
|
|
|
|
public static func dismantleUIView(_ view: RiveView, coordinator: Coordinator) {
|
|
view.stop()
|
|
if (view == coordinator.viewModel.riveView) {
|
|
coordinator.viewModel.deregisterView()
|
|
}
|
|
}
|
|
|
|
/// Constructs a coordinator for managing updating state
|
|
public func makeCoordinator() -> Coordinator {
|
|
return Coordinator(viewModel: viewModel)
|
|
}
|
|
|
|
public class Coordinator: NSObject {
|
|
public var viewModel: RiveViewModel
|
|
|
|
init(viewModel: RiveViewModel) {
|
|
self.viewModel = viewModel
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
public struct RiveViewRepresentable: NSViewRepresentable {
|
|
let viewModel: RiveViewModel
|
|
|
|
public init(viewModel: RiveViewModel) {
|
|
self.viewModel = viewModel
|
|
}
|
|
|
|
/// Constructs the view
|
|
public func makeNSView(context: Context) -> RiveView {
|
|
return viewModel.createRiveView()
|
|
}
|
|
|
|
public func updateNSView(_ view: RiveView, context: NSViewRepresentableContext<RiveViewRepresentable>) {
|
|
viewModel.update(view: view)
|
|
}
|
|
|
|
public static func dismantleNSView(_ view: RiveView, coordinator: Coordinator) {
|
|
view.stop()
|
|
coordinator.viewModel.deregisterView()
|
|
}
|
|
|
|
/// Constructs a coordinator for managing updating state
|
|
public func makeCoordinator() -> Coordinator {
|
|
return Coordinator(viewModel: viewModel)
|
|
}
|
|
|
|
public class Coordinator: NSObject {
|
|
public var viewModel: RiveViewModel
|
|
|
|
init(viewModel: RiveViewModel) {
|
|
self.viewModel = viewModel
|
|
}
|
|
}
|
|
}
|
|
#endif
|