Multi-User PDF Collaboration using Swift for iOS
Show multiple instances of an Instant document for collaborative editing. Get additional resources by visiting our guide on Instant Usage.
//// Copyright © 2021-2025 PSPDFKit GmbH. All rights reserved.//// The Nutrient sample applications are licensed with a modified BSD license.// Please see License for details. This notice may not be removed from this file.//
import Instantimport PSPDFKitimport PSPDFKitUIimport WebKit
class MultiUserInstantExample: Example {
override init() { super.init()
title = "Multi-User Instant Example" contentDescription = "Shows multiple instances of an Instant document for collaborative editing." category = .collaboration priority = 2 wantsModalPresentation = true embedModalInNavigationController = false }
weak var presentingViewController: UIViewController?
override func invoke(with delegate: ExampleRunnerDelegate) -> UIViewController? { let presentingViewController = delegate.currentViewController!
// We store the document info once the session is created. // We will try to find the stored info matching the document identifier for this example if available. if let docInfoDict = UserDefaults.standard.object(forKey: MultiUserInstantExampleLastViewedDocumentInfoKey) as? [String: String], let lastViewDocumentInfo = InstantDocumentInfo(from: docInfoDict) { // Present the InstantViewController directly using the existing document info extracted from the cache. presentInstantViewController(for: lastViewDocumentInfo, on: presentingViewController) } else { presentNewSession(on: presentingViewController) }
// Hold a reference to the presentingViewController to handle URLActionChallenge. self.presentingViewController = presentingViewController
return nil }
/// Connects to the Example API Client to get the document info for the current example /// and then displays that document in a `InstantViewController`. func presentNewSession(on viewController: UIViewController) { // Create a `WebExamplesAPIClient` instance using the URL of the client server // that has the list of documents the particular user can access. // // `WebExamplesAPIClient` can make calls to the server to get the document info required // to connect to a Nutrient Document Engine. // // For the purpose of this example we are using the Nutrient Web SDK Catalog server. // In your app, this should be replaced by the URL of your that is interacting with the Nutrient Document Engine. // See https://www.nutrient.io/guides/ios/instant-synchronization/ for more details. let apiClient = WebExamplesAPIClient(baseURL: InstantWebExamplesServerURL, delegate: self)
let progressHUDItem = StatusHUDItem.indeterminateProgress(withText: "Creating") progressHUDItem.setHUDStyle(.black)
progressHUDItem.push(animated: true, on: viewController.view.window) { // Asking the Nutrient Web SDK Catalog example server to create a new session // for the given user and the specified document identifier. // It should ideally provide a signed JWT (and the Nutrient Document Engine if not already available) // that can be used by the `InstantClient` to access and download the document for iOS. apiClient.createNewSession(documentIdentifier: .marketingDepartmentSchedule) { result in DispatchQueue.main.async { progressHUDItem.pop(animated: true, completion: nil)
switch result { case let .success(documentInfo): self.presentInstantViewController(for: documentInfo, on: viewController) case let .failure(error): viewController.showAlert(withTitle: "Couldn’t Get Instant Document Info", message: error.localizedDescription) } } } } }
private func presentInstantViewController(for instantDocumentInfo: InstantDocumentInfo, on viewController: UIViewController) { let instantViewController = MultiUserContainerViewController(documentInfo: instantDocumentInfo) instantViewController.modalPresentationStyle = .fullScreen viewController.present(instantViewController, animated: true) }
}
// MARK: Split Container View Controller
/// Presents a multi-user setup for an Instant document where the iOS Instant SDK is used one side/// to display and edit the document acting as one user along with a Web View on the other side/// showing the same document acting as another user.private class MultiUserContainerViewController: UIViewController, UISplitViewControllerDelegate, PDFViewControllerDelegate {
private var containedSplitViewController: UISplitViewController
init(documentInfo: InstantDocumentInfo) {
instantController = ContainedInstantDocumentViewController(documentInfo: documentInfo, lastViewedDocumentInfoKey: MultiUserInstantExampleLastViewedDocumentInfoKey) instantController.updateConfiguration { $0.useParentNavigationBar = true $0.shouldHideNavigationBarWithUserInterface = false }
webviewContainerController = InstantExampleWebViewContainerController(documentInfo: documentInfo)
instantController.title = "iOS SDK User" webviewContainerController.title = "Web SDK User"
let primaryColumnContainer = SidebarControllersContainingViewController(childViewController: instantController) let primaryColumnNavVC = PDFNavigationController(rootViewController: primaryColumnContainer) let secondaryColumnNavVC = PDFNavigationController(rootViewController: webviewContainerController)
// Create a `UISplitViewController` with the above controllers. let splitController = UISplitViewController(style: .doubleColumn) splitController.setViewController(primaryColumnNavVC, for: .primary) splitController.setViewController(secondaryColumnNavVC, for: .secondary)
// We want to show split the display area equally between the two controllers. splitController.preferredDisplayMode = .oneBesideSecondary splitController.minimumPrimaryColumnWidth = 200 splitController.maximumPrimaryColumnWidth = 1000 splitController.preferredPrimaryColumnWidthFraction = 0.5
// Disabled because we do not want to show the `displayModeButton`.#if !os(visionOS) splitController.presentsWithGesture = false#endif containedSplitViewController = splitController
super.init(nibName: nil, bundle: nil)
instantController.delegate = self }
@available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
var instantController: InstantDocumentViewController
var webviewContainerController: InstantExampleWebViewContainerController
override func viewDidLoad() { super.viewDidLoad()
addChild(containedSplitViewController) view.addSubview(containedSplitViewController.view) containedSplitViewController.didMove(toParent: self)
containedSplitViewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ containedSplitViewController.view.topAnchor.constraint(equalTo: view.topAnchor), containedSplitViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor), containedSplitViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor), containedSplitViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) }
}
private class ContainedInstantDocumentViewController: InstantDocumentViewController {
/// Whether automatic syncing of the Instant document is enabled or not. var isSyncEnabled = true
lazy var syncButtonItem = UIBarButtonItem(title: "Disable Sync", style: .plain, target: self, action: #selector(toggleSyncing(_:)))
override init(documentInfo: InstantDocumentInfo, lastViewedDocumentInfoKey: String? = nil) { super.init(documentInfo: documentInfo, lastViewedDocumentInfoKey: lastViewedDocumentInfoKey)
// We do not want to allow changing the document editing sessions at all for this example. collaborationOptionsConfiguration = .init( documentIdentifierForNewSession: documentInfo.documentId, allowJoiningExistingSessions: false, allowCreatingNewSessions: false, allowsOpeningArbitraryDocuments: true )
navigationItem.setLeftBarButtonItems([exampleCloseButtonItem, syncButtonItem], for: .document, animated: false)
annotationToolbarController?.annotationToolbar.supportedToolbarPositions = .right annotationToolbarController?.annotationToolbar.toolbarPosition = .right }
/// Toggles auto-syncing state of the Instant document. @objc func toggleSyncing(_ sender: UIBarButtonItem) { do { if isSyncEnabled { try InstantDocumentManager.shared.disableSync(for: documentInfo) sender.title = "Enable Sync" isSyncEnabled = false } else { try InstantDocumentManager.shared.enableSync(for: documentInfo) sender.title = "Disable Sync" isSyncEnabled = true } } catch { showAlert(withTitle: "Couldn't Disable Instant Syncing", message: error.localizedDescription) } }}
// MARK: - Web View Container
/// Presents a Web View displaying the Instant Document using the sharing URL.private class InstantExampleWebViewContainerController: UIViewController, WKUIDelegate {
/// Username that will be sent to the web page displaying the Instant Document when asked for the user name. lazy var webCommentUserName = "Web User"
var webView: WKWebView
var documentInfo: InstantDocumentInfo
init(documentInfo: InstantDocumentInfo) { self.documentInfo = documentInfo webView = WKWebView() super.init(nibName: nil, bundle: nil) webView.uiDelegate = self }
override func loadView() { view = webView }
override func viewDidLoad() { super.viewDidLoad()
// Use the Instant document's sharing URL to display the document in the web view. let url = documentInfo.url webView.load(URLRequest(url: url)) webView.allowsBackForwardNavigationGestures = false }
@available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo) async -> String? { webCommentUserName }
}
extension MultiUserInstantExample: WebExamplesAPIClientDelegate {
func examplesAPIClient(_ apiClient: WebExamplesAPIClient, didReceiveBasicAuthenticationChallenge challenge: URLAuthenticationChallenge, completion: @escaping (URLCredential?) -> Void) { presentingViewController?.presentBasicAuthPrompt(for: challenge) { username, password in guard let user = username, let pass = password else { completion(nil) return }
let urlCredential = URLCredential(user: user, password: pass, persistence: .permanent) completion(urlCredential) } }
}
This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.