Using PaperKit from SwiftUI

Using PaperKit from SwiftUI

GIST referenced in this post: PaperKit in SwiftUI PoC GIST.

GIST referenced in this post: PaperKit in SwiftUI PoC GIST.

I've had an iPad app idea rattling around in my head for a while which would leverage PencilKit and a drawing canvas to enable a specific scenario that I think might form the basis of an app. I particularly like Freeform and Lisa and I use it actively to colloborate on designs for our apps and so my mental model was to use something like that approach.

Freeform

Imagine my joy when I saw the WWDC 2025 session announcing PaperKit at which you can watch here https://developer.apple.com/videos/play/wwdc2025/285.

WWDC25 PaperKit session

I watched this session with great anticipation and delight that the capabilities clearly form the basis of the Freeform. The downside was that Apple chose to not provide a SwiftUI wrapper in this version of iOS. Because I wanted to start prototyping the app idea, I decided to build a quick PoC SwiftUI wrapper. Whilst it is definitely not comprehensive or production ready, it gets you up and running to play around with the basic functionality of PaperKit.

I coded back and forth with Claude Code for quite a whileto get the basics running and find a structure that was easy to understand whilst implementing the basic functionality of:

  1. Displaying a PaperKit canvas with view constraints for sizing.
  2. Toolbar controls to toggle between edit mode and view mode, undo last action and clear the canvas.
  3. Display and management of drawing tools whilst in edit mode.
  4. AccessoryItem for toolpicker for insertion of shapes and text.
  5. Automatic loading and saving of current canvas data on app launch and exit.

The result was a working prototype:

Prototype

One point to note about PaperKit: although it supports many of the canvas capabilities of Freeform, it isn't exhaustive; for example, it doesn't support the concept of adding connections between objects. The other big limitation is there is no network syncing support. This isn't surprising and I'm guessing it would be possible to build this using the delegate callbacks combined with a good peer-to-peer networking library.

Architecture Overview

The integration between SwiftUI and PaperKit requires several layers to bridge the two frameworks effectively. Here's how the various components relate to each other:

graph TB
    %% Bridge Layer  
    subgraph "Bridge Layer"
        PaperMarkupView["PaperMarkupView<br/>UIViewControllerRepresentable<br/>canvasSize, isEditable"]
        Coordinator["PaperMarkupCoordinator<br/>@Observable<br/>weak wrapperViewController"]
    end
    
    %% UIKit Layer
    subgraph "UIKit Layer"
        WrapperVC["PaperMarkupWrapperViewController<br/>UIViewController<br/>PaperMarkupViewController.Delegate"]
    end
    
    %% Framework Layer
    subgraph "Apple Frameworks"
        PaperVC["PaperMarkupViewController<br/>(PaperKit)"]
        ToolPicker["PKToolPicker<br/>(PencilKit)"]
        MarkupModel["PaperMarkup<br/>(PaperKit Data Model)"]
    end
    
    %% Storage Layer
    subgraph "Storage Layer"
        FileSystem["File System<br/>~/Library/Application Support/<br/>[Bundle ID]/markup.data"]
    end
    
    %% Ownership Relationships (Solid Lines)
    PaperMarkupView --> Coordinator
    PaperMarkupView --> WrapperVC
    WrapperVC --> PaperVC
    WrapperVC --> ToolPicker  
    WrapperVC --> MarkupModel
    
    %% Weak References (Dashed Lines)
    Coordinator -.->|weak ref| WrapperVC
    WrapperVC -.->|weak ref| Coordinator
    
    %% Protocol Conformances (Dotted Lines)
    PaperMarkupView -.->|conforms to| UIViewControllerRepresentable["UIViewControllerRepresentable<br/>(SwiftUI Protocol)"]
    WrapperVC -.->|conforms to| Delegate["PaperMarkupViewController.Delegate<br/>(PaperKit Protocol)"]
    Coordinator -.->|conforms to| Observable["@Observable<br/>(Swift Macro)"]
    
    %% Data Flow (Bold Lines)
    WrapperVC ==>|auto-save| FileSystem
    FileSystem ==>|auto-load| WrapperVC
    
    %% Styling
    classDef swiftui fill:#e1f5fe,stroke:#01579b,stroke-width:2px
    classDef bridge fill:#f3e5f5,stroke:#4a148c,stroke-width:2px  
    classDef uikit fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
    classDef framework fill:#fff3e0,stroke:#e65100,stroke-width:2px
    classDef storage fill:#fce4ec,stroke:#880e4f,stroke-width:2px
    classDef protocol fill:#f5f5f5,stroke:#424242,stroke-width:1px,stroke-dasharray: 5 5
    
    class PaperMarkupView,Coordinator bridge
    class WrapperVC uikit
    class PaperVC,ToolPicker,MarkupModel framework
    class FileSystem storage
    class UIViewControllerRepresentable,Delegate,Observable protocol

