We hit the same regression and traced it. Filed as FB24882188, with a small repro project.
What is happening: on iOS/iPadOS/macOS 27, PDFKit draws annotations into a private layer, PDFPageLayerAnnotationEffect, and configures it with contentsScale 1.0 and magnificationFilter "nearest". The PDFPageLayerTile layers that draw the page right beside it get the real on-screen scale (3.86 in our repro) and "linear". So annotations are rasterized at 1 pixel per point and then stretched with nearest-neighbour sampling, which is the pixelation. On iOS 26.5 the same layer gets the tile scale and "linear", which is why the same build is crisp there.
It is not caused by custom draw(with:in:) code: standard ink and free-text annotations show it too, and widgets as well (FB24843022, same defect).
Setting those two values on the live layer fixes it immediately, and PDFKit's next draw then uses the new scale. PDFKit resets the layer on every redraw, though, so the correction has to run right before each one. We do that by replacing -display on that one class (never on CALayer itself):
import UIKit
// iOS/iPadOS/macOS 27 workaround for pixelated PDFKit annotations (FB24882188, FB24843022).
// Call once at launch, before any PDFView draws. No-op if the private class is absent.
func installAnnotationLayerFix() {
guard #available(iOS 27.0, *),
let cls = NSClassFromString("PDFPageLayerAnnotationEffect"),
let method = class_getInstanceMethod(cls, #selector(CALayer.display)) else { return }
typealias Display = @convention(c) (CALayer, Selector) -> Void
let original = unsafeBitCast(method_getImplementation(method), to: Display.self)
let block: @convention(block) (CALayer) -> Void = { layer in
if Thread.isMainThread {
MainActor.assumeIsolated { correctScale(of: layer) }
}
original(layer, #selector(CALayer.display))
}
// Adds an override on the subclass only; CALayer itself is untouched.
class_replaceMethod(cls, #selector(CALayer.display),
imp_implementationWithBlock(block), method_getTypeEncoding(method))
}
/// Gives the layer its real on-screen density (screen scale x its magnification in the window)
/// and linear filtering, which is what PDFKit gives the page tiles beside it.
@MainActor
private func correctScale(of layer: CALayer) {
guard let window = hostView(of: layer)?.window else { return }
let bounds = layer.bounds
guard bounds.width > 0, bounds.height > 0 else { return }
let inWindow = layer.convert(bounds, to: window.layer)
let magnification = sqrt((inWindow.width * inWindow.height) / (bounds.width * bounds.height))
let scale = magnification * window.traitCollection.displayScale
guard scale.isFinite, scale > 0 else { return }
layer.contentsScale = scale
layer.magnificationFilter = .linear
}
@MainActor
private func hostView(of layer: CALayer) -> UIView? {
var current = layer.superlayer
while let l = current {
if let view = l.delegate as? UIView { return view }
current = l.superlayer
}
return nil
}
It depends on a private class name, so treat it as a stopgap until Apple fixes the regression; if the class is renamed or removed, it silently does nothing. In our app it fixed everything on iPadOS 27 and Mac Catalyst (macOS 27): new annotations, annotations loaded from saved files, zooming, page changes, rotation. It compiles under both Swift 5 and Swift 6 language modes.
If you're affected, please file your own report and mention FB24882188 and FB24843022 in it, so they get linked on Apple's side.