diff --git a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm index f1669805..5a4c0c6b 100644 --- a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm +++ b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm @@ -30,6 +30,15 @@ + (BOOL)shouldBeRecycled; @implementation HybridRiveViewComponent { std::shared_ptr _hybridView; + // Fabric can recreate this component view from an unchanged ShadowNode + // (e.g. react-freeze / Suspense re-inserting a previously hidden screen). + // The cached props' isDirty flags were already consumed by the previous + // view instance (they live on the shared Props object and are mutated on + // first apply), so updateProps would apply nothing and the fresh + // HybridRiveView would stay unconfigured: no file, no artboard, and a + // hybridRef that never fires (JS keeps a ref to the dead old hybrid). + // Force-apply every prop on this instance's first updateProps. + BOOL _didApplyInitialProps; } + (void) load { @@ -69,61 +78,66 @@ - (void) updateProps:(const std::shared_ptr&)props auto& newViewProps = const_cast(newViewPropsConst); RNRive::HybridRiveViewSpec_cxx& swiftPart = _hybridView->getSwiftPart(); + // Force-apply all props the first time this view instance updates (see + // _didApplyInitialProps above). + BOOL force = !_didApplyInitialProps; + _didApplyInitialProps = YES; + // 2. Update each prop individually swiftPart.beforeUpdate(); // artboardName: optional - if (newViewProps.artboardName.isDirty) { + if (force || newViewProps.artboardName.isDirty) { swiftPart.setArtboardName(newViewProps.artboardName.value); newViewProps.artboardName.isDirty = false; } // stateMachineName: optional - if (newViewProps.stateMachineName.isDirty) { + if (force || newViewProps.stateMachineName.isDirty) { swiftPart.setStateMachineName(newViewProps.stateMachineName.value); newViewProps.stateMachineName.isDirty = false; } // autoPlay: optional - if (newViewProps.autoPlay.isDirty) { + if (force || newViewProps.autoPlay.isDirty) { swiftPart.setAutoPlay(newViewProps.autoPlay.value); newViewProps.autoPlay.isDirty = false; } // file: hybrid-object - if (newViewProps.file.isDirty) { + if (force || newViewProps.file.isDirty) { swiftPart.setFile(newViewProps.file.value); newViewProps.file.isDirty = false; } // alignment: optional - if (newViewProps.alignment.isDirty) { + if (force || newViewProps.alignment.isDirty) { swiftPart.setAlignment(newViewProps.alignment.value); newViewProps.alignment.isDirty = false; } // fit: optional - if (newViewProps.fit.isDirty) { + if (force || newViewProps.fit.isDirty) { swiftPart.setFit(newViewProps.fit.value); newViewProps.fit.isDirty = false; } // layoutScaleFactor: optional - if (newViewProps.layoutScaleFactor.isDirty) { + if (force || newViewProps.layoutScaleFactor.isDirty) { swiftPart.setLayoutScaleFactor(newViewProps.layoutScaleFactor.value); newViewProps.layoutScaleFactor.isDirty = false; } // frameRate: optional - if (newViewProps.frameRate.isDirty) { + if (force || newViewProps.frameRate.isDirty) { swiftPart.setFrameRate(newViewProps.frameRate.value); newViewProps.frameRate.isDirty = false; } // semantics: optional - if (newViewProps.semantics.isDirty) { + if (force || newViewProps.semantics.isDirty) { swiftPart.setSemantics(newViewProps.semantics.value); newViewProps.semantics.isDirty = false; } // dataBind: optional - if (newViewProps.dataBind.isDirty) { + if (force || newViewProps.dataBind.isDirty) { swiftPart.setDataBind(newViewProps.dataBind.value); newViewProps.dataBind.isDirty = false; } // onError: function - if (newViewProps.onError.isDirty) { + if (force || newViewProps.onError.isDirty) { swiftPart.setOnError(newViewProps.onError.value); newViewProps.onError.isDirty = false; } @@ -131,7 +145,7 @@ - (void) updateProps:(const std::shared_ptr&)props swiftPart.afterUpdate(); // 3. Update hybridRef if it changed - if (newViewProps.hybridRef.isDirty) { + if (force || newViewProps.hybridRef.isDirty) { // hybridRef changed - call it with new this const auto& maybeFunc = newViewProps.hybridRef.value; if (maybeFunc.has_value()) { diff --git a/scripts/nitrogen-postprocess.ts b/scripts/nitrogen-postprocess.ts index 0cec6394..7d08cf56 100644 --- a/scripts/nitrogen-postprocess.ts +++ b/scripts/nitrogen-postprocess.ts @@ -10,6 +10,10 @@ const COMPONENT_FILE = join( ROOT, 'nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp' ); +const IOS_COMPONENT_FILE = join( + ROOT, + 'nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm' +); function makeHybridRiveViewManagerOpen() { if (!existsSync(MANAGER_FILE)) { @@ -44,6 +48,10 @@ function acceptNullForOptionalProps() { } const content = readFileSync(COMPONENT_FILE, 'utf-8'); + if (content.includes('value.isNull()')) { + console.log('HybridRiveViewComponent.cpp already accepts null props'); + return; + } const pattern = /^( *)return (CachedProp>)::fromRawValue\(\*runtime, value, (sourceProps\.\w+)\);$/gm; const updated = content.replace( @@ -53,13 +61,9 @@ function acceptNullForOptionalProps() { ); if (content === updated) { - if (content.includes('value.isNull()')) { - console.log('HybridRiveViewComponent.cpp already accepts null props'); - } else { - console.warn( - 'No optional CachedProp parse sites found in HybridRiveViewComponent.cpp — nitrogen output may have changed shape' - ); - } + console.warn( + 'No optional CachedProp parse sites found in HybridRiveViewComponent.cpp — nitrogen output may have changed shape' + ); return; } @@ -69,5 +73,74 @@ function acceptNullForOptionalProps() { ); } +// Fabric can recreate the iOS component view from an unchanged ShadowNode +// (e.g. react-freeze / Suspense re-inserting a previously hidden screen), but +// the cached props' isDirty flags were already consumed by the previous view +// instance (they live on the shared Props object and are mutated on first +// apply), so updateProps would apply nothing and the fresh view would stay +// unconfigured: no file, no artboard, and a hybridRef that never fires. +// Force-apply every prop on a view instance's first updateProps. +// Fixed upstream in nitro 0.37 (props are diffed against oldProps instead of +// mutating shared isDirty state) — remove on the next nitro upgrade: +// https://github.com/mrousavy/nitro/pull/1506 +const IVAR_BLOCK = ` // Fabric can recreate this component view from an unchanged ShadowNode + // (e.g. react-freeze / Suspense re-inserting a previously hidden screen). + // The cached props' isDirty flags were already consumed by the previous + // view instance (they live on the shared Props object and are mutated on + // first apply), so updateProps would apply nothing and the fresh + // HybridRiveView would stay unconfigured: no file, no artboard, and a + // hybridRef that never fires (JS keeps a ref to the dead old hybrid). + // Force-apply every prop on this instance's first updateProps. + BOOL _didApplyInitialProps; +`; + +const FORCE_BLOCK = ` + // Force-apply all props the first time this view instance updates (see + // _didApplyInitialProps above). + BOOL force = !_didApplyInitialProps; + _didApplyInitialProps = YES; +`; + +function forceApplyPropsOnFirstUpdate() { + if (!existsSync(IOS_COMPONENT_FILE)) { + console.warn('HybridRiveViewComponent.mm not found, skipping'); + return; + } + + const content = readFileSync(IOS_COMPONENT_FILE, 'utf-8'); + if (content.includes('_didApplyInitialProps')) { + console.log( + 'HybridRiveViewComponent.mm already force-applies initial props' + ); + return; + } + + const ivarAnchor = + ' std::shared_ptr _hybridView;\n'; + const updateAnchor = '\n // 2. Update each prop individually\n'; + const dirtyPattern = /if \(newViewProps\.(\w+)\.isDirty\)/g; + if ( + !content.includes(ivarAnchor) || + !content.includes(updateAnchor) || + !dirtyPattern.test(content) + ) { + console.warn( + 'Anchors for the force-apply patch not found in HybridRiveViewComponent.mm — nitrogen output may have changed shape' + ); + return; + } + + const updated = content + .replace(ivarAnchor, `${ivarAnchor}${IVAR_BLOCK}`) + .replace(updateAnchor, `${FORCE_BLOCK}${updateAnchor}`) + .replace(dirtyPattern, 'if (force || newViewProps.$1.isDirty)'); + + writeFileSync(IOS_COMPONENT_FILE, updated); + console.log( + 'Patched HybridRiveViewComponent.mm to force-apply props on first update' + ); +} + makeHybridRiveViewManagerOpen(); acceptNullForOptionalProps(); +forceApplyPropsOnFirstUpdate();