Implementation

Here's the complete implementation of the SwiftUI wrapper for PaperKit:

ContentView.swift
//
//  ContentView.swift
//  PaperKitPoCiOS
//
//  Created by James Clarke on 7/15/25.
//

import SwiftUI

struct ContentView: View {
    @State private var coordinator = PaperMarkupCoordinator()
    @State private var isEditMode = true
    
    var body: some View {
        PaperMarkupView(
            canvasSize: CGSize(width: 400, height: 800),
            isEditable: isEditMode,
            coordinator: coordinator
        )
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .toolbar {
            ToolbarItemGroup(placement: .navigationBarTrailing) {
                Button(isEditMode ? "View" : "Edit") {
                    isEditMode.toggle()
                }
                
                Button("Clear") {
                    coordinator.clear()
                }
                
                Button("Undo") {
                    coordinator.undo()
                }
            }
        }
        .navigationTitle("Markup Canvas")
        .navigationBarTitleDisplayMode(.inline)
    }
}

#Preview {
    ContentView()
}
PaperMarkupView.swift
//
//  PaperMarkupView.swift
//  PaperKitPoCiOS
//
//  Created by James Clarke and Claude Code on 8/3/25.
//
//  Description:
//  This file defines the SwiftUI view for a PaperKit-based markup canvas.
//  It uses a UIViewControllerRepresentable to wrap the PaperMarkupViewController
//  from the PaperKit framework, allowing it to be used within a SwiftUI application.
//
//  The implementation includes:
//  - A coordinator to manage communication between SwiftUI and UIKit.
//  - A wrapper view controller (`PaperMarkupWrapperViewController`) to host and
//    configure the PaperKit canvas, PencilKit tool picker, and data persistence.
//  - Automatic saving and loading of markup data to the Application Support directory.
//

import SwiftUI
import PaperKit
import PencilKit
import UIKit



// MARK: - PaperMarkupView
///
/// A SwiftUI view that wraps the `PaperMarkupWrapperViewController` to provide a
/// PaperKit drawing canvas.
///
/// ## SwiftUI-UIKit Bridge Pattern
/// This struct conforms to `UIViewControllerRepresentable`, which is SwiftUI's
/// standard protocol for integrating UIKit view controllers into SwiftUI views.
/// Think of it as a "translator" that lets SwiftUI communicate with UIKit components.
///
/// ## Why This Pattern Is Needed
/// PaperKit is a UIKit-based framework, but we want to use it in a SwiftUI app.
/// This bridge pattern allows us to:
/// - Wrap UIKit components for use in SwiftUI
/// - Handle the lifecycle differences between SwiftUI and UIKit
/// - Maintain proper state synchronization between the two frameworks
///
struct PaperMarkupView: UIViewControllerRepresentable {
    let canvasSize: CGSize
    let isEditable: Bool
    let coordinator: PaperMarkupCoordinator
    
