Blog Post

How to Extend React Native APIs

Information

This article was first published in August 2018 and was updated in December 2023.

We recently introduced the React Native UI component for iOS, along with one for Android, and today we’re excited to announce that we’ve further extended the APIs of our React Native library and added new Catalog examples. In version 1.20.0 of our React Native library, we added many new features — like manual saving, the ability to programmatically manipulate annotations and forms, and so much more! The building blocks of these newly added features are React Native props, events, and functions.

In our native SDKs, we expose a lot of APIs for full customization, but we only use a subset of those APIs in our React Native library. In this article, we’ll show you how to bring native iOS APIs to React Native to make it easier for everyone to expose native iOS code to React Native in general.

So let’s get started!

Information

If you’re looking into extending our Android SDK API to React Native, have a look at our Advanced Techniques for React Native UI Components blog post.

Props

Props are parameters that allow you to customize UI components. In the example below, we added the disableAutomaticSaving prop, which allows disabling automatic saving for the presented document.

First, we define the prop in index.js, which is where PSPDFKitView, our React Native UI component, is defined:

// index.js
class PSPDFKitView extends React.Component {
	render() {
		return <RCTPSPDFKitView ref="pdfView" {...this.props} />;
	}
}

PSPDFKitView.propTypes = {
	/**
	 * Controls whether or not the document will automatically be saved.
	 * Defaults to automatically saving (`false`).
	 * @type {boolean}
	 * @memberof PSPDFKitView
	 */
	disableAutomaticSaving: PropTypes.bool,
};

We then expose the prop as an Objective-C Boolean property, like so:

// RCTPSPDFKitView.h
@interface RCTPSPDFKitView: UIView

@property (nonatomic) BOOL disableAutomaticSaving;

@end

Then, we use it in our Objective-C logic. In this case, we use it in pdfViewController:shouldSaveDocument:withOptions:, which is a delegate method that should return true when autosave is enabled and false when it isn’t:

// RCTPSPDFKitView.m
@implementation RCTPSPDFKitView

#pragma mark - PSPDFViewControllerDelegate

- (BOOL)pdfViewController:(PSPDFViewController *)pdfController shouldSaveDocument:(nonnull PSPDFDocument *)document withOptions:(NSDictionary<PSPDFDocumentSaveOption,id> *__autoreleasing  _Nonnull * _Nonnull)options {
  return !self.disableAutomaticSaving;
}
@end

To expose the Objective-C property to React Native (make it available in JavaScript), we use the RCT_EXPORT_VIEW_PROPERTY macro, like this:

// RCTPSPDFKitViewManager.m
@implementation RCTPSPDFKitViewManager

RCT_EXPORT_MODULE()

RCT_EXPORT_VIEW_PROPERTY(disableAutomaticSaving, BOOL)

@end

Once we’ve completed the above, this is how to use the newly added prop in JavaScript code:

// Catalog.tsx

class ManualSave extends React.Component {
	render() {
		return (
			<View style={{ flex: 1 }}>
				<PSPDFKitView
					ref="pdfView"
					document="PDFs/Annual Report.pdf"
					disableAutomaticSaving={true}
				/>
			</View>
		);
	}
}
Information

For details, see the official React Native properties documentation.

Events

Events, or callbacks, allow us to get notified in JavaScript when something occurs in native Objective-C code. Take a look at the official documentation for more information about React Native events.

Events should be prefixed with “on,” so in our case, we implemented onDocumentSaved, which is called when the document is saved. In PSPDFKit for iOS, we have pdfDocumentDidSave:, which is a delegate called every time the document is saved.

Since we want to be notified in JavaScript when a PDF is saved, we define the new event in index.js, similarly to how we did it with the prop before, like so:

// index.js
class PSPDFKitView extends React.Component {
	render() {
		return (
			<RCTPSPDFKitView
				ref="pdfView"
				{...this.props}
				onDocumentSaved={this._onDocumentSaved}
			/>
		);
	}

	_onDocumentSaved = (event) => {
		if (this.props.onDocumentSaved) {
			this.props.onDocumentSaved(event.nativeEvent);
		}
	};
}

In Objective-C, in RCTPSPDFKitView.h and RCTPSPDFKitViewManager.m, we expose the event as a view property (RCT_EXPORT_VIEW_PROPERTY) of type RCTBubblingEventBlock:

// RCTPSPDFKitView.h
@interface RCTPSPDFKitView: UIView

