Blog Post

How to Bridge Native iOS Code to Flutter

Illustration: How to Bridge Native iOS Code to Flutter

PSPDFKit’s Flutter SDK aims to expose a lot of our framework’s APIs to Dart. These APIs allow you to implement simple use cases in Flutter very easily. We also aimed to keep our Flutter plugin quite extensible to allow users to bridge native Objective-C code, which enables the plugin to adapt to complicated use cases.

We’ll be covering how to bridge native Objective-C code to Flutter in this blog post, but before we go ahead with the tutorial, we’ll first cover some background information on some of the Flutter SDK (Dart) and platform (Objective-C) components.

If you’d rather watch the process, head over to our Flutter video tutorial.

Dart Components Explained

In the PSPDFKit Flutter SDK, we expose two different components in Dart. Depending on your project’s requirements, you can use one or the other.

  • PspdfkitView + UiKitView (PspdfkitView component) — To display PDF files in Flutter, we use platform views provided by Flutter: On iOS, this is UiKitView. Along with the platform views, we also need to use PspdfkitView, which is a simple class that manages the communication between the platform view and Objective-C code (PspdfPlatformView). This platform view must be created with a viewType ('com.pspdfkit.widget') and attached to a new PspdfkitView after initialization so that it can take care of the communication between Flutter and the Objective-C code. We already provide a ready-to-use widget, PspdfkitWidget, which does both of these things and can be used for simple use cases. The PspdfkitView component can be composed into more complex widgets and is the recommended way to display PDF files within an already present Flutter navigation hierarchy. For more complex use cases, check out the Programmatic Form Filling Example or the Process Annotations Example in our Flutter example app.

  • Pspdfkit global plugin — To display a PDF file modally (over all current views, with a navigation bar managed by the platform), we can use the Pspdfkit global plugin. This has the benefit of being much simpler if no customization is needed. It’s still possible to update several settings during initialization time. Check out the Pspdfkit Global Plugin View Examples section of the Flutter example app to see what is possible when using the global plugin.

We recommend using the PspdfkitView component for most use cases, whereas the global plugin view should be used for scenarios where a PDF is required to be presented modally.

Platform Components Explained

Corresponding to the two Flutter SDK components described above are their platform counterparts, which are written in Objective-C: PspdfPlatformView.m and PspdfkitPlugin.m.

PspdfPlatformView.m is an implementation of Flutter’s FlutterPlatformView protocol, and it represents both the main view used to display PDF files and its related view controller. It also manages its own navigation stack, which is set up by default to toggle the navigation bar on and off when tapping.

PspdfPlatformViews are created when required by PspdfPlatformViewFactory, which is an implementation of Flutter’s FlutterPlatformViewFactory protocol. The FlutterPlatformViewFactory protocol requires us to implement the following:

- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
                                   viewIdentifier:(int64_t)viewId
                                        arguments:(id _Nullable)args;

In the above method, we create new PspdfPlatformViews as required by the factory and return them to be used on the Flutter side. For each new PspdfPlatformView we create, we also create a unique FlutterMethodChannel with the viewId so that we can send data back and forth from the Flutter side.

PspdfkitPlugin.m is an implementation of Flutter’s FlutterPlugin protocol, and it manages a single global PSPDFViewController that can be used to modally show one PDF file at a time. The plugin takes care of implementing these methods:

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar;

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;

The methods above are used to register the view factory and the global FlutterMethodChannel, which is used to send asynchronous method calls between Flutter and the platform code.

Now that we’ve covered the basics of the Dart and platform components used in the Flutter widget, let’s get started with the tutorial!

Prerequisites

This post assumes you’re familiar with Flutter and that you have already integrated PSPDFKit into your app. Otherwise, you can use our example project from our Flutter open source repository to get set up.

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.

Step 1: Get the Objective-C Sample Code

The native PSPDFCatalog app comes with a lot of runnable Objective-C examples. Our Catalog sample project can be found in the Examples directory of the PSPDFKit DMG download, which is where you’ll find the Objective-C sample code from PSCAnnotationToolbarButtonsExample.m that we’ll bridge over to our Flutter app.

find-objc-catalog-example-xcode

If you’re an existing customer, download PSPDFKit for iOS from the customer portal. Otherwise, if you don’t already have PSPDFKit, sign up for our 60-day trial and you’ll receive an email with the download instructions.

Step 2: Modify the Objective-C Code in the Flutter Plugin

For our use case, we’ll only modify the PspdfPlatformView.m class, which is the Objective-C counterpart of our Flutter platform view component.

find-objc-catalog-example-xcode

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

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

@property (nonatomic) PSPDFToolbarButton *clearAnnotationsButton;

@end

