Skip to content
Closed
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
142 changes: 142 additions & 0 deletions example/src/reproducers/Issue393DisplayZoom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { useEffect, useState } from 'react';
import {
View,
Text,
StyleSheet,
Pressable,
type ViewStyle,
} from 'react-native';
import {
RiveView,
Fit,
useRiveFile,
type RiveFile,
} from '@rive-app/react-native';
import { type Metadata } from '../shared/metadata';

/**
* Issue #393: on iOS devices where UIScreen.nativeScale != scale (Display
* Zoom, iPhone mini and Plus), MTKView sizes its drawable through ancestor
* transforms. A rotated parent squashes the artboard; a parent at scale(0)
* when the view mounts leaves it blank after the scale returns to 1.
*
* Run on an iPhone 13 mini simulator. All three artboards should keep the
* same aspect ratio, including after Remount and Resize.
*/

const W = 100;
const H = 148;
const ZERO_SCALE_HOLD_MS = 1500;

function Cell({
label,
transform,
file,
resized,
}: {
label: string;
transform: ViewStyle['transform'];
file: RiveFile;
resized: boolean;
}) {
const size = resized ? styles.resized : styles.size;
return (
<View style={styles.cell}>
<Text style={styles.cellLabel}>{label}</Text>
<View style={[styles.frame, size, { transform }]}>
<RiveView file={file} fit={Fit.Contain} style={size} />
</View>
</View>
);
}

export default function Issue393DisplayZoom() {
const { riveFile, error } = useRiveFile(
require('../../assets/rive/rewards.riv')
);
const [generation, setGeneration] = useState(0);
const [zeroScaled, setZeroScaled] = useState(true);
const [resized, setResized] = useState(false);

useEffect(() => {
setZeroScaled(true);
const timer = setTimeout(() => setZeroScaled(false), ZERO_SCALE_HOLD_MS);
return () => clearTimeout(timer);
}, [generation, riveFile]);

return (
<View style={styles.container}>
<Text style={styles.subtitle}>
generation {generation} ·{' '}
{zeroScaled ? 'scale(0) holding' : 'scale(0) released'}
</Text>
{error ? <Text style={styles.error}>{String(error)}</Text> : null}
{riveFile ? (
<View style={styles.row} key={generation}>
<Cell
label="control"
transform={[]}
file={riveFile}
resized={resized}
/>
<Cell
label="rotate 14°"
transform={[{ rotate: '14deg' }]}
file={riveFile}
resized={resized}
/>
<Cell
label="scale 0→1"
transform={[{ scale: zeroScaled ? 0 : 1 }]}
file={riveFile}
resized={resized}
/>
</View>
) : (
<Text style={styles.subtitle}>loading…</Text>
)}
<Pressable
style={styles.button}
onPress={() => setGeneration((g) => g + 1)}
>
<Text style={styles.buttonText}>Remount</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => setResized((r) => !r)}>
<Text style={styles.buttonText}>
{resized ? 'Restore size' : 'Resize'}
</Text>
</Pressable>
</View>
);
}

Issue393DisplayZoom.metadata = {
name: 'Issue #393: Display Zoom drawable',
description:
'iOS RiveView squashed or blank under a rotated or zero-scaled parent',
} satisfies Metadata;

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff', padding: 12 },
subtitle: { fontSize: 12, color: '#666', textAlign: 'center', margin: 8 },
error: { color: 'red', textAlign: 'center' },
row: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 24,
},
cell: { alignItems: 'center' },
cellLabel: { fontSize: 12, marginBottom: 16 },
frame: { backgroundColor: '#dde6ff' },
size: { width: W, height: H },
resized: { width: W * 1.3, height: H * 0.7 },
button: {
marginTop: 40,
alignSelf: 'center',
backgroundColor: '#007AFF',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
buttonText: { color: '#fff', fontWeight: '600' },
});
47 changes: 47 additions & 0 deletions ios/new/RiveReactNativeView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import RiveRuntime
import NitroModules
import UIKit
import MetalKit

enum BindData {
case none
Expand All @@ -23,6 +24,7 @@
@MainActor
class RiveReactNativeView: UIView {
private var riveUIView: RiveUIView?
private var mtkViewObservation: NSKeyValueObservation?
private var riveInstance: RiveRuntime.Rive?
private var pendingBindInstance: ViewModelInstance?
private var viewReadyContinuations: [CheckedContinuation<Bool, Never>] = []
Expand Down Expand Up @@ -98,7 +100,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 103 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 @@ -210,8 +212,43 @@
throw RuntimeError.error(withMessage: "Text runs are not supported by the new runtime — use data binding")
}

override func layoutSubviews() {
super.layoutSubviews()
syncDrawableSize()
}

override func didMoveToWindow() {
super.didMoveToWindow()
syncDrawableSize()
}

// MARK: - Internal

private var riveMTKView: MTKView? {
riveUIView?.subviews.lazy.compactMap { $0 as? MTKView }.first
}

/// MTKView's automatic resize derives per-axis factors through ancestor
/// transforms when `nativeScale != scale` (Display Zoom, mini and Plus
/// iPhones), which squashes the artboard under a rotated parent and blanks
/// it under a zero-scale one (#393). Size the drawable from the view's bounds.
private func syncDrawableSize() {
guard let riveUIView, let mtkView = riveMTKView,
let nativeScale = window?.windowScene?.screen.nativeScale
else { return }
mtkView.autoResizeDrawable = false
riveUIView.layoutIfNeeded()
let pointSize = mtkView.bounds.size
guard pointSize.width > 0, pointSize.height > 0 else { return }
let size = CGSize(
width: (pointSize.width * nativeScale).rounded(),
height: (pointSize.height * nativeScale).rounded()
)
if mtkView.drawableSize != size {
mtkView.drawableSize = size
}
}

private func setupRiveUIView(with rive: RiveRuntime.Rive) {
if let existing = riveUIView {
// Reuse the existing view — avoids tearing down the MTKView on every
Expand All @@ -234,13 +271,23 @@
uiView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
self.riveUIView = uiView
// RiveUIView adds its MTKView asynchronously. This fires before the MTKView
// reaches the window, where it would compute the broken resize factors.
mtkViewObservation = uiView.layer.observe(\.sublayers) { [weak self] _, _ in
MainActor.assumeIsolated {
guard let self else { return }
self.riveMTKView?.autoResizeDrawable = false
self.setNeedsLayout()
}
}
}
}

private func cleanup() {
dispatchPrecondition(condition: .onQueue(.main))
configTask?.cancel()
configTask = nil
mtkViewObservation = nil
riveUIView?.removeFromSuperview()
riveUIView = nil
riveInstance = nil
Expand Down
Loading