Open Multiple Windows in Our iOS Viewer

It’s possible to open multiple windows from a single app on iPad. PSPDFKit for iOS has allowed you to show documents in multiple windows since version 9.

However, PSPDFKit never opens a new window itself. This is done to avoid interfering with the behavior of your app. This means that creating new scenes is your responsibility, but PSPDFKit provides some built-in functionality and APIs that might help you adopt this feature.

Tabbed Bar

One of the most interesting scenarios where documents can be used to create new windows is the tabbed bar. It’s possible to drag tabs out of the tabbed bar controller to create a new scene or drag tabs across different tabbed bars in different scenes. See the drag-and-drop API in PDFTabbedViewController for more information on this topic.

Creating a New Window Scene from a Tab

You can also drag documents out to create a new window scene if your app supports multiple scenes. You can do this by providing an NSUserActivity via the userActivity property on Document. This will be used in an item provider for the drag session when dragging the tab. Your app needs to handle the user activity and decide how to create a new scene.

This user activity could look like this:

let activity = NSUserActivity(activityType: "com.your-app.user-activity-type")
activity.userInfo = ["documentURL": document.fileURL!.absoluteString]
document.userActivity = activity
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:@"com.your-app.user-activity-type"];
activity.userInfo = @{ @"documentURL": document.fileURL.absoluteString };
document.userActivity = activity;

We recommended closing the document that was dragged out into a new scene to avoid having the same document opened multiple times. (For more information on this, see the section about preventing the same document from being opened across multiple scenes.)

This can be done, for example, by using a subclass of PDFTabbedViewController and emitting a notification with the newly opened document from the new scene to close existing documents from other tabbed bars:

NotificationCenter.default.post(name: .DocumentOpenedInNewScene, object: nil, userInfo: ["documentURL": document.fileURL])
[NSNotificationCenter.defaultCenter postNotificationName:PSCDocumentOpenedInNewSceneNotification object:nil userInfo:@{ @"documentURL": document.fileURL}];

And in the PDFTabbedViewController subclass, to close a document opened in another scene, you can use logic like this:

NotificationCenter.default.addObserver(self, selector: #selector(documentOpenedInNewSceneNotification(_:)), name: .DocumentOpenedInNewScene, object: nil)

@objc func documentOpenedInNewSceneNotification(_ notification: Notification) {
    guard let url = notification.userInfo?["documentURL"] as? URL else { return }
    for document in self.documents where url.resolvingSymlinksInPath() == document.fileURL?.resolvingSymlinksInPath() {
        self.removeDocument(document, animated: true)
    }
}
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(documentOpenedInNewSceneNotification:) name:PSCDocumentOpenedInNewSceneNotification object:nil];

- (void)documentOpenedInNewSceneNotification:(NSNotification *)notification {
    NSURL *url = notification.userInfo[@"documentURL"];
    if (url == nil) { return; }
    for (PSPDFDocument *document in self.documents) {
        if ([url.URLByResolvingSymlinksInPath isEqual:document.fileURL.URLByResolvingSymlinksInPath]) {
            [self removeDocument:document animated:YES];
        }
    }
}

Dragging Documents across Scenes

It’s also possible to drag documents from a tabbed bar in one scene to another tabbed bar in another scene. This is disabled by default; to enable it, set allowDraggingTabsToExternalTabbedBar and/or allowDroppingTabsFromExternalTabbedBar on PDFTabbedViewController, depending on your use case.

Screen Mirroring

If you’re using ScreenController to handle screen mirroring and your app uses a UIWindowSceneDelegate to set up the UI, you might not want to conditionally create a window for external displays, so as to not cause conflicts regarding which window should be shown on the external screen. You can accomplish this like so:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard session.role == .windowApplication else { return }
    ...
}
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
    if (session.role != UIWindowSceneSessionRoleApplication) { return; }
    ...
}

Preventing the Same Document from Being Opened across Multiple Scenes

Note that it’s not recommended to show the same document in multiple PDFViewController instances at the same time. Any changes you make will only be saved to disk and synced when a save is triggered. This can lead to data corruption or data loss. Consider this when adopting support for multiple windows.

We recommend adding special handling for instances where a user opens a document that’s already open in one scene in another scene.

There are several ways to achieve this. One way to do this is by leveraging the userInfo of a UISceneSession to keep track of all the opened documents in each scene. Then, when a new document is opened, you can query all of the openSessions and check if the document is already open in one of them. If so, then you can handle opening the document by activating this scene instead of opening it in the current scene.

There might also be cases where you’d want to open a document in the current scene instead of activating the scene where the document is already open. One of those reasons might be explicitly dragging and dropping a document into a new scene. In such a case, you should make sure to open the document in the newly created scene and close it in all other scenes.

These reasons might be different for your app, so use this approach as a guideline on how you could track open documents and activate a scene where a document is opened.

One way to set the user info with the information of which documents are currently shown in a scene session is by using the document’s UID, like this:

if let document = self.pdfController.document {
    self.view.window?.windowScene?.session.userInfo = [OpenDocumentsInSceneUserInfoKey: [document.uid]
}
self.view.window.windowScene.session.userInfo = @{OpenDocumentsInSceneUserInfoKey: @[document.UID]};

Then, when opening a document, you can use the following logic to check all open sessions to see if they already contain the document in question. If they do, you can open the session with the document instead:

func openExistingScene(for document: Document) -> Bool {
    for sceneSession in openSessions {
        if let uids = sceneSession.userInfo?[OpenDocumentsInSceneUserInfoKey] as? [String], uids.contains(document.uid) {
            requestSceneSessionActivation(sceneSession, userActivity: nil, options: nil, errorHandler: nil)
            return true
        }
    }
    return false
}
- (BOOL)psc_openExistingSceneForDocument:(PSPDFDocument *)document {
    for (UISceneSession *sceneSession in self.openSessions) {
        NSArray<NSString *> *uids = sceneSession.userInfo[OpenDocumentsInSceneUserInfoKey];
        if ([uids containsObject:document.UID]) {
            [self requestSceneSessionActivation:sceneSession userActivity:nil options:nil errorHandler:nil];
            return YES;
        }
    }
    return NO;
}

Instant and Support for Multiple Windows

On account of being a subclass of PDFViewController, InstantViewController has the same limitations as PDFViewController. In other words, showing the same documents in multiple instances of InstantViewController isn’t recommended, as it can corrupt the data as described in the above section, and also because it can cause syncing issues.

However, you can still show multiple instances of InstantViewController that show different documents or a collaboration session differentiated by its document link in different scenes, provided your application supports opening multiple scenes. You’ll have to handle the special case of a user trying to open a document link that’s already shown by an instance of InstantViewController in one scene and another instance of InstantViewController in another scene.

You can read about the different approaches to ensure that this doesn’t happen in the Preventing the Same Document from Being Opened across Multiple Scenes section above.