@property (nonatomic, copy) RCTBubblingEventBlock onDocumentSaved;

@end
// RCTPSPDFKitViewManager.m
@implementation RCTPSPDFKitViewManager

RCT_EXPORT_MODULE()

RCT_EXPORT_VIEW_PROPERTY(onDocumentSaved, RCTBubblingEventBlock)

@end

Then we set the document’s delegate when the new document is created:

// RCTPSPDFKitViewManager.m
@implementation RCTPSPDFKitViewManager

RCT_EXPORT_MODULE()

RCT_CUSTOM_VIEW_PROPERTY(document, pdfController.document, RCTPSPDFKitView) {
  if (json) {
    view.pdfController.document = [RCTConvert PSPDFDocument:json];
    // Set the delegate of the newly created document.
    view.pdfController.document.delegate = (id<PSPDFDocumentDelegate>)view;

    //...
}

@end

Now, in RCTPSPDFKitView.m, we implement pdfDocumentDidSave: by invoking self.onDocumentSaved(@{}) with its return payload. In this case, the event will return an empty dictionary:

// RCTPSPDFKitView.m
@implementation RCTPSPDFKitView

#pragma mark - PSPDFDocumentDelegate

- (void)pdfDocumentDidSave:(nonnull PSPDFDocument *)document {
  if (self.onDocumentSaved) {
    self.onDocumentSaved(@{});
  }
}

@end

This is how the event is used in React Native JavaScript code:

// Catalog.tsx

class EventListeners extends Component {
	render() {
		return (
			<View style={{ flex: 1 }}>
				<PSPDFKitView
					document={'PDFs/Annual Report.pdf'}
					onDocumentSaved={(event) => {
						alert('Document Saved!');
					}}
				/>
			</View>
		);
	}
}

Methods

The official React Native documentation covers props and events in detail, but there isn’t much mentioned about how to call a native method from React Native. We spent quite a bit of time trying to figure this one out; we went through some trial and error, and we looked at how other popular UI components are implemented.

Calling a Method

Unlike with props and events, you can’t use this.prop.doSomething(). Rather, you need to use the native module to access PSPDFKitViewManager to ultimately call its method.

Here’s how we implemented saveCurrentDocument, a method that allows manually saving a document:

// index.js
class PSPDFKitView extends React.Component {
	render() {
		return <RCTPSPDFKitView ref="pdfView" {...this.props} />;
	}

	/**
	 * Saves the document that's currently open.
	 * @method saveCurrentDocument
	 * @memberof PSPDFKitView
	 * @example
	 * const result = await this.pdfRef.current.saveCurrentDocument();
	 *
	 * @returns { Promise<boolean> } A promise resolving to `true` if the document was saved, and `false` if not.
	 */
	saveCurrentDocument = function () {
		NativeModules.PSPDFKitViewManager.saveCurrentDocument(
			findNodeHandle(this.refs.pdfView),
		);
	};
}

From here on, the implementation of saveCurrentDocument looks similar to the implementation of props and events:

// RCTPSPDFKitView.h
@interface RCTPSPDFKitView: UIView

- (void)saveCurrentDocument;

@end
// RCTPSPDFKitView.m
@implementation RCTPSPDFKitView

- (void)saveCurrentDocument {
  [self.pdfController.document saveWithOptions:nil error:NULL];
}

@end
// RCTPSPDFKitViewManager.m
@implementation RCTPSPDFKitViewManager

RCT_EXPORT_METHOD(saveCurrentDocument:(nonnull NSNumber *)reactTag) {
  dispatch_async(dispatch_get_main_queue(), ^{
    RCTPSPDFKitView *component = (RCTPSPDFKitView *)[self.bridge.uiManager viewForReactTag:reactTag];
    [component saveCurrentDocument];
  });
}

@end

This is how we call saveCurrentDocument from React Native when pressing a “Save” button:

// Catalog.tsx

<View>
	<Button
		onPress={() => {
			// Manual Save
			this.pdfRef.current?.saveCurrentDocument();
		}}
		title="Save"
	/>
</View>

Calling a Method with Parameters and a Return Value

Calling a method with a return value is similar to calling a method. We just need to make sure to call return when invoking the native module method, as seen below:

// index.js
class PSPDFKitView extends React.Component {
	render() {
		return <RCTPSPDFKitView ref="pdfView" {...this.props} />;
	}

	/**
	 * Gets all annotations of the given type from the specified page.
	 *
	 * @method getAnnotations
	 * @memberof PSPDFKitView
	 * @param { number } pageIndex The page index to get the annotations for, starting at 0.
	 * @param { string } [type] The type of annotations to get. If not specified or `null`, all annotation types will be returned.
	 * @example
	 * const result = await this.pdfRef.current.getAnnotations(3, 'pspdfkit/ink');
	 * @see {@link https://pspdfkit.com/guides/web/json/schema/annotations/} for supported types.
	 *
	 * @returns { Promise } A promise containing an object with an array of InstantJSON objects.
	 */
	getAnnotations = function (pageIndex, type) {
		return NativeModules.PSPDFKitViewManager.getAnnotations(
			pageIndex,
			type,
			findNodeHandle(this.refs.pdfView),
		);
	};
}

We also need to make sure our native methods have return values:

// RCTPSPDFKitView.h
@interface RCTPSPDFKitView: UIView

- (NSDictionary *)getAnnotations:(PSPDFPageIndex)pageIndex type:(PSPDFAnnotationType)type;

@end
// RCTPSPDFKitView.m
@implementation RCTPSPDFKitView

- (NSDictionary *)getAnnotations:(PSPDFPageIndex)pageIndex type:(PSPDFAnnotationType)type {
  NSArray <PSPDFAnnotation *>* annotations = [self.pdfController.document annotationsForPageAtIndex:pageIndex type:type];
  NSArray <NSDictionary *> *annotationsJSON = [RCTConvert instantJSONAnnotationsFromPSPDFAnnotationArray:annotations];
  return @{@"annotations" : annotationsJSON};
}

@end

In this example, we return all annotations at the given page index. This is how we implement the method in RCTPSPDFKitViewManager.m:

// RCTPSPDFKitViewManager.m
@implementation RCTPSPDFKitViewManager

RCT_REMAP_METHOD(getAnnotations, getAnnotations:(nonnull NSNumber *)pageIndex type:(NSString *)type reactTag:(nonnull NSNumber *)reactTag resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
  dispatch_async(dispatch_get_main_queue(), ^{
    RCTPSPDFKitView *component = (RCTPSPDFKitView *)[self.bridge.uiManager viewForReactTag:reactTag];
    NSDictionary *annotations = [component getAnnotations:(PSPDFPageIndex)pageIndex.integerValue type:[RCTConvert instantJSONAnnotationType:type]];
    if (annotations) {
      resolve(annotations);
    } else {
      reject(@"error", @"Failed to get annotations", nil);
    }
  });
}

@end

Note that we used the RCTPromiseResolveBlock and RCTPromiseRejectBlock promise blocks. The resolve block returns the annotations, while the reject block fails if an error occurs.

In the example below, we show an alert with the payload from getAnnotations() in JavaScript:

// Catalog.tsx

<View>
	<Button
		onPress={async () => {
			// Get ink annotations from the current page.
			const annotations = await this.pdfRef.current?.getAnnotations(
				this.state.currentPageIndex,
				'pspdfkit/ink',
			);
			alert(JSON.stringify(annotations));
		}}
		title="getAnnotations"
	/>
</View>

Customizing the UI Using Native Code

Sometimes, making native APIs available to React Native doesn’t really make sense for a specific use case — for example, when exposing the annotation toolbar buttons. This is technically feasible, but there’s more to it than just exposing the button. One needs to expose the native button object and its properties, along with all the related callbacks and delegates.

If you have such a use case, we recommend doing it directly in Objective-C, as this is easier. Sometimes it’s just a matter of reusing (aka copying and pasting) existing code from our examples in our Catalog sample project and slightly modifying the code to fit your specific needs. In this example, we’re adding a “Clear All” annotations button to the annotation toolbar and reusing the same code from CustomizeAnnotationToolbarExample.swift from the Catalog app in RCTPSPDFKitView.m and RCTPSPDFKitViewManager.m. To see it in action, please check out the customize-the-toolbar-in-native-code branch and run the Catalog example on your device.

Conclusion

Hopefully this blog post helped you learn some new tips and tricks for exposing iOS code to React Native. If you have any questions about PSPDFKit for React Native, please don’t hesitate to reach out to us. We’re happy to help.

Explore related topics

Related products

PSPDFKit for React Native

Product page Guides Example Projects

Share post
Free trial Ready to get started?
Free trial