    /// Initializes the SwiftUI view for the markup canvas.
    /// - Parameters:
    ///   - canvasSize: The intrinsic size of the drawing canvas.
    ///   - isEditable: A flag to determine if the canvas is interactive.
    ///   - coordinator: The coordinator object to manage state and actions.
    init(canvasSize: CGSize = CGSize(width: 400, height: 800), isEditable: Bool = true, coordinator: PaperMarkupCoordinator) {
        self.canvasSize = canvasSize
        self.isEditable = isEditable
        self.coordinator = coordinator
    }
    
    /// **Required SwiftUI Protocol Method**
    /// Creates the coordinator object that manages communication with the view controller.
    /// 
    /// **When Called**: SwiftUI calls this method ONCE when the view is first created.
    /// **Purpose**: The coordinator acts as a delegate/communication bridge between
    /// SwiftUI (declarative) and UIKit (imperative) worlds.
    /// 
    /// **Why Needed**: SwiftUI views are structs (value types) that get recreated,
    /// but we need a persistent object (reference type) to maintain state and handle
    /// callbacks from the UIKit side.
    func makeCoordinator() -> PaperMarkupCoordinator {
        coordinator
    }
    
    
    /// **Required SwiftUI Protocol Method**
    /// Creates and configures the `PaperMarkupWrapperViewController` instance.
    /// 
    /// **When Called**: SwiftUI calls this method ONCE when the view first appears.
    /// **Purpose**: This is where we create the actual UIKit view controller that
    /// will be embedded in our SwiftUI view hierarchy.
    /// 
    /// **Important**: This method should only create and configure - never update.
    /// Updates are handled by `updateUIViewController(_:context:)`.
    func makeUIViewController(context: Context) -> PaperMarkupWrapperViewController {
        let wrapperViewController = PaperMarkupWrapperViewController(
            canvasSize: canvasSize,
            isEditable: isEditable
        )
        // Link the coordinator to the newly created view controller.
        context.coordinator.setWrapperViewController(wrapperViewController)
        return wrapperViewController
    }
    
    /// **Required SwiftUI Protocol Method**
    /// Updates the `PaperMarkupWrapperViewController` when the SwiftUI view's state changes.
    /// 
    /// **When Called**: SwiftUI calls this method whenever any `@State`, `@Binding`, 
    /// or other observed property changes (like `isEditable` in our case).
    /// 
    /// **Purpose**: This is SwiftUI's way of keeping the UIKit component in sync
    /// with SwiftUI's declarative state. When SwiftUI re-evaluates our view due to
    /// state changes, it calls this method to "push" those changes to UIKit.
    /// 
    /// **Performance Note**: This method should be lightweight since it can be
    /// called frequently during SwiftUI's update cycles.
    func updateUIViewController(_ uiViewController: PaperMarkupWrapperViewController, context: Context) {
        uiViewController.setCanvasEditable(isEditable)
    }
}

