Multiple Windows

Beginning with iOS 13, it is 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 is 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 PSPDFTabbedViewController for more information on this topic.

Create 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 PSPDFDocument. 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:

Copy
1
2
3
let activity = NSUserActivity(activityType: "com.your-app.user-activity-type")
activity.userInfo = ["documentURL": document.fileURL!.absoluteString]
document.userActivity = activity
Copy
1
2
3
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 in order 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 PSPDFTabbedViewController and emitting a notification with the newly opened document from the new scene to close existing documents from other tabbed bars:

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

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

Copy
1
2
3
4
5
6
7
8
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)
    }
}
Copy
1
2
3
4
5
6
7
8
9
10
11
[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];
        }
    }
}

Drag Documents across Scenes

It is 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 PSPDFTabbedViewController, depending on your use case.

Screen Mirroring

If you are using PSPDFScreenController to handle screen mirroring and your app adopts the new scene-based flow on iOS 13 and up, 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:

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

Prevent the Same Document from Being Opened across Multiple Scenes

Note that it is not recommended to show the same document in multiple PSPDFViewController 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 is 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 would 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:

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

And 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, and if yes, you can open the session with the document instead:

Copy
1
2
3
4
5
6
7
8
9
func openExistingScene(for document: PSPDFDocument) -> 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
}
Copy
1
2
3
4
5
6
7
8
9
10
- (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;
}