Internal PDF links stop working on iOS 27 / macOS 27 beta — every link opens the first page

On iOS 27 beta / Xcode 27.0, PDFDocument.write(to:) no longer serializes destinations for link annotations that are created at runtime. The annotation and its PDFActionGoTo action are both written, but the destination's page reference is not, so every internal link is dead.

This worked correctly on iOS 26. PDFs generated on iOS 26 still navigate correctly; the same code on iOS 27 beta produces PDFs whose links are all dead.

Answered by DTS Engineer in 904658022

I reproduced this on the iPadOS 27.0 beta, build 24A5430a. Your code is correct, and the destination is being lost during serialization, exactly as you described. Everything below refers to that beta build, so it may of course change before release. I have attached the analysis behind this response to the bug you filed, so engineering has the same detail.

The failure is visible in the written file. The /GoTo action is present, but its /D array begins with the fit-type name instead of a reference to the destination page. The page reference is absent rather than wrong. Parsing the output with CoreGraphics rather than PDFKit confirms it, since element 0 of /D does not resolve to a page object.

Documents written on 26 read correctly on the 27 beta. I checked that with a file written on macOS 26.5 and read back on the device. Re-saving such a document on the beta, without adding anything to it, preserves its existing links. So the problem is confined to destinations that are created in memory and serialized for the first time. Destinations already present in a file survive.

One thing you may not have run into yet: outlines are affected more severely. Writing a document whose outlineRoot you have set produces a file with no outline at all, rather than an outline with a broken destination. If your documents carry bookmarks, those are being dropped silently.

Which documents are affected

On the 27 beta this depends on where the document's pages came from, not on whether the document is new:

  • Pages that arrived through PDFDocument(data:) or PDFDocument(url:), with no page inserted afterwards: affected.
  • Pages added with insert(_:at:): not affected. Building a document a page at a time inserts pages already, and that is what avoids the problem.

A common way to produce a PDF with working internal links is to render pages with a graphics context, load the result with PDFDocument(data:), then add link annotations. That pattern falls in the affected group even though the document is brand new.

A workaround

Inserting a page into the document and removing it again restores correct destination serialization:

// Temporary: works around a destination-serialization regression seen on the iOS/iPadOS 27 beta.
// Any throwaway page will do, and it is removed again immediately.
if let filler = PDFDocument(data: singlePageData)?.page(at: 0) {
    document.insert(filler, at: document.pageCount)
    document.removePage(at: document.pageCount - 1)
}

The document ends with the page count it started with. In my testing this restored link destinations, restored outlines, and left pre-existing annotations and their destinations intact. I checked the resulting file with CoreGraphics, where the destination resolves to a page object, so the links are valid to readers other than PDFKit.

A real insert is needed: exchangePage(at:withPageAt:) with the same index on both sides does not have the same effect. The order does not matter, so the insert and remove can go anywhere before the write, including after your annotations are already in place.

This relies on an undocumented side effect rather than on anything the API guarantees. I would treat it as temporary and remove it once it is no longer needed.

Two things you can skip

dataRepresentation() behaves the same as write(to:), so switching write paths does not help.

Setting destination (https://developer.apple.com/documentation/pdfkit/pdfannotationkey/destination) directly rather than an action writes a /Dest entry into the file, but the destination still does not resolve.

One caveat about files already written

Files your app wrote on the beta before applying a workaround cannot be repaired afterwards. The intended target page is not recorded in them at all, so there is nothing to recover from. That makes the write path the thing worth addressing first.

I reproduced this on the iPadOS 27.0 beta, build 24A5430a. Your code is correct, and the destination is being lost during serialization, exactly as you described. Everything below refers to that beta build, so it may of course change before release. I have attached the analysis behind this response to the bug you filed, so engineering has the same detail.

The failure is visible in the written file. The /GoTo action is present, but its /D array begins with the fit-type name instead of a reference to the destination page. The page reference is absent rather than wrong. Parsing the output with CoreGraphics rather than PDFKit confirms it, since element 0 of /D does not resolve to a page object.

Documents written on 26 read correctly on the 27 beta. I checked that with a file written on macOS 26.5 and read back on the device. Re-saving such a document on the beta, without adding anything to it, preserves its existing links. So the problem is confined to destinations that are created in memory and serialized for the first time. Destinations already present in a file survive.

One thing you may not have run into yet: outlines are affected more severely. Writing a document whose outlineRoot you have set produces a file with no outline at all, rather than an outline with a broken destination. If your documents carry bookmarks, those are being dropped silently.

Which documents are affected

On the 27 beta this depends on where the document's pages came from, not on whether the document is new:

  • Pages that arrived through PDFDocument(data:) or PDFDocument(url:), with no page inserted afterwards: affected.
  • Pages added with insert(_:at:): not affected. Building a document a page at a time inserts pages already, and that is what avoids the problem.

A common way to produce a PDF with working internal links is to render pages with a graphics context, load the result with PDFDocument(data:), then add link annotations. That pattern falls in the affected group even though the document is brand new.

A workaround

Inserting a page into the document and removing it again restores correct destination serialization:

// Temporary: works around a destination-serialization regression seen on the iOS/iPadOS 27 beta.
// Any throwaway page will do, and it is removed again immediately.
if let filler = PDFDocument(data: singlePageData)?.page(at: 0) {
    document.insert(filler, at: document.pageCount)
    document.removePage(at: document.pageCount - 1)
}

The document ends with the page count it started with. In my testing this restored link destinations, restored outlines, and left pre-existing annotations and their destinations intact. I checked the resulting file with CoreGraphics, where the destination resolves to a page object, so the links are valid to readers other than PDFKit.

A real insert is needed: exchangePage(at:withPageAt:) with the same index on both sides does not have the same effect. The order does not matter, so the insert and remove can go anywhere before the write, including after your annotations are already in place.

This relies on an undocumented side effect rather than on anything the API guarantees. I would treat it as temporary and remove it once it is no longer needed.

Two things you can skip

dataRepresentation() behaves the same as write(to:), so switching write paths does not help.

Setting destination (https://developer.apple.com/documentation/pdfkit/pdfannotationkey/destination) directly rather than an action writes a /Dest entry into the file, but the destination still does not resolve.

One caveat about files already written

Files your app wrote on the beta before applying a workaround cannot be repaired afterwards. The intended target page is not recorded in them at all, so there is nothing to recover from. That makes the write path the thing worth addressing first.

Internal PDF links stop working on iOS 27 / macOS 27 beta — every link opens the first page
 
 
Q