// MARK: - PaperMarkupWrapperViewController
///
/// A UIKit view controller that wraps and manages PaperKit's `PaperMarkupViewController`.
///
/// ## Purpose
/// This wrapper provides a clean interface between the SwiftUI representable view
/// and PaperKit's UIKit-based components. It handles all the UIKit-specific setup
/// and lifecycle management that PaperKit requires.
///
/// ## Responsibilities
/// 1. **PaperKit Integration**: Hosts `PaperMarkupViewController` as a child view controller
/// 2. **Tool Management**: Sets up and manages `PKToolPicker` for drawing tools
/// 3. **Data Persistence**: Automatically saves/loads drawings to Application Support directory
/// 4. **Delegate Handling**: Responds to PaperKit callbacks for drawing events
/// 5. **State Management**: Manages editable state and canvas clearing
///
/// ## Architecture & User Interaction Flow
/// ```
/// SwiftUI PaperMarkupView (SwiftUI declarative layer)
///     ↓ (via UIViewControllerRepresentable protocol)
/// PaperMarkupWrapperViewController (UIKit bridge layer)
///     ↓ (child view controller pattern)
/// PaperKit's PaperMarkupViewController (Apple's drawing framework)
/// ```
/// 
/// ## Complete User Interaction Flow
/// ```
/// 1. User taps "Clear" button in SwiftUI
///        ↓
/// 2. ContentView calls coordinator.clear()
///        ↓ 
/// 3. Coordinator calls wrapperViewController.clearCanvas()
///        ↓
/// 4. WrapperViewController creates new empty PaperMarkup
///        ↓
/// 5. Updates PaperKit's PaperMarkupViewController with empty canvas
/// 
/// OR
/// 
/// 1. User draws on canvas with Apple Pencil
///        ↓
/// 2. PaperKit detects drawing and calls delegate method
///        ↓
/// 3. paperMarkupViewControllerDidChangeMarkup() fired automatically
///        ↓
/// 4. Auto-save triggered in background using modern async/await
///        ↓
/// 5. Drawing saved to ~/Library/Application Support/[Bundle ID]/markup.data
/// ```
///
/// ## File Storage
/// Drawings are automatically saved to:
/// `[App Support]/[Bundle ID]/markup.data`
///
class PaperMarkupWrapperViewController: UIViewController, PaperMarkupViewController.Delegate {
    
    // MARK: Properties
    
    private var paperViewController: PaperMarkupViewController!
    private var markupModel: PaperMarkup!
    private var toolPicker: PKToolPicker!
    
    private var isEditable: Bool
    private var canvasSize: CGSize
    
    /// **Weak Reference to Prevent Retain Cycles**
    /// A weak reference to the coordinator to communicate back to the SwiftUI layer.
    /// 
    /// **Why Weak?** To prevent "retain cycles" - a memory leak where two objects
    /// hold strong references to each other and can never be deallocated.
    /// In our case: Coordinator → WrapperViewController → Coordinator would create a cycle.
    weak var coordinator: PaperMarkupCoordinator?
    
    
    // MARK: Initialization
    
    init(canvasSize: CGSize, isEditable: Bool) {
        self.isEditable = isEditable
        self.canvasSize = canvasSize
        super.init(nibName: nil, bundle: nil)
        initializeMarkupModel(with: canvasSize)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: View Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupPaperMarkupViewController()
        setupToolPicker()
    }
    
    /// **First Responder Concept**
    /// The view controller can only become the "first responder" if it's editable.
    /// 
    /// **What's a First Responder?** In iOS, the "first responder" is the object
    /// that receives user input events first (like keyboard input or tool picker actions).
    /// Only the first responder can display the PencilKit tool picker.
    /// 
    /// **Why This Matters**: When `isEditable` is false, we don't want to show drawing
    /// tools, so we refuse to become first responder.
    override var canBecomeFirstResponder: Bool {
        return isEditable
    }
    
    // MARK: Setup Methods
    
    /// Initializes the `PaperMarkup` data model with the specified canvas size.
    private func initializeMarkupModel(with canvasSize: CGSize) {
        let bounds = CGRect(origin: .zero, size: canvasSize)
        markupModel = PaperMarkup(bounds: bounds)
    }
    
    /// Creates, configures, and embeds the `PaperMarkupViewController`.
    private func setupPaperMarkupViewController() {
        paperViewController = PaperMarkupViewController(markup: markupModel, supportedFeatureSet: .latest)
        
        // **Child View Controller Pattern**
        // This is a standard UIKit pattern for embedding one view controller inside another.
        // Steps: 1) Add the view to our view hierarchy, 2) Add as child controller, 3) Notify of completion
        view.addSubview(paperViewController.view)
        addChild(paperViewController)  // Tells UIKit about the parent-child relationship
        paperViewController.didMove(toParent: self)  // Lets the child know it's been added
        
        // Configure constraints to make the child view fill the parent.
        paperViewController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            paperViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
            paperViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            paperViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            paperViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // Set a reasonable zoom range.
        paperViewController.zoomRange = 0.1...3.0
        
        // Attempt to load any previously saved markup data.
        loadMarkup()
        
        // Set the delegate to self to handle events like data changes.
        paperViewController.delegate = self
    }
    
