I've run into what appears to be a bug in PDFKit's radio button serialization.
When creating a radio button group with PDFAnnotation, only the first annotation added via page.addAnnotation() gets a correct /AS entry in the written PDF — all other annotations always get /AS /Off, regardless of buttonWidgetState.
Minimal reproduction
func makeRadioButton(optionId: String, isSelected: Bool) -> PDFAnnotation {
let ann = PDFAnnotation(bounds: CGRect(x: 0, y: 0, width: 20, height: 20),
forType: .widget, withProperties: nil)
ann.widgetFieldType = .button
ann.widgetControlType = .radioButtonControl
ann.fieldName = "Choice"
ann.buttonWidgetStateString = optionId
ann.buttonWidgetState = isSelected ? .onState : .offState
return ann
}
let pdf = PDFDocument()
let page = PDFPage()
pdf.insert(page, at: 0)
// Intend to select B
page.addAnnotation(makeRadioButton(optionId: "A", isSelected: false))
page.addAnnotation(makeRadioButton(optionId: "B", isSelected: true))
page.addAnnotation(makeRadioButton(optionId: "C", isSelected: false))
_ = pdf.dataRepresentation()
// Result: /AS is /Off for all three — B is not selected in the PDF
What I observed
- Selecting A (first annotation added):
/AS /Awritten correctly works - Selecting B or C:
/AS /Offfor all buttons
Additionally, dataRepresentation() corrupts the in-memory state as a side effect: buttonWidgetState of the selected annotation is .onState before the call and .offState after.
Root cause
During serialization, dataRepresentation() internally calls setButtonWidgetState:.onState on each annotation in turn to generate appearance streams. This triggers PDFKit's radio-group exclusivity logic, which silently clears all other annotations — so by the time /AS is written, only the first annotation's selection survives.
Workaround
It took a while to track this down, so I'm documenting the workaround here in case it helps others.
Add the annotation that should be selected first via page.addAnnotation():
// Add selected annotation first
page.addAnnotation(makeRadioButton(optionId: "B", isSelected: true))
page.addAnnotation(makeRadioButton(optionId: "A", isSelected: false))
page.addAnnotation(makeRadioButton(optionId: "C", isSelected: false))
Tested on macOS 26.3 / Xcode 26.3. Filed as Feedback FB22167174.
Full code including workaround is here:
Has anyone else hit this? Is there a cleaner method I'm missing?