Swift and SwiftUI tutorials for Swift Developers

PencilKit in SwiftUI

As an iOS Developer, creating rich, tactile experiences is one of the most rewarding goals. Since the introduction of the Apple Pencil, iPad users (and increasingly, iPhone users via touch gestures) expect to interact with their apps seamlessly, taking notes, drawing, or annotating documents. This is where one of the most powerful tools in Swift programming comes into play: the PencilKit framework.

If you are developing in Xcode and have adopted the declarative paradigm, you may have noticed that integrating UIKit-based frameworks into SwiftUI can seem like an initial challenge. However, the interoperability offered by Swift makes it not only possible but highly efficient.

In this detailed tutorial, we will deeply explore how to implement PencilKit in SwiftUI and iOS, creating a functional drawing canvas, managing the tool palette, and saving user strokes, all optimized for iOS and iPadOS.


1. What is PencilKit and why should you use it?

PencilKit is a native Apple framework designed to make it easy for any iOS Developer to incorporate a low-latency drawing environment into their applications. Before PencilKit, creating a drawing app required complex mathematical calculations with Core Graphics, OpenGL, or Metal to interpret touches, calculate Apple Pencil pressure, and render strokes without lag.

PencilKit solves all this by offering:

  • PKCanvasView: The main view where drawing takes place.
  • PKToolPicker: The floating (or docked) user interface that provides pens, markers, erasers, and color pickers.
  • PKDrawing: The underlying data model that stores the stroke vectors, allowing you to save, load, and even inspect the drawing.

2. The Challenge: Integrating PencilKit into SwiftUI

Currently, Apple does not provide a purely SwiftUI native view for PencilKit. PKCanvasView is a subclass of UIScrollView (which in turn inherits from UIView).

Therefore, in modern Swift programming, we need to build a “bridge” using the UIViewRepresentable protocol. This protocol allows us to wrap UIKit views so that SwiftUI treats them as first-class citizens within its view tree.


3. Setting up the Project in Xcode

  1. Open Xcode and create a new project by selecting “App” under the iOS tab.
  2. Name your project (e.g., “PencilKitSwiftUITutorial”).
  3. Ensure the interface is set to SwiftUI and the language is Swift.
  4. It is recommended that in the project settings (General tab), under “Supported Destinations”, you make sure both iPhone and iPad are checked, as PencilKit shines especially on iPadOS.

4. Creating the Canvas: Implementing UIViewRepresentable

Let’s create our bridge component. Create a new Swift file named PencilKitCanvas.swift. Here we will define the structure that will wrap our PKCanvasView.

import SwiftUI
import PencilKit

struct PencilKitCanvas: UIViewRepresentable {
    // We bind the drawing to a SwiftUI state so we can save/load it
    @Binding var canvasView: PKCanvasView

    // makeUIView is called only once when SwiftUI creates the view
    func makeUIView(context: Context) -> PKCanvasView {
        // Configure the canvas to accept both Apple Pencil and finger touch
        canvasView.drawingPolicy = .anyInput
        
        // Assign the delegate to respond to changes in the canvas
        canvasView.delegate = context.coordinator
        
        // A transparent background allows composing the canvas over other views
        canvasView.backgroundColor = .clear
        canvasView.isOpaque = false
        
        return canvasView
    }

    // updateUIView is called every time the SwiftUI state changes
    func updateUIView(_ uiView: PKCanvasView, context: Context) {
        // Here we could update canvas properties if the external state changes
    }
    
    // The Coordinator acts as a bridge for UIKit delegates (PKCanvasViewDelegate)
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, PKCanvasViewDelegate {
        var parent: PencilKitCanvas
        
        init(_ parent: PencilKitCanvas) {
            self.parent = parent
        }
        
        // Delegate method: called every time the user draws something
        func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
            // Useful if you need to notify SwiftUI that the drawing changed (e.g., for auto-save)
        }
    }
}

Code Analysis for the iOS Developer:

  • @Binding var canvasView: We pass the PKCanvasView from the parent view in SwiftUI. This gives us control to extract the PKDrawing (the drawing) and show the PKToolPicker from the main view.
  • drawingPolicy = .anyInput: By default, on iPadOS, PencilKit assumes the user will draw with the Apple Pencil and scroll with their finger. By changing it to .anyInput, we allow drawing with the finger on iOS (iPhone).
  • Coordinator: It is fundamental in Swift programming when using UIViewRepresentable. It allows us to conform to the PKCanvasViewDelegate protocol to react to user events.

5. Adding the PKToolPicker (Tool Palette)

The canvas by itself only draws with the default brush. To offer a complete experience, we need to show Apple’s tool palette.

Create a new file named DrawingView.swift. This will be the SwiftUI view that the user sees and that will orchestrate our canvas and tools.

import SwiftUI
import PencilKit

struct DrawingView: View {
    // We hold the canvas instance and the tool picker as view state
    @State private var canvasView = PKCanvasView()
    @State private var toolPicker = PKToolPicker()
    
    var body: some View {
        ZStack {
            // App background
            Color.white.ignoresSafeArea()
            
            // Our wrapped canvas
            PencilKitCanvas(canvasView: $canvasView)
                .ignoresSafeArea()
        }
        // We use onAppear to present the Tool Picker once the view is ready
        .onAppear {
            setupToolPicker()
        }
    }
    
    private func setupToolPicker() {
        // Link the Tool Picker to our canvas
        toolPicker.setVisible(true, forFirstResponder: canvasView)
        toolPicker.addObserver(canvasView)
        
        // Make the canvas the "First Responder" so the Tool Picker appears
        canvasView.becomeFirstResponder()
    }
}