    /// Initializes and configures the PencilKit `PKToolPicker`.
    private func setupToolPicker() {
        // **Become First Responder to Show Tool Picker**
        // This tells iOS "this view controller wants to handle user input" and enables
        // the PencilKit tool picker to be displayed for this canvas.
        becomeFirstResponder()
        
        toolPicker = PKToolPicker()
        toolPicker.addObserver(paperViewController)
        
        // **Modern PencilKit Tool Picker Management**
        // Apple's recommended approach (iOS 14+) for managing tool picker visibility.
        // This replaces older methods like `setVisible(_:forFirstResponder:)`.
        pencilKitResponderState.activeToolPicker = toolPicker
        pencilKitResponderState.toolPickerVisibility = .visible
        
        // Add an accessory button to the tool picker for additional actions.
        toolPicker.accessoryItem = UIBarButtonItem(
            barButtonSystemItem: .add,
            target: self,
            action: #selector(addOrEditMarkupButtonPressed(_:))
        )
    }
    
    // MARK: Public Control Methods
    
    /// Clears the canvas by replacing the current markup model with a new, empty one.
    func clearCanvas() {
        // Re-initialize the model with a new empty drawing.
        initializeMarkupModel(with: canvasSize)
        markupModel?.drawing = PKDrawing()
        
        // Update the view controller with the new, empty model.
        paperViewController.markup = markupModel
    }
    
    /// Trivial implementation of undo last action
    func undoLastAction() {
        paperViewController.undoManager?.undo()
    }
    
    /// Sets the editable state of the canvas and shows or hides the tool picker accordingly.
    func setCanvasEditable(_ editable: Bool) {
        isEditable = editable
        
        if editable {
            toolPicker.setVisible(true, forFirstResponder: paperViewController)
            becomeFirstResponder()
        } else {
            toolPicker.setVisible(false, forFirstResponder: paperViewController)
            resignFirstResponder()
        }
    }
    
    // MARK: Private Helpers & Actions
    
    /// Action method for the tool picker's accessory button.
    @objc private func addOrEditMarkupButtonPressed(_ button: UIBarButtonItem) {
        let markupEditViewController = MarkupEditViewController(supportedFeatureSet: .latest)
        markupEditViewController.delegate = paperViewController as? MarkupEditViewController.Delegate
        markupEditViewController.modalPresentationStyle = .popover
        markupEditViewController.popoverPresentationController?.barButtonItem = button
        present(markupEditViewController, animated: true)
    }
    
}

// MARK: - Data Persistence
///
/// Extension handling all data loading and saving operations for the markup canvas.
///
/// ## Storage Strategy
///
/// - **Automatic Location**: Saves to Application Support directory (user can't see this folder)
/// - **App Isolation**: Each app gets its own folder using Bundle ID to prevent conflicts
/// - **Auto-Save**: Triggers immediately when user draws (no "Save" button needed)
/// - **Auto-Load**: Restores previous drawing when app launches
/// - **Crash Protection**: Uses atomic writes so files are never corrupted
/// 
/// **File Path Example**: 
/// `~/Library/Application Support/com.yourcompany.PaperKitApp/markup.data`
///
extension PaperMarkupWrapperViewController {
    
    /// Computed property for the file URL where the markup data is saved.
    /// The data is stored in the app's Application Support directory.
    private var markupDataFileURL: URL {
        let fileManager = FileManager.default
        // Use the app's bundle identifier to create a unique folder.
        let bundleID = Bundle.main.bundleIdentifier ?? "com.example.PaperKitApp"
        
        let appSupportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
        let appDirectory = appSupportURL.appendingPathComponent(bundleID)
        
        // Ensure the directory exists before attempting to write to it.
        try? fileManager.createDirectory(at: appDirectory, withIntermediateDirectories: true, attributes: nil)
        
        return appDirectory.appendingPathComponent("markup.data")
    }
    
    /// **Automatic Data Loading**
    /// Loads markup data from the file system.
    /// 
    /// **When Called**: Automatically during view controller setup (setupPaperMarkupViewController)
    /// **What Happens**: If a previous drawing exists, it's loaded silently in the background
    /// **If No File**: Gracefully does nothing - user gets a blank canvas
    func loadMarkup() {
        guard FileManager.default.fileExists(atPath: markupDataFileURL.path) else {
            return // No saved data exists - user gets blank canvas (normal for first launch)
        }
        
        // We use `Task` to perform file I/O on a background thread, keeping the UI responsive.
        Task {
            do {
                // File loading happens on background thread (good for performance)
                let data = try Data(contentsOf: markupDataFileURL)
                let loadedMarkup = try PaperMarkup(dataRepresentation: data)
                
                // **MainActor Requirement**
                // UI updates MUST happen on the main thread. `await MainActor.run` ensures
                // this code runs on the main thread.
                await MainActor.run {
                    self.paperViewController.markup = loadedMarkup
                    self.markupModel = loadedMarkup
                }
            } catch {
                print("Failed to load markup data: \(error.localizedDescription)")
            }
        }
    }
    
    /// **Automatic Background Saving**
    /// Saves the current markup data to persistent storage.
    /// 
    /// **When Called**: Automatically triggered by paperMarkupViewControllerDidChangeMarkup() 
    /// whenever the user draws, erases, or modifies anything
    /// **Background Operation**: File I/O happens off the main thread for smooth UI performance
    /// **User Experience**: Completely invisible to user - no save dialogs or loading spinners
    private func saveMarkup(_ markup: PaperMarkup) {
        // **Modern Async/Await Pattern**
        // This replaces older completion handler patterns for cleaner, more readable code.
        Task {
            do {
                let data = try await markup.dataRepresentation()
                
                // `.atomic` option ensures file integrity - either the write succeeds completely
                // or fails completely (no partial/corrupted files)
                try data.write(to: markupDataFileURL, options: .atomic)
            } catch {
                print("Failed to save markup data: \(error.localizedDescription)")
            }
        }
    }
}

// MARK: - PaperKit Framework Callbacks (Delegate Methods)
///
/// Extension implementing the PaperMarkupViewController delegate protocol.
/// 
/// ## Important: These Are Automatic Framework Callbacks
/// These methods are **automatically called by the PaperKit framework** when specific
/// events occur. You don't call these methods directly - PaperKit calls them for you.
/// 
/// ## The Auto-Save Flow
/// ```
/// User draws on canvas
///        ↓
/// PaperKit detects change
///        ↓  
/// PaperKit calls paperMarkupViewControllerDidChangeMarkup(_:)
///        ↓
/// Our code automatically saves to disk
/// ```
/// 
/// This creates a seamless auto-save experience without any manual save buttons.
///
extension PaperMarkupWrapperViewController {
    
    /// **PaperKit Framework Callback**
    /// Called automatically by PaperKit whenever the user modifies the drawing on the canvas.
    /// 
    /// **When This Fires**: Every time the user:
    /// - Draws a new stroke with Apple Pencil or finger
    /// - Erases content
    /// - Adds or modifies text/shapes
    /// - Performs any markup operation
    /// 
    /// **What We Do**: Immediately trigger automatic background saving to ensure
    /// no user work is ever lost, even if the app crashes or is terminated.
    func paperMarkupViewControllerDidChangeMarkup(_ paperMarkupViewController: PaperMarkupViewController) {
        guard let currentMarkup = paperMarkupViewController.markup else { return }
        saveMarkup(currentMarkup)
    }
    