Then, we copy the custom annotation toolbar’s implementation at the bottom of PspdfPlatformView.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`.
    for (PSPDFAnnotation *annotation in annotations) {
      [pageView removeAnnotation:annotation options:nil animated:YES];
    }
  }
}

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

// If annotation changes are detected, 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

And 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. Here’s how we do it:

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
  //...
  // This line is already present in the example.
  PSPDFConfiguration *configuration = [PspdfkitFlutterConverter configuration:configurationDictionary isImageDocument:isImageDocument];

  // Update the configuration to override the default class with our custom one.
  configuration = [configuration configurationUpdatedWithBuilder:^(PSPDFConfigurationBuilder * _Nonnull builder) {
    [builder overrideClass:PSPDFAnnotationToolbar.class withClass:CustomButtonAnnotationToolbar.class];
  }];
  //...
}

That’s all! The custom delete button is now part of the annotation toolbar of our Flutter app! Note that this customization will only be applied to the PspdfPlatformView component, and not to the global plugin component, as we didn’t make any changes to the global PSPDFViewController in PspdfkitPlugin.m. To directly check out the code described in this blog, use this branch on our Flutter repo.

Video Tutorial

We created a companion video for this tutorial. You can follow along or simply watch to see how the above use case is implemented into a Flutter app.

Sending Data from the iOS Side (Objective-C) to Dart

Sending data from the platform to Flutter is also a common use case. To send data back to the Flutter side from the platform (iOS), you can make use of FlutterMethodChannel. If you look at our Flutter example project, you’ll notice we have two callbacks already: pdfViewControllerWillDismiss and pdfViewControllerDidDismiss. However, those callbacks don’t send any data back to the Flutter side. To send some data back, we can use the channel’s arguments. Let’s implement this by modifying our Flutter example (clone or download the Flutter project if you don’t have it already; we’ll make the changes there).

Here, we’ll implement a callback to notify us in the Dart code whenever the user scrolls the document and changes the document’s spread index. In this example, we’ll use the PSPDFDocumentViewControllerSpreadIndexDidChangeNotification notification provided by PSPDFKit, but we can use any sort of callback here, such as a delegate method. For the same spread index change event, we can also use -documentViewController:didChangeSpreadIndex: which is a method of PSPDFDocumentViewControllerDelegate.

Here’s a list of changes that we’ll need to make to the example:

  1. In PspdfkitPlugin.m, in the handleMethodCall method, right after we call setLicenseKey, we can start listening to the spreadIndex change notifications like so:

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

Then we can implement the spreadIndexDidChange method in the same file, like this:

- (void)spreadIndexDidChange:(NSNotification *)notification {
    long oldPageIndex = [notification.userInfo[@"PSPDFDocumentViewControllerOldSpreadIndexKey"] longValue];
    long currentPageIndex = [notification.userInfo[@"PSPDFDocumentViewControllerSpreadIndexKey"] longValue];
    NSMutableDictionary *pageIndices = @{@"oldPageIndex": @(oldPageIndex), @"currentPageIndex": @(currentPageIndex)};
    [channel invokeMethod:@"spreadIndexDidChange" arguments:pageIndices];
}
  1. In the lib/src/main.dart file, we need to add another callback function right after pdfViewControllerDidDismiss:

static void Function(dynamic) spreadIndexDidChange;

Then we add a new case to handle it in the _platformCallHandler function:

case 'spreadIndexDidChange':
    spreadIndexDidChange(call.arguments);
    break;

Here, we’re passing along the call arguments that we’ll use in our example.

  1. Finally, in example/lib/main.dart, we can use these arguments by creating a handler function like this:

void spreadIndexDidChangeHandler(dynamic arguments) {
    print(arguments);
}

The last missing piece is to connect this function we created with the plugin. We can do that in the widget’s build method after we set the other handlers (for will dismiss and did dismiss):

Pspdfkit.spreadIndexDidChange = (dynamic arguments) => spreadIndexDidChangeHandler(arguments);

After editing the example like shown here, run the Flutter example by following the steps in the example. You’ll see that on changing pages, the new and old page index will get printed from the Dart side. You can follow this pattern for all different sort of callbacks and update the plugin to adapt to your use case.

Conclusion

This post covered how to bridge native iOS code to Flutter. We also modified the open source PSPDFKit Flutter example to send data from the platform side to Flutter. We hope this tutorial helps you with implementing more complex use cases in your app.

Share Post

Related Articles

Explore more
TUTORIALS  |  iOS • Development • How To • Swift

Adding Annotations in Swift with PDFKit vs. PSPDFKit

DEVELOPMENT  |  SwiftUI • iOS

SwiftUI In Production

RELEASES  |  iOS • Products

PSPDFKit 10.3 for iOS Adds Electronic Signatures and Instant Comments