Blog Post

How to Bridge Native iOS Code to React Native

Illustration: How to Bridge Native iOS Code to React Native
Information

This article was first published in January 2020 and was updated in December 2023.

Our React Native PDF library exposes a lot of JavaScript APIs, making the implementation of most use cases for PSPDFKit a breeze. However, sometimes there are certain iOS APIs that are difficult to port to React Native, mainly because of the design pattern, paradigm, and philosophy differences between iOS and React Native development.

In this tutorial, we’ll discuss how to implement a complex use case in a React Native project by bridging native Objective-C code.

You can also watch our companion video to this blog tutorial to see how we implemented it in a React Native app.

Why Bridge Native Code?

In our React Native library, we expose the PSPDFKitView object, which is the main native UI component and which uses an iOS view controller under the hood. In PSPDFKitView, we abstract away a lot of the internal functionality and APIs of the underlying PSPDFViewController to offer a better React Native JavaScript API.

Where some use cases are concerned, it’s a lot easier and more efficient to implement these APIs directly in Objective-C rather than exposing them as new JavaScript APIs. If you’d like to learn more, read our blog on how to extend React Native APIs.

Prerequisites

This post assumes you’re familiar with React Native and that you’ve already integrated PSPDFKit into your app. Otherwise, you can use our Catalog sample project from our React Native library.

The Use Case

In this tutorial, we’ll add a custom button to the annotation toolbar, and this button will remove all annotations from the current document. In the video below, you can see it in action.

So, let’s get started!

Step 1: Getting the Sample Code

The native Catalog app comes with a lot of runnable Swift examples. Our Catalog sample project can be found in the Examples directory of the PSPDFKit DMG download, which is where you’ll find the Swift sample code from CustomButtonAnnotationToolbarExample.swift that we’ll bridge over to our React Native app.

find-objc-catalog-example-xcode

If you’re an existing customer, download PSPDFKit for iOS from the PSPDFKit Portal. Otherwise, if you don’t already have PSPDFKit, download our free trial.

Step 2: Modifying the Objective-C Code in the React Native Plugin

For our use case, we’ll only modify the RCTPSPDFKitView.m class, which is the Objective-C representation of our React Native UI component, PSPDFKitView.

find-objc-catalog-example-xcode

First, we add the custom annotation toolbar’s interface at the top of RCTPSPDFKitView.m:

// Custom annotation toolbar subclass that adds a Clear button that removes all visible annotations.
@interface CustomButtonAnnotationToolbar : PSPDFAnnotationToolbar

@property (nonatomic) PSPDFToolbarButton *clearAnnotationsButton;

@end

Then, using the Swift code as reference, we add the custom annotation toolbar’s implementation at the bottom of RCTPSPDFKitView.m:

@implementation CustomButtonAnnotationToolbar

#pragma mark - Lifecycle

