PDFKitにおけるページ番号の取得方法

# はじめに
こんにちは！株式会社ZOZOでiOSエンジニアをしている加藤です。私はショップスタッフの販売サポートツール「FAANS」の開発に従事しています。FAANSでは、この度、ショップスタッフのコーデ投稿を支援するPDFの表示機能が実装され、コーデ投稿に関する情報が掲載されたPDFをアプリ内で閲覧できるようになりました。

FAANSにおけるPDFの閲覧機能はPDFKitを用いて作成されており、PDFの左下に閲覧中のページ数を表示するデザインを採用しています。本記事では、PDFKitを用いて表示したPDFにおいて、閲覧中のページ数を取得し、表示する方法について書きます！

![Simulator Screen Recording - iPhone 16 - 2024-12-15 at 17.37.38.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588901/7cee669d-7c31-c7e9-b3bb-4521b78acecd.gif)

# 目次
- PDFKitを用いたPDF表示
- 縦長PDFのページ番号表示
- 横長PDFのページ番号表示

# PDFKitを用いたPDF表示
PDFKitを用いたPDF表示は以下のようなプログラムで行えます。
```swift
var document: PDFDocument?

@MainActor
func setupPDFView() {
    let pdfView = PDFView()
    pdfView.displayMode = .singlePageContinuous // PDFを連続で表示する
    pdfView.displayDirection = .vertical // 縦方向に表示する
    pdfView.autoScales = true // 自動的にPDFのスケールを画面に合わせる
    pdfView.document = document
    return pdfView
}

func loadPdfDocument() async {
    do {
        let (data, _) = try await URLSession.shared.data(with: pdfUrl) // URLから非同期でPDFデータを取得
        if let document = PDFDocument(data: data), document.pageCount > 0 {
            self.document = document // 有効なPDFを設定
        } else {
            // 表示できるPDFがないケース
        }
    } catch {
        // 表示できるPDFがないケース
    }
}

extension URLSession {
    func data(with url: URL) async throws -> (Data, URLResponse) {
        try await data(from: url)
    }
}
```
上記のように、URLを指定してPDFDocumentを生成して、PDFViewのdocumentに設定することでPDFが表示されます。また、`.displayMode = .singlePageContinuous`、`.displayDirection = .vertical`に設定することでPDFが縦方向に連続で表示されるようにしています。

# 縦長PDFのページ番号表示
先に提示したGifのように、縦長PDFを表示中に閲覧しているページ番号を取得する方法を紹介します。以下のコードは、ページが変更されるたびに通知を受け取り、現在のページ番号を取得する仕組みです。
```swift
let pdfView = PDFView()

NotificationCenter.default.addObserver(forName: .PDFViewPageChanged, object: pdfView, queue: .main) { _ in
    Task { @MainActor in
        // 現在のページ番号を取得
        if let pageNumber = pdfView.currentPage?.pageRef?.pageNumber {
            // pageNumberを画像上に表示
        }
    }
}
```
このコードでは、NotificationCenterを使用して`.PDFViewPageChanged`通知を監視しています。この通知は、PDFViewでページをスクロールするなどして表示中のページが変更されるたびにトリガーされます。また、pdfView.currentPageで現在のページを取得でき、pageRef?.pageNumberから現在のページ番号（1始まり）を取得しています。

# 横長PDFのページ番号表示
横長のPDFを表示する場合、前節と同じようにページ番号を取得すると、初期状態で表示されるページ番号が2になってしまいます。初期のページ番号が2になっているGifを以下に示します。
![Simulator Screen Recording - iPhone 16 - 2024-12-15 at 20.53.28.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588901/6c1c6fc4-2f47-7a01-be47-9766142fe59d.gif)

初期のページ番号が2になってしまう原因は、横長PDFの場合、画面上に複数ページが同時に表示されることです。初期の画面では、1〜3ページが表示され、中央に最も近いページが2ページ目のため、取得されるページ数が2となります。FAANSでは、PDFの上端が表示されている場合は、ページ数の表示は1にしたいため、以下のように実装しました。
```swift
let pdfView = PDFView()

// ページ変更通知を監視して、現在のページ番号を更新
NotificationCenter.default.addObserver(forName: .PDFViewPageChanged, object: pdfView, queue: .main) { _ in
    Task { @MainActor in
        // 現在のページ番号を取得し、上端が表示されている場合は1を設定
        if let pageNumber = pdfView.currentPage?.pageRef?.pageNumber {
            currentPage = showPdfTop ? 1 : pageNumber
        }
    }
}

// scrollViewのcontentOffsetを監視してPDFのスクロール位置を検出
if let scrollView = pdfView.scrollView() {
    cancellableStore.cancellable = scrollView.publisher(for: \.contentOffset)
        .sink { [weak pdfView] contentOffset in
            Task { @MainActor in
                guard let pdfView = pdfView else { return }
                    if contentOffset.y <= 0.0 {
                        // PDFの最上部が表示されている場合
                        currentPage = 1
                        showPdfTop = true
                    } else if let pageNumber = pdfView.currentPage?.pageRef?.pageNumber, pageNumber != currentPage {
                        // ページが変更された場合に通知を送信
                        NotificationCenter.default.post(name: .PDFViewPageChanged, object: pdfView)
                        showPdfTop = false
                    }
                }
            }
}

fileprivate extension PDFView {
    // PDFViewのサブビューからUIScrollViewを取得
    func scrollView() -> UIScrollView? {
        return self.subviews.compactMap { $0 as? UIScrollView }.first
    }
}
```
このプログラムでは、PDFViewからスクロール位置を検出するために内部のUIScrollViewを取得し、そのcontentOffsetをCombineのPublisherを使用して監視しています。contentOffsetが0以下の場合、PDFの最上端が表示されていると判断してページ番号を1に設定します。これにより、横長PDFであっても意図したページ番号を正しく表示できます。

# まとめ
PDFKitを用いてPDFを表示する方法と、縦長・横長のPDFで現在のページ番号を取得する方法を紹介しました。この記事が、今後PDFKitを用いてiOSアプリにPDF表示機能を実装する方の参考になれば幸いです。