With just this, if you run the app in your simulator or physical device via Xcode, you already have a fully functional drawing app! The palette will float on the iPad and adjust perfectly to the keyboard or bottom space on the iPhone.


6. Saving and Loading the Drawing (Persistence)

An app that doesn’t save the user’s work isn’t very useful. The power of using PencilKit in SwiftUI and iOS lies in how easily we can serialize the PKDrawing object.

Let’s modify our DrawingView to include buttons in a navigationBar that allow clearing the canvas, saving the drawing in Data format (ideal for UserDefaults, CoreData, or SwiftData), and restoring it.

import SwiftUI
import PencilKit

struct DrawingAppView: View {
    @State private var canvasView = PKCanvasView()
    @State private var toolPicker = PKToolPicker()
    
    // Variable to temporarily store the drawing data in UserDefaults
    @AppStorage("savedDrawing") private var savedDrawingData: Data = Data()

    var body: some View {
        NavigationView {
            PencilKitCanvas(canvasView: $canvasView)
                .ignoresSafeArea()
                .onAppear {
                    setupToolPicker()
                    loadDrawing() // Load drawing on launch
                }
                .navigationTitle("PencilKit + SwiftUI")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .navigationBarLeading) {
                        Button(action: clearCanvas) {
                            Image(systemName: "trash")
                        }
                    }
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Save", action: saveDrawing)
                    }
                }
        }
    }
    
    private func setupToolPicker() {
        toolPicker.setVisible(true, forFirstResponder: canvasView)
        toolPicker.addObserver(canvasView)
        canvasView.becomeFirstResponder()
    }
    
    private func saveDrawing() {
        // PKCanvasView has a .drawing property we can serialize to DataRepresentation
        savedDrawingData = canvasView.drawing.dataRepresentation()
        print("Drawing saved successfully.")
    }
    
    private func loadDrawing() {
        // Attempt to recreate the PKDrawing from the stored data
        if !savedDrawingData.isEmpty {
            do {
                let drawing = try PKDrawing(data: savedDrawingData)
                canvasView.drawing = drawing
            } catch {
                print("Error loading drawing: \(error.localizedDescription)")
            }
        }
    }
    
    private func clearCanvas() {
        // To clear, we simply assign a new empty drawing
        canvasView.drawing = PKDrawing()
    }
}

Here we’ve used the @AppStorage property wrapper, an excellent addition to SwiftUI, which saves the serialized data directly into UserDefaults. While this is perfect for a tutorial or lightweight prototype, as a professional iOS Developer, for production apps you should consider saving these files to the file system (File Manager) if the drawing is very complex or heavy.


7. Advanced Use: Analyzing Strokes and Customizing Tools

Swift programming applied to PencilKit doesn’t stop at simply showing a canvas. Since iOS 14, Apple has allowed developers to inspect individual strokes.

The PKDrawing object contains an array of PKStroke. Each stroke contains information about the path, the ink, and the transformation.

Reading User Strokes

If you want to know how many strokes the user has drawn or analyze their behavior (for example, for handwriting recognition apps in Swift), you can iterate over them:

func analyzeStrokes() {
    let strokes = canvasView.drawing.strokes
    print("The user has made \(strokes.count) strokes.")
    
    for stroke in strokes {
        let path = stroke.path
        let ink = stroke.ink
        print("Stroke with color: \(ink.color) and tool type: \(ink.inkType)")
    }
}

Changing Tools Programmatically

Sometimes you don’t want to use the PKToolPicker. Imagine you are creating a kids’ game in SwiftUI and want to provide your own colorful buttons. You can modify the canvas tool directly:

// Change to a red pen programmatically
func useRedPen() {
    canvasView.tool = PKInkingTool(.pen, color: .red, width: 5)
}

// Change to the eraser
func useEraser() {
    canvasView.tool = PKEraserTool(.vector) // Erases whole stroke
    // canvasView.tool = PKEraserTool(.bitmap) // Erases by pixels
}

8. Best Practices: iOS vs iPadOS

When developing apps with PencilKit in SwiftUI and iOS, a good iOS Developer should keep the running device’s hardware in mind, utilizing the conditional capabilities that Xcode and Swift offer.

  • On iPadOS: The user expects their Apple Pencil to draw and their finger to scroll the page (think of Apple’s Notes app). To achieve this, configure canvasView.drawingPolicy = .pencilOnly. If you use a ScrollView wrapping the canvas, this will allow natural navigation.
  • On iOS (iPhone): Since there is no Apple Pencil, you must use canvasView.drawingPolicy = .anyInput. If you don’t, the user will touch the screen and absolutely nothing will happen, creating a terrible user experience (UX).

You can detect the device (idiom) in Swift like this:

if UIDevice.current.userInterfaceIdiom == .pad {
    canvasView.drawingPolicy = .pencilOnly // Optional, depending on your design
} else {
    canvasView.drawingPolicy = .anyInput
}

Also, make sure to manage Dark Mode. PencilKit is smart: if a user draws with black ink in light mode, and the device switches to dark mode, PencilKit automatically inverts that black to white so it remains visible against the dark background. If you need an “absolute” color that doesn’t change (like a legal signature), you’ll need to force the canvas’s trait collection or use custom colors defined as fixed.


Conclusion

Integrating PencilKit in SwiftUI and iOS opens up a world of interactive possibilities for any iOS Developer. Although it requires a small leap to UIKit using UIViewRepresentable, the Xcode API is so polished that the process is extremely clean.

Modern Swift programming allows us to combine the best of both worlds: the speed and declarative design of SwiftUI to build our user interface (buttons, navigation, menus), and the low-level graphical power of UIKit to handle near-zero latency drawing.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Confirmation Dialog vs Alert in SwiftUI

Next Article

How to use SF Symbols in Xcode

Related Posts