Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ The following table compares feature availability with the [previous Rive React
| `useRiveFile()` hook | ✅ | Convenient hook to load a Rive file |
| `RiveView` error handling | ✅ | Error handler for failed view operations |
| `source` .riv file loading | ✅ | Conveniently load .riv files from JS source |
| Accessibility semantics | ⚠️ | Editor-authored semantics → VoiceOver (iOS; Android in progress) |
| Animation selection | ❌ | Animation playback not planned, use state machines |
| Renderer options | ❌ | Single renderer option available (Rive) |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
override var alignment: Alignment? = null
override var fit: Fit? = null
override var layoutScaleFactor: Double? = null

// Accepted for API parity; semantics are only available in the new Rive
// runtime, and Android support is pending upstream (iOS-only for now).
override var semantics: Semantics? = null
override var dataBind: Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName? = null
set(value) {
if (field != value) {
Expand Down
4 changes: 4 additions & 0 deletions android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
override var alignment: Alignment? = null
override var fit: Fit? = null
override var layoutScaleFactor: Double? = null

// Accepted for API parity; semantics support is pending in the upstream
// rive-android runtime (iOS-only for now).
override var semantics: Semantics? = null
override var dataBind: Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName? = null
set(value) {
if (field != value) {
Expand Down
Binary file added example/assets/rive/tabtest.riv
Binary file not shown.
122 changes: 122 additions & 0 deletions example/src/demos/SemanticsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { RiveView, useRiveFile, Semantics } from '@rive-app/react-native';
import type { Metadata } from '../shared/metadata';

/*
Semantics — editor-authored accessibility exposed to VoiceOver.

tabtest.riv (from rive-runtime's semantics test assets) authors a tab bar
with roles, labels and selection state. With semantics enabled, each tab
becomes an accessibility element: VoiceOver reads them and can activate
them. iOS new (default) backend only; see
https://rive.app/docs/runtimes/apple/semantics
*/

const MODES: { label: string; value: Semantics }[] = [
{ label: 'Off', value: Semantics.Off },
{ label: 'On', value: Semantics.On },
{ label: 'Automatic', value: Semantics.Automatic },
];

export default function SemanticsExample() {
const { riveFile, error } = useRiveFile(
require('../../assets/rive/tabtest.riv')
);
const [semantics, setSemantics] = useState(Semantics.On);

if (error) {
return (
<View style={styles.container}>
<Text style={styles.errorText}>{error.message}</Text>
</View>
);
}

return (
<View style={styles.container}>
<Text style={styles.subtitle}>
With semantics On (or Automatic + VoiceOver running), the tabs below are
exposed to VoiceOver as selectable accessibility elements.
</Text>
<View style={styles.modeRow}>
{MODES.map(({ label, value }) => (
<TouchableOpacity
key={label}
testID={`semantics-${label}`}
style={[styles.modeButton, semantics === value && styles.modeOn]}
onPress={() => setSemantics(value)}
>
<Text
style={[
styles.modeLabel,
semantics === value && styles.modeLabelOn,
]}
>
{label}
</Text>
</TouchableOpacity>
))}
</View>
{riveFile && (
<RiveView
file={riveFile}
semantics={semantics}
style={styles.rive}
autoPlay={true}
/>
)}
</View>
);
}

SemanticsExample.metadata = {
name: 'Semantics (VoiceOver)',
description:
'Editor-authored accessibility semantics exposed to VoiceOver (iOS new backend)',
order: 3,
} satisfies Metadata;

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
subtitle: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 12,
},
modeRow: {
flexDirection: 'row',
justifyContent: 'center',
gap: 8,
marginBottom: 16,
},
modeButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
borderColor: '#ccc',
},
modeOn: {
backgroundColor: '#222',
borderColor: '#222',
},
modeLabel: {
fontSize: 14,
color: '#333',
},
modeLabelOn: {
color: '#fff',
},
rive: {
flex: 1,
},
errorText: {
color: 'red',
},
});
3 changes: 3 additions & 0 deletions ios/legacy/HybridRiveView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ class HybridRiveView: HybridRiveViewSpec {
var alignment: Alignment?
var fit: Fit?
var layoutScaleFactor: Double?
// Accepted for API parity; semantics are only available in the new Rive
// runtime (the experimental backend).
var semantics: Semantics?
var onError: (RiveError) -> Void = { _ in }

func awaitViewReady() throws -> Promise<Bool> {
Expand Down
11 changes: 11 additions & 0 deletions ios/new/HybridRiveView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class HybridRiveView: HybridRiveViewSpec {
var alignment: Alignment?
var fit: Fit?
var layoutScaleFactor: Double?
var semantics: Semantics?
var onError: (RiveError) -> Void = { _ in }

func awaitViewReady() throws -> Promise<Bool> {
Expand Down Expand Up @@ -211,6 +212,7 @@ class HybridRiveView: HybridRiveViewSpec {
autoPlay: autoPlay ?? DefaultConfiguration.autoPlay,
file: riveFile,
fit: toRiveFit(fit, alignment: alignment, layoutScaleFactor: layoutScaleFactor),
semantics: toRiveSemantics(semantics),
bindData: try dataBind.toBindData()
)

Expand Down Expand Up @@ -258,6 +260,15 @@ class HybridRiveView: HybridRiveViewSpec {
}
}

private func toRiveSemantics(_ semantics: Semantics?) -> RiveRuntime.Semantics {
switch semantics {
case .off: return .off
case .on: return .on
case .automatic: return .automatic
case nil: return RiveUIView.Constants.Defaults.semantics
}
}

private func toRiveAlignment(_ alignment: Alignment?) -> RiveRuntime.Alignment? {
guard let alignment = alignment else { return nil }

Expand Down
8 changes: 8 additions & 0 deletions ios/new/RiveReactNativeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
let autoPlay: Bool
let file: File
let fit: RiveRuntime.Fit
let semantics: RiveRuntime.Semantics
let bindData: BindData
}

Expand All @@ -27,6 +28,9 @@
private var isViewReady = false
private var configTask: Task<Void, Never>?
private var isPaused = false
private var semantics: RiveRuntime.Semantics = RiveUIView.Constants.Defaults.semantics {
didSet { riveUIView?.semantics = semantics }
}
var autoPlay: Bool = true

/// Configure failures are reported here (wired to the onError prop).
Expand All @@ -52,6 +56,8 @@
func configure(_ config: ViewConfiguration, dataBindingChanged: Bool = false, reload: Bool = false, initialUpdate: Bool = false) {
dispatchPrecondition(condition: .onQueue(.main))

semantics = config.semantics

if reload {
cleanup()
}
Expand Down Expand Up @@ -87,7 +93,7 @@
// Probe for a default ViewModel first. If the artboard has none,
// the SDK would fire an error event — skip auto-binding silently instead.
do {
let _ = try await config.file.getDefaultViewModelInfo(for: artboard)

Check warning on line 96 in ios/new/RiveReactNativeView.swift

View workflow job for this annotation

GitHub Actions / lint-swift

Prefer `_ = foo()` over `let _ = foo()` when discarding a result from a function (redundant_discardable_let)
dataBind = .auto
} catch {
dataBind = .none
Expand Down Expand Up @@ -208,8 +214,10 @@
// not found") from the old MTKView after removeFromSuperview.
existing.rive = rive
existing.isPaused = isPaused
existing.semantics = semantics
} else {
let uiView = RiveUIView(rive: rive, isPaused: isPaused)
uiView.semantics = semantics
uiView.translatesAutoresizingMaskIntoConstraints = false
addSubview(uiView)
NSLayoutConstraint.activate([
Expand Down
13 changes: 13 additions & 0 deletions nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions nitrogen/generated/android/c++/JSemantics.hpp

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading