How to Bridge Native iOS Code to Flutter

Illustration: How to Bridge Native iOS Code to Flutter

PSPDFKit’s Flutter wrapper 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 have 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, I first wanted to cover some background information on some of the Flutter wrapper (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 wrapper, 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 is 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 wrapper 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:

Copy
1
2
3
- (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:

Copy
1
2
3
+ (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 have covered the basics of the Dart and platform components used in the Flutter widget, let’s get started with the tutorial!

Prerequisites

I’ll be assuming you’re familiar with Flutter and that you already have 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 will be adding 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 will find the Objective-C sample code from PSCAnnotationToolbarButtonsExample.m that we will bridge over to our Flutter app.

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 will receive an email with the download instructions.

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

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

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

Copy
1
2
3
4
5
6
// 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:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@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:

Copy
1
2
3
4
5
6
7
8
9
10
11
- (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

Watch the companion video to this blog tutorial. Follow along or simply see how the above use case is implemented into a Flutter app.

Conclusion

This post covered how to bridge native iOS code to Flutter. I hope that this tutorial will help you with implementing more complex use cases in your app.

PSPDFKit for iOS

Download the free 60-day trial and add it to your app today.