- (instancetype)initWithAnnotationStateManager:(PSPDFAnnotationStateManager *)annotationStateManager {
  if ((self = [super initWithAnnotationStateManager:annotationStateManager])) {
    // The biggest challenge here isn't the Clear button, but rather correctly updating the Clear button's states.
    NSNotificationCenter *dnc = NSNotificationCenter.defaultCenter;
    [dnc addObserver:self selector:@selector(annotationChangedNotification:) name:PSPDFAnnotationChangedNotification object:nil];
    [dnc addObserver:self selector:@selector(annotationChangedNotification:) name:PSPDFAnnotationsAddedNotification object:nil];
    [dnc addObserver:self selector:@selector(annotationChangedNotification:) name:PSPDFAnnotationsRemovedNotification object:nil];

    // We could also use the delegate, but this is cleaner.
    [dnc addObserver:self selector:@selector(willShowSpreadViewNotification:) name:PSPDFDocumentViewControllerWillBeginDisplayingSpreadViewNotification object:nil];

    // Add Clear button.
    UIImage *clearImage = [[PSPDFKitGlobal imageNamed:@"trash"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    _clearAnnotationsButton = [PSPDFToolbarButton new];
    _clearAnnotationsButton.accessibilityLabel = @"Clear";
    [_clearAnnotationsButton setImage:clearImage];
    [_clearAnnotationsButton addTarget:self action:@selector(clearButtonPressed:) forControlEvents:UIControlEventTouchUpInside];

    [self updateClearAnnotationButton];
    self.additionalButtons = @[_clearAnnotationsButton];

    // Hide the callout and the signature buttons from the annotation toolbar.
    NSMutableArray <PSPDFAnnotationToolbarConfiguration *> *toolbarConfigurations = [NSMutableArray<PSPDFAnnotationToolbarConfiguration *> new];;
    for(PSPDFAnnotationToolbarConfiguration *toolbarConfiguration in self.configurations) {
      NSMutableArray<PSPDFAnnotationGroup *> *filteredGroups = [NSMutableArray<PSPDFAnnotationGroup *> new];
      for (PSPDFAnnotationGroup *group in toolbarConfiguration.annotationGroups) {
        NSMutableArray<PSPDFAnnotationGroupItem *> *filteredItems = [NSMutableArray<PSPDFAnnotationGroupItem *> new];
        for(PSPDFAnnotationGroupItem *item in group.items) {
          BOOL isCallout = [item.variant isEqualToString:PSPDFAnnotationVariantStringFreeTextCallout];
          BOOL isSignature = [item.type isEqualToString:PSPDFAnnotationStringSignature];
          if (!isCallout && !isSignature) {
            [filteredItems addObject:item];
          }
        }
        if (filteredItems.count) {
          [filteredGroups addObject:[PSPDFAnnotationGroup groupWithItems:filteredItems]];
        }
      }
      [toolbarConfigurations addObject:[[PSPDFAnnotationToolbarConfiguration alloc] initWithAnnotationGroups:filteredGroups]];
    }

    self.configurations = [toolbarConfigurations copy];
  }
  return self;
}

- (void)dealloc {
  [NSNotificationCenter.defaultCenter removeObserver:self];
}

///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Clear Button Action

- (void)clearButtonPressed:(id)sender {
  // Iterate over all visible pages and remove everything but links and widgets (forms).
  PSPDFViewController *pdfController = self.annotationStateManager.pdfController;
  PSPDFDocument *document = pdfController.document;
  for (PSPDFPageView *pageView in pdfController.visiblePageViews) {
    NSArray<PSPDFAnnotation *> *annotations = [document annotationsForPageAtIndex:pageView.pageIndex type:PSPDFAnnotationTypeAll & ~(PSPDFAnnotationTypeLink | PSPDFAnnotationTypeWidget)];
    [document removeAnnotations:annotations options:nil];

    // Remove any annotation on the page as well (updates views).
    // Alternatively, you can call `reloadData` on the `pdfController` as well.
    for (PSPDFAnnotation *annotation in annotations) {
      [pageView removeAnnotation:annotation options:nil animated:YES];
    }
  }
}

///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Notifications

// If we detect annotation changes, schedule a reload.
- (void)annotationChangedNotification:(NSNotification *)notification {
  // Reevaluate toolbar button.
  if (self.window) {
    [self updateClearAnnotationButton];
  }
}

- (void)willShowSpreadViewNotification:(NSNotification *)notification {
  [self updateClearAnnotationButton];
}

///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - PSPDFAnnotationStateManagerDelegate

- (void)annotationStateManager:(PSPDFAnnotationStateManager *)manager didChangeUndoState:(BOOL)undoEnabled redoState:(BOOL)redoEnabled {
  [super annotationStateManager:manager didChangeUndoState:undoEnabled redoState:redoEnabled];
  [self updateClearAnnotationButton];
}

///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Private

- (void)updateClearAnnotationButton {
  __block BOOL annotationsFound = NO;
  PSPDFViewController *pdfController = self.annotationStateManager.pdfController;
  [pdfController.visiblePageIndexes enumerateIndexesUsingBlock:^(NSUInteger pageIndex, BOOL *stop) {
    NSArray<PSPDFAnnotation *> *annotations = [pdfController.document annotationsForPageAtIndex:pageIndex type:PSPDFAnnotationTypeAll & ~(PSPDFAnnotationTypeLink | PSPDFAnnotationTypeWidget)];
    if (annotations.count > 0) {
      annotationsFound = YES;
      *stop = YES;
    }
  }];
  self.clearAnnotationsButton.enabled = annotationsFound;
}

@end

Finally, we override the annotation toolbar’s class. This is the step where we tell PSPDFKit to use our custom annotation toolbar instead of the default one by updating PSPDFViewController’s configuration:

— (instancetype)initWithFrame:(CGRect)frame {
  if ((self = [super initWithFrame:frame])) {
    // Set configuration to use the custom annotation toolbar when initializing the `PSPDFViewController`.
    // For more details, see our documentation: https://pspdfkit.com/guides/ios/customizing-the-interface/customize-the-annotation-toolbar/
-    _pdfController = [[RCTPSPDFKitViewController alloc] init];
+    _pdfController = [[PSPDFViewController alloc] initWithDocument:nil configuration:[PSPDFConfiguration
+    configurationWithBuilder:^(PSPDFConfigurationBuilder *builder) {
+     [builder overrideClass:PSPDFAnnotationToolbar.class withClass:CustomButtonAnnotationToolbar.class];
+    }]];

    // Store the `closeButton`'s target and selector in order to call it later.
    _closeButtonAttributes = @{@"target" : _pdfController.closeButtonItem.target,
                              @"action" : NSStringFromSelector(_pdfController.closeButtonItem.action)};

    [_pdfController.closeButtonItem setTarget:self];
    [_pdfController.closeButtonItem setAction:@selector(closeButtonPressed:)];
    _closeButton = _pdfController.closeButtonItem;

    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(annotationChangedNotification:) name:PSPDFAnnotationChangedNotification object:nil];
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(annotationChangedNotification:) name:PSPDFAnnotationsAddedNotification object:nil];
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(annotationChangedNotification:) name:PSPDFAnnotationsRemovedNotification object:nil];

    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(spreadIndexDidChange:) name:PSPDFDocumentViewControllerSpreadIndexDidChangeNotification object:nil];
  }

  return self;
}

That’s all! The custom delete button is now part of the annotation toolbar of our React Native app!

You can find the modified version of RCTPSPDFKitView.m here, and you can also check out the customize-annotation-toolbar-blog branch and run the Catalog example on your device.

Conclusion

This post covered how to bridge native iOS code to React Native. I hope that this tutorial will help you bridge Objective-C code to React Native so you can implement more complex use cases in your app.

We also offer a React Native PDF SDK that helps developers work with documents in their applications. Our SDK provides PDF viewing, annotating, form filling, editing and much more. Learn how our customers use our PDF SDK, explore our web demo, or start your free trial.

Related Products
PSPDFKit for React Native

Product Page
Guides
Example Projects

Share Post
Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
DEVELOPMENT  |  iOS • Swift • Tips

Privacy Manifests and Required Reason APIs on iOS

PRODUCTS  |  iOS • Releases

PSPDFKit 13.4 for iOS Introduces Revamped API Documentation and Improves Multiple Annotation Selection

DEVELOPMENT  |  visionOS • iOS

Apple’s Vision of Our Digital Future