From 7b7a1597be807e5b4a687fca580ca76eb4025ad8 Mon Sep 17 00:00:00 2001 From: Erhard Brand <141146947+erhard-pspdfkit@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:33:18 +0200 Subject: [PATCH] Revert "fix(ios): preserve unsaved annotations when switching annotation tools (Fabric)" --- ios/Fabric/NutrientView.mm | 13 +- samples/Catalog/Catalog.tsx | 5 - samples/Catalog/ExamplesNavigationMenu.tsx | 11 -- .../examples/SwitchAnnotationTools.tsx | 171 ------------------ 4 files changed, 2 insertions(+), 198 deletions(-) delete mode 100644 samples/Catalog/examples/SwitchAnnotationTools.tsx diff --git a/ios/Fabric/NutrientView.mm b/ios/Fabric/NutrientView.mm index aaf7573c..8d2cd831 100644 --- a/ios/Fabric/NutrientView.mm +++ b/ios/Fabric/NutrientView.mm @@ -319,7 +319,6 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & _view.componentID = [self.nativeId integerValue]; auto newProps = std::static_pointer_cast(props); - auto oldViewProps = std::static_pointer_cast(oldProps); if (newProps != nullptr) { // Parse configuration first so remoteDocumentConfiguration is available when applying document NSDictionary *jsonConfig = nil; @@ -328,16 +327,8 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & jsonConfig = [NutrientFabricUtils dictionaryFromJSONString:_configurationJSONString]; } - // Basic document props. - // - // Only (re)apply the document when the path actually changes. Fabric calls - // updateProps with the full prop set on every re-render, so applying the - // document unconditionally here would create a fresh PSPDFDocument and - // reload the controller on *any* prop change (e.g. a dynamic - // menuItemGrouping driven by React state), discarding unsaved in-memory - // annotations. Mirrors the document-identity guard used on Android. - BOOL documentChanged = (oldViewProps == nullptr) || (newProps->document != oldViewProps->document); - if (!newProps->document.empty() && documentChanged) { + // Basic document props + if (!newProps->document.empty()) { _document = RCTNSStringFromString(newProps->document); NSNumber *reference = [NSNumber numberWithInteger:[self.nativeId integerValue]]; NSDictionary *remoteConfig = jsonConfig[@"remoteDocumentConfiguration"]; diff --git a/samples/Catalog/Catalog.tsx b/samples/Catalog/Catalog.tsx index 1179798f..51d3ea9c 100644 --- a/samples/Catalog/Catalog.tsx +++ b/samples/Catalog/Catalog.tsx @@ -25,7 +25,6 @@ import { ManualSave } from './examples/ManualSave'; import Measurement from './examples/Measurement'; import { OpenImageDocument } from './examples/OpenImageDocument'; import { ProgrammaticAnnotations } from './examples/ProgrammaticAnnotations'; -import { SwitchAnnotationTools } from './examples/SwitchAnnotationTools'; import { ProgrammaticFormFilling } from './examples/ProgrammaticFormFilling'; import { NutrientViewComponent } from './examples/NutrientViewComponent'; import { SaveAs } from './examples/SaveAs'; @@ -84,10 +83,6 @@ class Catalog extends React.Component { component={OpenRemoteDocument} /> - diff --git a/samples/Catalog/ExamplesNavigationMenu.tsx b/samples/Catalog/ExamplesNavigationMenu.tsx index 7ef8decb..d75784c0 100644 --- a/samples/Catalog/ExamplesNavigationMenu.tsx +++ b/samples/Catalog/ExamplesNavigationMenu.tsx @@ -277,17 +277,6 @@ export default [ title: 'AI Assistant', }); }, - }, - { - key: 'item26', - name: 'Switch Annotation Tools', - description: - 'Switch between annotation creation tools without losing unsaved annotations. Toggle to compare a dynamic vs. static menuItemGrouping.', - action: (component: any) => { - extractFromAssetsIfMissing(exampleDocumentName, function () { - component.props.navigation.push('SwitchAnnotationTools'); - }); - }, } ]; diff --git a/samples/Catalog/examples/SwitchAnnotationTools.tsx b/samples/Catalog/examples/SwitchAnnotationTools.tsx deleted file mode 100644 index c1ee63de..00000000 --- a/samples/Catalog/examples/SwitchAnnotationTools.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React from 'react'; -import { - processColor, - Switch, - Text, - TouchableOpacity, - View, -} from 'react-native'; -import NutrientView, { Annotation } from '@nutrient-sdk/react-native'; - -import { pspdfkitColor, writableDocumentPath } from '../configuration/Constants'; -import { BaseExampleAutoHidingHeaderComponent } from '../helpers/BaseExampleAutoHidingHeaderComponent'; - -type Tool = 'ink' | 'highlight' | 'note' | 'signature'; - -const TOOL_TO_ANNOTATION: Record = { - ink: Annotation.Type.INK, - highlight: Annotation.Type.HIGHLIGHT, - note: Annotation.Type.NOTE, - signature: Annotation.Type.SIGNATURE, -}; - -// A single, stable grouping defined once at module scope. Because this -// reference and its value never change, switching tools never updates a -// NutrientView prop, so the native view is never re-configured/reloaded and -// unsaved annotations are preserved. -const STATIC_MENU_ITEM_GROUPING = ['ink', 'highlight', 'note', 'signature']; - -interface IState { - activeTool: Tool; - // When true we filter menuItemGrouping down to the active tool on every - // switch (rebuilding the prop each time — reproduces the annotation loss). - // When false we keep a single static grouping (the recommended approach). - dynamicGrouping: boolean; -} - -/** - * Switching annotation creation tools without saving. - * - * Deriving `menuItemGrouping` from React state and rebuilding it on every tool - * switch changes a NutrientView prop, which can make the native view re-apply - * its configuration / reload and discard the in-memory (unsaved) annotation. - * - * Toggle "Dynamic grouping" ON to see that behavior, OFF to use a static - * grouping (with imperative enter/exit calls) that keeps the annotation. - */ -export class SwitchAnnotationTools extends BaseExampleAutoHidingHeaderComponent { - pdfRef: React.RefObject; - - constructor(props: any) { - super(props); - this.pdfRef = React.createRef(); - this.state = { - // Default to the static grouping (recommended). Toggle the switch ON to - // see the annotation loss caused by a dynamic, state-driven grouping. - activeTool: 'ink', - dynamicGrouping: false, - }; - } - - // Customer's pattern: grouping filtered to the active tool only. - private dynamicMenuItemGrouping() { - return (['ink', 'highlight', 'note', 'signature'] as Tool[]).filter( - item => item === this.state.activeTool, - ); - } - - // Option 1 fix: always the same stable grouping (see STATIC_MENU_ITEM_GROUPING). - private staticMenuItemGrouping() { - return STATIC_MENU_ITEM_GROUPING; - } - - private async switchTool(tool: Tool) { - // Commit whatever is being drawn and leave the current mode. - await this.pdfRef.current?.exitCurrentlyActiveMode(); - this.setState({ activeTool: tool }); - setTimeout(async () => { - await this.pdfRef.current?.enterAnnotationCreationMode( - TOOL_TO_ANNOTATION[tool], - ); - }, 250); - } - - override render() { - const tools: Tool[] = ['ink', 'highlight', 'note', 'signature']; - return ( - - - {this.renderWithSafeArea(insets => ( - - - Dynamic grouping (repro) - this.setState({ dynamicGrouping: v })} - /> - - - {tools.map(tool => ( - this.switchTool(tool)}> - {tool} - - ))} - - this.pdfRef.current?.getDocument().save()}> - Save - - - ))} - - ); - } -} - -const styles = { - flex: { flex: 1 }, - pdfColor: { flex: 1, color: pspdfkitColor }, - controls: { - backgroundColor: '#f7f7f7', - paddingHorizontal: 10, - paddingTop: 8, - }, - row: { - flexDirection: 'row' as 'row', - alignItems: 'center' as 'center', - justifyContent: 'space-between' as 'space-between', - marginVertical: 6, - }, - label: { fontSize: 15, color: '#333' }, - button: { - flex: 1, - paddingVertical: 10, - marginHorizontal: 3, - borderRadius: 5, - backgroundColor: '#e6e6e6', - }, - buttonActive: { backgroundColor: '#cfe3ff' }, - saveButton: { - paddingVertical: 12, - marginVertical: 6, - borderRadius: 5, - backgroundColor: '#f0f0f0', - }, - buttonText: { - fontSize: 15, - color: pspdfkitColor, - textAlign: 'center' as 'center', - }, -};