[FB22167174] PDFKit: `buttonWidgetState = .onState` ignored for non-first radio button annotations on `dataRepresentation()`

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 /A written correctly works
  • Selecting B or C: /AS /Off for 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?

The code I attached seems 404. I will attach the file again.

[FB22167174] PDFKit: `buttonWidgetState = .onState` ignored for non-first radio button annotations on `dataRepresentation()`
 
 
Q