メモ機能のあるアプリを運用していると、ある時期から同じ要望が繰り返し届くようになりました。「書いた内容を PDF で送りたい」。私自身、最初は共有シートにテキストを流すだけで十分だと考えていたのですが、仕事の記録として使ってくださっている方にとって、体裁の整った PDF はテキストの代わりにならない「成果物」なのだと気づかされました。
Rork Max はネイティブ Swift を生成するため、OS に同梱されている PDFKit をそのまま使えます。表示・検索・書き出しまで依存ライブラリの追加なしで完結する、という点が React Native 構成との最も大きな違いです。以下、PDF の表示から書き出し、パスワード保護、共有シート連携までを、動くコードで順に組み上げていきます。
なぜ Rork Max(ネイティブ Swift)で PDF を扱うのか
React Native 版の Rork で PDF を扱う場合、選択肢はサードパーティ製ライブラリか WebView 表示、生成側は expo-print のような HTML 経由のブリッジになります。動くことは動くのですが、依存の保守と表現の細かい制御に限界がありました。
手段
表示
検索・注釈
生成
依存追加
WebView 表示(RN)
△ 簡易表示のみ
✕
✕
不要
react-native-pdf 等(RN)
◯
△ ライブラリ次第
✕
必要
expo-print(RN)
✕
✕
△ HTML 経由
必要
PDFKit(Rork Max / ネイティブ)
◯
◯
◯ UIGraphicsPDFRenderer 併用
不要(OS 同梱)
個人開発では「依存を増やさないこと」自体が保守コストの削減です。PDF まわりを OS 標準の枠組みに寄せられるのは、ネイティブ生成を選ぶ実利のひとつだと考えています。
Rork Max へのプロンプトは、次のような一文から始めるのが実用的でした。
保存済みのメモを PDF として表示・書き出しできる画面を追加してください。表示は PDFKit の PDFView、書き出しは UIGraphicsPDFRenderer を使い、共有シートから送れるようにしてください。
最初のつまずきどころは autoScales です。指定を忘れると PDF が原寸で描画され、画面の隅に豆粒のような表示になります。生成コードに含まれていないことがあるため、表示が小さい場合はまずここを確認してください。
PDFDocument(url:) はページを遅延読み込みするため、数百ページの PDF でも初期表示は軽快です。一方で Data に全読みしてから PDFDocument(data:) に渡す実装は、大きなファイルでメモリを一気に消費します。ローカルファイルなら URL 初期化を使うことをお勧めします。
// キーワード検索して最初の一致へジャンプif let document = pdfView.document { let matches = document.findString("請求書", withOptions: .caseInsensitive) if let first = matches.first { pdfView.go(to: first) pdfView.setCurrentSelection(first, animate: true) }}
import UIKitfunc exportNotesPDF(title: String, body: String) -> URL { let pageRect = CGRect(x: 0, y: 0, width: 595.2, height: 841.8) // A4 (72dpi) let renderer = UIGraphicsPDFRenderer(bounds: pageRect) let url = FileManager.default.temporaryDirectory .appendingPathComponent("notes-\(Int(Date().timeIntervalSince1970)).pdf") let titleAttrs: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 20, weight: .bold) ] let bodyAttrs: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 12), .paragraphStyle: NSParagraphStyle.default ] try? renderer.writePDF(to: url) { context in context.beginPage() title.draw(in: CGRect(x: 48, y: 48, width: 499, height: 40), withAttributes: titleAttrs) let bodyText = NSAttributedString(string: body, attributes: bodyAttrs) let framesetter = CTFramesetterCreateWithAttributedString(bodyText) var currentRange = CFRange(location: 0, length: 0) var firstPage = true repeat { if !firstPage { context.beginPage() } let top: CGFloat = firstPage ? 100 : 48 let textRect = CGRect(x: 48, y: top, width: 499, height: pageRect.height - top - 48) let path = CGPath(rect: textRect, transform: nil) let frame = CTFramesetterCreateFrame(framesetter, currentRange, path, nil) // Core Text は左下原点なので上下反転してから描く let cg = context.cgContext cg.saveGState() cg.translateBy(x: 0, y: pageRect.height) cg.scaleBy(x: 1, y: -1) CTFrameDraw(frame, cg) cg.restoreGState() let visible = CTFrameGetVisibleStringRange(frame) currentRange.location += visible.length firstPage = false } while currentRange.location < bodyText.length } return url}
要点は3つです。第一に、ページ分割は CTFrameGetVisibleStringRange で「描けた文字数」を取り、残りを次ページへ回すループで実現します。第二に、Core Text は座標系が左下原点のため、上下反転してから描画しないと文字が逆さまになります。第三に、日本語はシステムフォント指定でそのまま埋め込まれるため、フォントの同梱作業は不要です。
PDF を作る(2)ImageRenderer — SwiftUI ビューをそのまま1ページに
iOS 16 以降なら、SwiftUI の ImageRenderer でビューをそのまま PDF にできます。レイアウトを SwiftUI で組んだ画面をそのまま「紙」にする発想です。
import SwiftUI@MainActorfunc exportViewAsPDF<V: View>(_ view: V, size: CGSize) -> URL { let url = FileManager.default.temporaryDirectory .appendingPathComponent("summary.pdf") let renderer = ImageRenderer(content: view.frame(width: size.width, height: size.height)) renderer.proposedSize = .init(size) renderer.render { rendered, draw in var box = CGRect(origin: .zero, size: rendered) guard let context = CGContext(url as CFURL, mediaBox: &box, nil) else { return } context.beginPDFPage(nil) draw(context) context.endPDFPage() context.closePDF() } return url}
書き出し機能を載せたバージョンを配信した週から、レビュー欄の「PDF で送れないのか」という声は止まりました。配信から2週間の計測では、書き出しの利用は DAU の約8%で安定し、書き出しを使った利用者の翌週残存は使っていない層より明確に高い、という結果になりました。地味な機能ですが、アプリの外に「成果物」として出ていくファイルは、それ自体が小さな広告にもなります。