    /// **PaperKit Framework Callback**
    /// Called automatically when the user selects or deselects markup elements.
    /// 
    /// **When This Fires**: When the user taps to select text, shapes, or drawings
    /// for editing, moving, or deleting.
    /// 
    /// **Current Implementation**: Empty - we don't need custom selection handling,
    /// but PaperKit requires this method to be implemented.
    func paperMarkupViewControllerDidChangeSelection(_ paperMarkupViewController: PaperMarkupViewController) {}
    
    /// **PaperKit Framework Callback**
    /// Called automatically when the user begins a new drawing stroke.
    /// 
    /// **When This Fires**: The moment the user touches the screen and starts
    /// drawing with Apple Pencil, finger, or any drawing tool.
    /// 
    /// **Current Implementation**: Empty - we don't need custom drawing start handling,
    /// but PaperKit requires this method to be implemented.
    func paperMarkupViewControllerDidBeginDrawing(_ paperMarkupViewController: PaperMarkupViewController) {}
    
    /// **PaperKit Framework Callback**
    /// Called automatically when the visible portion of the canvas changes.
    /// 
    /// **When This Fires**: When the user:
    /// - Pinches to zoom in/out
    /// - Scrolls/pans around the canvas
    /// - Rotates the device (if orientation changes are supported)
    /// 
    /// **Current Implementation**: Empty - we don't need custom viewport handling,
    /// but PaperKit requires this method to be implemented.
    func paperMarkupViewControllerDidChangeContentVisibleFrame(_ paperMarkupViewController: PaperMarkupViewController) {}
}


// MARK: - PaperMarkupCoordinator
///
/// An observable object that acts as a coordinator between the `PaperMarkupView` (SwiftUI)
/// and the `PaperMarkupWrapperViewController` (UIKit).
///
/// ## Why We Need This Coordinator
/// SwiftUI is **declarative** ("what should be displayed") while UIKit is **imperative** 
/// ("do this action now"). The coordinator bridges this gap by:
/// 
/// 1. **Action Translation**: Converts SwiftUI button taps into UIKit method calls
/// 2. **State Management**: Maintains persistent references across SwiftUI view recreations
/// 3. **Communication Bridge**: Allows SwiftUI to trigger actions on UIKit components
/// 
/// ## Example Usage Flow
/// ```
/// SwiftUI Button("Clear") { coordinator.clear() }
///                    ↓
/// coordinator.clear() calls wrapperViewController?.clearCanvas()
///                    ↓
/// UIKit canvas is cleared immediately
/// ```
///
@Observable
class PaperMarkupCoordinator {
    
    // A weak reference to the wrapper view controller to avoid retain cycles.
    private weak var wrapperViewController: PaperMarkupWrapperViewController?
    
    /// Connects this coordinator with its corresponding `PaperMarkupWrapperViewController`.
    /// This method is called during the view controller's creation process.
    /// - Parameter controller: The `PaperMarkupWrapperViewController` instance to coordinate with.
    func setWrapperViewController(_ controller: PaperMarkupWrapperViewController) {
        self.wrapperViewController = controller
        controller.coordinator = self // Establish the back-reference.
    }
    
    /// Clears all drawings and content from the markup canvas.
    func clear() {
        wrapperViewController?.clearCanvas()
    }
    
    /// Trivial implementation of undo
    func undo() {
        wrapperViewController?.undoLastAction()
    }
    
    /// Toggles the user's ability to draw on the canvas.
    /// - Parameter isEditable: A boolean indicating whether the canvas should be editable.
    func setEditable(_ isEditable: Bool) {
        wrapperViewController?.setCanvasEditable(isEditable)
    }
}

// MARK: - SwiftUI Preview
#Preview {
    NavigationView {
        PaperMarkupView(coordinator: PaperMarkupCoordinator())
            .edgesIgnoringSafeArea(.bottom)
            .navigationTitle("Markup Canvas")
            .navigationBarTitleDisplayMode(.inline)
    }
}

No comments yet

ObjectivePixel updates

News about our apps and what we’re building. Confirm your email to join; unsubscribe whenever you like.