iOS 27 adds PKStrokeRecognizer to PencilKit, enabling on-device handwriting recognition across 29 languages, plus new path conversion APIs, stroke identity/selection, and programmatic stroke slicing. These APIs let any app read, search, and manipulate handwritten content without requiring a custom ML model.
• PKStrokeRecognizer runs entirely on-device, supports 29 languages, and works on all iOS 27 devices — no server round-trips or Apple Intelligence hardware required
• Bézier-to-PKStrokePath conversion means existing canvas apps can plug in handwriting recognition without migrating to PKCanvasView
• Stable stroke UUIDs, selection control, and substroke extraction unlock custom annotation, replay, and interactive search experiences that were previously impossible in PencilKit
A SwiftUI view where the user writes a word on a PKCanvasView and taps Check — PKStrokeRecognizer compares the recognized text to the target word and shows a pass/fail banner.
import SwiftUI
import PencilKit
struct HandwritingCheckerView: View {
let targetWord = "heart"
@State private var canvas = PKCanvasView()
@State private var resultMessage: String = ""
@State private var resultColor: Color = .clear
@State private var isChecking = false
private let recognizer: PKStrokeRecognizer = {
var r = PKStrokeRecognizer()
r.preferredLanguages = [Locale.Language(identifier: "en")]
return r
}()
var body: some View {
VStack(spacing: 16) {
Text("Write: \"\(targetWord)\"")
.font(.title2.bold())
CanvasWrapper(canvas: canvas)
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary, lineWidth: 1)
)
if !resultMessage.isEmpty {
Text(resultMessage)
.font(.headline)
.foregroundStyle(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(resultColor, in: RoundedRectangle(cornerRadius: 8))
}
HStack(spacing: 12) {
Button("Check") {
Task { await checkHandwriting() }
}
.buttonStyle(.borderedProminent)
.disabled(isChecking)
Button("Clear") {
canvas.drawing = PKDrawing()
resultMessage = ""
}
.buttonStyle(.bordered)
}
}
.padding()
}
@MainActor
private func checkHandwriting() async {
guard !canvas.drawing.strokes.isEmpty else {
resultMessage = "Write something first!"
resultColor = .orange
return
}
isChecking = true
defer { isChecking = false }
let drawing = canvas.drawing
// Collect all stroke IDs from the current drawing
let strokeIDs = drawing.strokes.map(\.id)
do {
// recognizedText returns the single best interpretation
let recognized = try await recognizer.recognizedText(
for: drawing,
strokeIDs: strokeIDs
)
let trimmed = recognized.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if trimmed == targetWord.lowercased() {
resultMessage = "✅ Correct! (\"\(recognized)\")"
resultColor = .green
} else {
resultMessage = "❌ Got \"\(recognized)\" — try again"
resultColor = .red
}
} catch {
resultMessage = "Recognition failed: \(error.localizedDescription)"
resultColor = .orange
}
}
}
// UIViewRepresentable wrapper so PKCanvasView works in SwiftUI
struct CanvasWrapper: UIViewRepresentable {
let canvas: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvas.backgroundColor = UIColor.secondarySystemBackground
canvas.tool = PKInkingTool(.pen, color: .label, width: 5)
return canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {}
}
#Preview {
HandwritingCheckerView()
}• recognizerVersion can change as Apple updates models — persist it alongside indexed content and re-index when it changes • Stroke slicing (programmatic erasing) can be expensive on complex drawings; run it on a background thread • Indexable content returns all candidates concatenated, not just the top result — don't display it directly to users • preferredLanguages must be set before recognition calls; defaults to device language settings
Runs on all devices supported by iOS 27. Simulator only supports Latin-character languages for handwriting recognition.
More iOS 27 APIs land every week.
Get notified when new capabilities are published — no noise, just signal.