Adds support for the updated `CADisplayLink` API available on macOS 14+, which allows for setting preferred fps / frame rate range. This also moves the `DisplayLinkProxy` into a protocol with concrete types for all platforms.
## Changes
- [ ] Update `DisplayLinkProxy` to `RiveDisplayLink` protocol
- Replace usage of proxy with new protocol
- [ ] On macOS 14+, use `CADisplayLink`
- Additionally adds support for preferred fps / frame rate range
## Notes
While this is not "breaking", it appropriately changes the availability of some setters to match the API availability of the display link used. Some developers may find that there are errors - for example, setting FPS was unavailable on macOS, since `CVDisplayLink` didn't support it. Instead of no-oping, this function is now unavailable if the OS doesn't support it.
Diffs=
34820199f2 Use updated CADisplayLink API for macOS 14+ (#8899)
Co-authored-by: David Skuza <david@rive.app>
Addresses this issue: https://2dimensions.slack.com/archives/C07VD44ASE5/p1739456802968219
Rive's RenderPath steals the memory for the RawPath the runtime builds, which is usually fine apart from cases where we expect to use the RawPath after drawing with it. For example, with inner feather we want to be able to compute the bounds of the RawPath after it has been drawn. If during animation a user turns on inner feathering on a path which has not changed recently, which need to be able to compute its bounds, we can't if the RawPath is now gone.
This also can happen with trim paths and dash paths which are re-trimmed/dashed after having being rendered a few frames. Right now we force rebuild the whole path to re-trim it because we lose the RawPath.
This PR allows the ShapePaintPath to hold on to its RawPath by adding RenderPath::addRawPath. This is optimized memory usage but not having the runtime need to re-alloc the RawPath whenever it animates, but it means we're further from making RenderPaths immutable.
For the future: It'd be good to chat about a long term solution that can accommodate all goals of allocating less memory (prior to this change we were re-allocating RawPath memory every frame when animating), not rebuilding paths as often, and still allow for immutability.
Diffs=
9058a3fdad Add RenderPath::addRawPath (#9038)
Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
The (likely) most common use of all `RiveFile` initializers is the `RiveFile.init(name:extension:in:loadCdn:customLoader)` initializer available in `RiveFile+Extensions.swift`. This is the initializer called when a (view) model is initialized with a file name. However, some crash reports are showing that this function is crashing within the Swift runtime. I believe that under certain conditions (I've been unable to reproduce, unfortunately), the Swift runtime is crashing when initializing new types, or bridging between Swift and ObjC - I think that minimizing the usage of different types and bridging may fix some of these crashes.
By switching from `init(byteArray:`) to `init(data:)`, we can remove some (extra) initialization, casting, and bridging between Swift and ObjC.
In the current codepath, there is first new initialization within Swift from `Data` to `[UInt8]` (allowed because `Data` conforms to `Sequence`), which then gets bridged to Objective-C as `NSArray<NSNumber*>*`. Notice the additional bridge from `UInt8` to `NSNumber`, since ObjC can't directly handle Swift value types. So, bytes are getting transformed into a new type - from Swift `Data`, to `UInt8`, to an `NSObject` (since `NSNumber` is a reference type, required for use with `NSArray`). From here, _another_ byte array is created, converting each `NSNumber` back into a byte representation.
`init(data:)` is a simpler solution - since `Data` bridges between Swift and ObjC (as NSData), we can then treat the new `NSData.bytes` as a `UInt8*` to pass to the C++ runtime, without having to allocate a new byte array, or bridging between Swift primitives and ObjC types.
## Current Codepath
- Grab the file data as `Data`
- Initialize a new array - the data as `[UInt8]`
- Pass the bytes to `init(byteArray:`)
- This bridges `[UInt8]` to `NSArray<NSNumber*>*`
- Create a _new_ byte array from the supplied array.
- Into the C++ runtime…
- The file loads
## New Codepath
- Grab the file data as `Data`
- Pass the data to `init(data:)`
- This bridges `Data` to `NSData`
- Cast `data.bytes` to UInt8*
- Into the C++ runtime…
- The file loads
## Origin
It seems like some of these changes occurred when out-of-band asset support was added to the iOS runtime. There were some new `RiveFile` initializers added; I'm not sure if there was an intention of switching which was used or not.
## Testing
Nearly the entirety of the Example app uses file name initialization. I ran through the entire app and ensured each animation loaded as intended (both in the Simulator and on a physical device).
Diffs=
11cf3a1ad9 feat(ios): use RiveFile.init(data:) initializer over init(byteArray:) (#9020)
Co-authored-by: David Skuza <david@rive.app>
Initially the work for https://github.com/rive-app/rive-ios/issues/352
Additionally resolves all warnings except for updating the project to recommended settings, I'll want to take a deeper look into what those changes are / will be.
Diffs=
f257ec36eb Resolve various iOS warnings (#8827)
Co-authored-by: David Skuza <david@rive.app>
Similar to Android: https://github.com/rive-app/rive/pull/8725
With changes from Option C, the C++ runtime will report that an animation should not continue if advancing by 0. This causes the iOS advance logic to break, and if stopping / pausing and then playing, the advance by 0 will continually return false, not allowing an animation to play again.
Diffs=
d527114521 Force advancing on iOS if advancing by 0 (#8766)
Co-authored-by: David Skuza <david@rive.app>
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>
This pull request builds on top of fallback font support on iOS by including the ability to provide fallback fonts based on the styling of the missing character. Currently, the style information only contains weight. This weight is grabbed by calling `getAxisValue`. According to Luigi, this is a linear search, so perhaps there's room for a performance optimization later on.
There is one lower-level C++ change: `gFallbackProc` returns the font for the missing character, in addition _to_ the missing character (as a second parameter). This font will be used to generate the requested styling within the iOS runtime.
This adds a new class property to `RiveFont`: `fallbackFontCallback` (whose name I'm open to changing). This is a block (i.e closure) that will be called when a fallback font is requested. It supplies the styling of the missing character so that, for example, different fonts can be used based on the weight of the missing character. For example usage, see `SwiftFallbackFonts.swift`. This provider is what's used under-the-hood, and utilizes the pre-existing `fallbackFonts` class property
The "trickiest" bit here is the caching. NSDictionary requires equatable / hashable types as keys, and we want to minimize additional generation of a Rive font, so we cache any used fonts in a wrapper type, used as the value. When new fallback fonts are provided, either directly or when a new provider block is set, the cache will be reset. Once the weight is determined, generating the right key is as simple as calling the right initializer, and when set, generating the right value is simple as calling the right initializer with the created Rive font.
Finally, `RiveFactory` was getting a little bloated, so I did a little file cleanup.
This pull requests also includes Android support from #8621
Diffs=
7986d64d83 Support supplying mobile fallback fonts by style with caching (#8396)
Co-authored-by: David Skuza <david@rive.app>
Co-authored-by: Umberto <usonnino@gmail.com>
Co-authored-by: Umberto Sonnino <umberto@rive.app>
More experimenting on top of https://github.com/rive-app/rive/pull/8556
This breaks some APIs that still need to be fixed up elsewhere.
The basic concept here is that when you request a system font you can tell it whether you're requesting to use it a Harfbuzz shaped system font or a CoreText (System) shaped system font.
We prioritize Harfbuzz first so that we have predictable performance and results across edit and runtime. To do this the fallback process now comes with an index. Iteration will keep happening until no font is returned (or all glyphs are found).
If the registered fallbacks look like this:
```
RiveFont.fallbackFonts = [
UIFont(name: "PingFangSC-Semibold", size: 12)!,
UIFont(name: "Hiragino Sans", size: 12)!
]
```
The fallback process will return:
```
iter 0: Ping Fang Harfbuzz Shaped
iter 1: Hiragino San Harfbuzz Shaped
iter 2: Ping Fang Coretext Shaped
iter 3: Hiragino Sans Coretext Shaped
```
We also use Coretext last as usually shaping with Coretext causes Apple's shaper to do its own fallbacks (it calls them cascades like css). So iter 2 (in the example above) will be the final iteration as all glyphs get filled via Apple's own fallbacks.

Diffs=
3eefba5039 CoreText fallback shaper ex (#8568)
Co-authored-by: David Skuza <david@rive.app>
Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
Under certain circumstances, e.g "off-screen rendering" (i.e a Rive view not added to a view hierarchy), there is no window scene available, but there are still trait collections available. When using window scene, Rive would determine it is unable to draw because there is no window scene. However, using the trait collection allows Rive to continue drawing despite not being added to a view hierarchy. This allows manual calls to `draw` to continue when the view is _not_ added to a view hierarchy.
Diffs=
13939097af Use trait collection for display scale over window scene (#8472)
Co-authored-by: David Skuza <david@rive.app>
First steps towards supporting artboard resizing in our runtimes. This PR includes:
- New Fit type `autoResizeArtboard`. After a bit of back and forth, I think this keeps the API simple.
- ScaleFactor which represents a scale value to scale the artboard by in addition to resizing (only applies with `autoResizeArtboard`). This may be useful because an artboard is created at a specific width/height, but it may be deployed to platforms where it is rendered to a much smaller or larger surface/texture. Currently the default is 1.0 (no scale), however, an alternative is to have it default to something like textureSize / artboardSize so we sort of auto normalize it.
- Implemented on iOS. Once this is finalzed, we can work with DevRels to implement across all runtimes.
- TODO : Bubble up an event when the artboard size is changed internally by the .riv
https://github.com/user-attachments/assets/20e9fdda-5c3e-4f3f-b2f5-104ff0291fbe
Diffs=
e71b4cc081 feat: add runtime layout fit type for ios, android, web (#8341)
Co-authored-by: CI <pggordonhayes@gmail.com>
Co-authored-by: David Skuza <david@rive.app>
Co-authored-by: Philip Chung <philterdesign@gmail.com>
Fixes a build error introduced in #8334.
TL;DR: Adds a missing compile-time check for iOS vs macOS, so macOS was attempting to use a UIKit type.
Diffs=
64aab1d825 Fix build error when validating drawing on macOS (#8379)
Co-authored-by: David Skuza <david@rive.app>
Adds matching getters for the three input types: `Bool`, `Float`, and `Double`. This cleans up having to go through `riveModel?.stateMachine?` and other SMI input functions to retrieve values.
Diffs=
1707c43b86 Add input getters to iOS view model (#8291)
Co-authored-by: David Skuza <david@rive.app>
This pull request fixes some crashes (see [rive-ios #343](https://github.com/rive-app/rive-ios/issues/343)) where the (drawable) size of the underlying view was not within the bounds within which Metal is capable of drawing.
Possible conditions:
- Width or height of the view is 0
- Transform of the view is x / y scale 0
The maximum allowed drawable sizes are based on [this Apple document](https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf). See: page 7.
Essentially, before every draw, we now check whether we _can_ draw via Metal based on the size of the view, as well as the provided drawable size of the underlying Metal view. If not, we do an early return and no drawing happens.
Unit tests have also been added for a few main scenarios under which this could occur.
Diffs=
fab89ebbba Validate rect and drawable size before drawing on iOS (#8334)
Co-authored-by: David Skuza <david@rive.app>
Adds (structured) logging via the iOS `Logger` API. Under the hood, this uses `os_log` for both in-memory and on-disk logging, depending on which level is used; this means _where_ is handled by the system, we just provide the level.
The API is a little interesting; you can't have a "generic" `log` function that takes in the message. iOS requires that you use interpolation when logging.
Logging is structured so that various categories are set under one subsystem. These categories are: view model, state machine, artboard, file, and view. Each of these can log one of debug, info, default, error, and fault levels. The developer can filter which categories and levels can be logged; Xcode also supports filtering within the console.
Logging itself is split into three things: the categories, the levels, and the logging. Within each "category" of logging, there exist events that can be logged. These are enums with associated values. When using Objective-C, there are helper functions that under-the-hood call logging functions with these events. Since there are a few categories, and various events for each category, these categories are split into extensions on `RiveLogger`. At the end of the day, there exists a single log function, which ensures a log category and level are available for logging, and then a log function that is essentially a switch statement on each event, logging the (interpolated) message.
These logging events are then utilized in the files mentioned above; the categories match the files where logging has been added. Primarily setters are called, or errors that may not be handled, but may be useful. Fatal errors are also logged.
When adding new logging:
1. Check if an existing extension exists. If not, create one.
2. Create an enum of possible events.
3. Create a `log` function that takes the model, and the event.
4. Create a `_log` function that verifies that an event has been called.
## Example usage
```swift
// Somewhere early in the app lifecycle…
RiveLogger.isEnabled = true
RiveLogger.isVerbose = true // advances are considered verbose
RiveLogger.levels = [.fatal] // filter for only specific levels, such as fatal errors
RiveLogger.categories = [.stateMachine, .viewModel] // filter for only specific categories
```
Diffs=
e1fc239974 Add logging to rive-ios (#8252)
Co-authored-by: David Skuza <david@rive.app>