How to Bridge Native PSPDFKit for iOS Code to Flutter
In this tutorial, we’ll cover how to use the PSPDFKit for Flutter library in two complex use cases by bridging native Objective-C code to Flutter. The Objective-C code we’ll be bridging in this tutorial will make use of some components in the native PSPDFKit for iOS SDK.
There are two main methods of bridging native Android and iOS code with Flutter applications:
-
By communicating between Flutter (Dart) and native code (Android — Java/Kotlin, iOS — Swift/Objective-C) using method channels.
PSPDFKit’s Flutter library aims to expose a lot of our native PSPDFKit for iOS framework’s APIs to Dart using both methods. These APIs allow you to easily implement simple use cases in Flutter. We also aimed to keep our Flutter plugin extensible to allow users to bridge native Objective-C code and adapt the plugin to more complex use cases.
ℹ️ Note: This blog post was updated in April 2022.
Prerequisites
Before starting this tutorial, we recommend you first have a fundamental understanding of the following topics:
-
Flutter and Dart
-
Bridging Flutter with native iOS code using hosted views and method channels
-
How to use the native PSPDFKit for iOS SDK (recommended)
This post also assumes you’ve already integrated the PSPDFKit for Flutter SDK into your app. Otherwise, you can use our example project from our PSPDFKit Flutter SDK repository on GitHub.
The modifications we’ll make to our SDK and example project in this tutorial are available on this branch of the PSPDFKit Flutter SDK repo.
Background Information
We’ll first cover some background information on some of the Flutter SDK (Dart) and native iOS platform (Objective-C) components in our PSPDFKit for Flutter SDK.
Dart Components Explained
We expose two different components in Dart. Depending on your project’s requirements, you can use one or the other.
-
PspdfkitWidgetController
+UiKitView
— To display PDF files in Flutter, we use platform views provided by Flutter. On iOS, this isUiKitView
. Along with the platform views, we also need to usePspdfkitWidgetController
, which is a simple class that uses a method channel on'com.pspdfkit.widget.$id'
to manage the communication between the platform view and Objective-C code (PspdfPlatformView
). This platform view must be created with aviewType
('com.pspdfkit.widget'
) and attached to a newPspdfkitWidgetController
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. ThePspdfkitWidgetController
component can be composed into more complex widgets and is the recommended way of displaying 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, and 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’s possible when using the global plugin.
We recommend the PspdfkitWidgetController
component for most use cases, whereas the global plugin view should be used for scenarios where a PDF needs to be presented modally.
iOS Platform Components Explained
Corresponding to the two Flutter SDK components described above are their iOS 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.
PspdfPlatformView
s 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 PspdfPlatformView
s 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
, the latter of 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!
Use Case 1 — Adding a Delete Button
We’ll add a custom delete 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 Code for the Custom Delete Button
We created a branch in Flutter repository for this tutorial. You’ll need to add two Objective-C files from that branch to the ios
directory of your Flutter project:
If you’re following along using the PSPDFKit for Flutter example project, add these two files from the branch to the ios/Classes
folder in your local copy of the repository.
Step 2 — Modify the Objective-C Code in the iOS Folder
For this use case, modify both the PspdfPlatformView.m
class, which is the Objective-C counterpart of the Flutter platform view component, and the PspdfkitPlugin.m
file, which is the Objective-C counterpart of the Pspdfkit global plugin in Flutter.
Changes to PspdfPlatformView.m
Import the custom delete button at the top of the file:
// ... // These lines are already present in the file: #import "PspdfPlatformView.h" #import "PspdfkitFlutterHelper.h" #import "PspdfkitFlutterConverter.h" // Import the custom toolbar here. #import "PspdfkitCustomButtonAnnotationToolbar.h" @import PSPDFKit; @import PSPDFKitUI; // ...
In the initWithFrame
method, override the default annotation toolbar by telling PSPDFKit to use the custom annotation toolbar instead. This is done by updating the PSPDFViewController
’s configuration:
- (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args messenger:(NSObject<FlutterBinaryMessenger> *)messenger { NSString *name = [NSString stringWithFormat:@"com.pspdfkit.widget.%lld",viewId]; _platformViewId = viewId; // ... NSString *documentPath = args[@"document"]; if (documentPath != nil && [documentPath isKindOfClass:[NSString class]] && [documentPath length] > 0) { // ... // These lines are already in the file. PSPDFConfiguration *configuration = [PspdfkitFlutterConverter configuration:configurationDictionary isImageDocument:isImageDocument]; // Update the configuration to override the default class with the custom one. configuration = [configuration configurationUpdatedWithBuilder:^(PSPDFConfigurationBuilder * _Nonnull builder) { [builder overrideClass:PSPDFAnnotationToolbar.class withClass:PspdfkitCustomButtonAnnotationToolbar.class]; }]; // ... }
The custom delete button is now part of the annotation toolbar of the Flutter app! Note that at this point, 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
.
If you’re following along with the PSPDFKit for Flutter example project, you can see the custom delete button in action by following these steps:
-
Run the project in the iOS simulator using
flutter run
. -
Under PSPDFKit Widget Examples, select the Basic Example.
-
Click the annotation button in the toolbar.
To directly check out the code described in this blog, use this branch on our Flutter repo.
Changes to PspdfkitPlugin.m
The changes here are similar to the changes we already made to PspdfPlatformView.m
.
Import the custom delete button at the top of the file:
// ... // These lines are already present in the file: #import "PspdfPlatformView.h" #import "PspdfkitFlutterHelper.h" #import "PspdfkitFlutterConverter.h" // Import the custom toolbar here. #import "PspdfkitCustomButtonAnnotationToolbar.h" @import PSPDFKit; @import PSPDFKitUI; // ...
In the handleMethodCall
method, override the default annotation toolbar by telling PSPDFKit to use the custom annotation toolbar instead. This is done by updating the PSPDFViewController
’s configuration in the handler for calls to present
through the com.pspdfkit.global
method channel:
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@"frameworkVersion" isEqualToString:call.method]) { // ... }else if ([@"present" isEqualToString:call.method]) { // ... // These lines are already in the file PSPDFConfiguration *configuration = [PspdfkitFlutterConverter configuration:configurationDictionary isImageDocument:isImageDocument]; // Update the configuration to override the default class with the custom one. configuration = [configuration configurationUpdatedWithBuilder:^(PSPDFConfigurationBuilder * _Nonnull builder) { [builder overrideClass:PSPDFAnnotationToolbar.class withClass:PspdfkitCustomButtonAnnotationToolbar.class]; }]; // ... }
That’s all! The custom delete button is now part of the annotation toolbar that’s used by both the PspdfPlatformView
component and the PspdfkitPlugin
global plugin.
Use Case 2 — Sending Notifications from iOS to Dart
Sending data from iOS to Flutter is also a common use case. To send data back to the Flutter side from 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 PSPDFDocumentViewControllerSpreadIndexDidChangeNotification
, a 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 we’ll need to make to the example:
-
In
PspdfkitPlugin.m
, in thehandleMethodCall
method, insert an observer for the spread index right after callingpresentingViewController presentViewController:navigationController animated:YES completion:nil
, like so:
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(spreadIndexDidChange:) name:PSPDFDocumentViewControllerSpreadIndexDidChangeNotification object:nil];
Then, implement the spreadIndexDidChange
method at the bottom of 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]; }
-
In the
lib/src/main.dart
file of the PSPDFKit Flutter SDK, we need to add another callback function right afterpdfViewControllerDidDismiss
:
static late 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.
-
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 different kinds 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 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.
If you have any questions about PSPDFKit for Flutter, please don’t hesitate to reach out to us. We’re happy to help.
Rukky joined Nutrient as an intern in 2022 and is currently a software engineer on the Server and Services Team. She’s passionate about building great software, and in her spare time, she enjoys reading cheesy novels, watching films, and playing video games.