Slide 1

Slide 1 text

PencilKitで実装する PDFへの手書き注釈 iOSDC Japan 2024 Day2 Track B Ras (@ras0q)

Slide 2

Slide 2 text

Apple Pencilを活用した iOSアプリを作りたい!

Slide 3

Slide 3 text

Apple Pencilを活用したiOSアプリを作りたい! アプリの機能の1つとして手書きの描画機能を実装したい お絵かき 手書きメモ 世の中のPDF注釈アプリが需要にマッチしないため自作したい 複雑すぎる操作UI ファイルのアクセス制限 無限に現れる広告 など… このトークを見ることでApple Pencilを一層活用できるようになります!!!

Slide 4

Slide 4 text

Keynote 1. PencilKitとは? 2. PencilKitでアプリを作る 3. PDFに描画する 4. 描画をPDF注釈として保存する 5. PencilKitを使ってみた感想 ↓↓サンプルレポジトリ ↓↓ github.com/ras0q/iosdc2024 Powered by

Slide 5

Slide 5 text

Ras ras0q.com 東京工業大学 大学院 修士1年 10月に大学が改名するらしい デジタル創作同好会traP Web開発をメインに創作をしています traPortfolio をリリースしました ピクシブ株式会社 アルバイト iOSアプリエンジニア育成プロジェクト pixiv / pixiv Sketch / Pastela iOSDC Japan 参加(3) 登壇(2)

Slide 6

Slide 6 text

Keynote 1. PencilKitとは 2. PencilKitでアプリを作る 3. PDFに描画する 4. 描画をPDF注釈として保存する 5. PencilKitを使ってみた感想 ↓↓サンプルレポジトリ ↓↓ github.com/ras0q/iosdc2024

Slide 7

Slide 7 text

PencilKit?

Slide 8

Slide 8 text

PencilKit? 引用元: WWDC19

Slide 9

Slide 9 text

PencilKit ドローイングをiOSアプリに組み込むことができる純正ライブラリ 指やApple Pencilからの入力を受け取ってアプリで使う画像データに変換する 描画ツール搭載 鉛筆のほかに 万年筆や定規も... 純正アプリにも ファイル, メモ, 写真... アプリ間の連携 別アプリへ図形の コピペが可能

Slide 10

Slide 10 text

Keynote 1. PencilKitとは? ✅ 2. PencilKitでアプリを作る 3. PDFに描画する 4. 描画をPDF注釈として保存する 5. PencilKitを使ってみた感想 ↓↓サンプルレポジトリ ↓↓ github.com/ras0q/iosdc2024

Slide 11

Slide 11 text

try! PencilKit in 1 minute

Slide 12

Slide 12 text

try! PencilKit import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } }

Slide 13

Slide 13 text

try! PencilKit import PencilKit private lazy var canvasView = PKCanvasView(frame: view.frame) view.addSubview(canvasView) import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } }

Slide 14

Slide 14 text

try! PencilKit private lazy var toolPicker = PKToolPicker() toolPicker.addObserver(canvasView) toolPicker.setVisible(true, forFirstResponder: canvasView) canvasView.becomeFirstResponder() import PencilKit import UIKit class ViewController: UIViewController { private lazy var canvasView = PKCanvasView(frame: view.frame) override func viewDidLoad() { super.viewDidLoad() view.addSubview(canvasView) } }

Slide 15

Slide 15 text

try! PencilKit import PencilKit import UIKit class ViewController: UIViewController { private lazy var canvasView = PKCanvasView(frame: view.frame) private lazy var toolPicker = PKToolPicker() override func viewDidLoad() { super.viewDidLoad() view.addSubview(canvasView) toolPicker.addObserver(canvasView) toolPicker.setVisible(true, forFirstResponder: canvasView) canvasView.becomeFirstResponder() } }

Slide 16

Slide 16 text

✅ PencilKitでアプリを作る

Slide 17

Slide 17 text

Keynote 1. PencilKitとは? ✅ 2. PencilKitでアプリを作る ✅ 3. PDFに描画する 4. 描画をPDF注釈として保存する 5. PencilKitを使ってみた感想 ↓↓サンプルレポジトリ ↓↓ github.com/ras0q/iosdc2024

Slide 18

Slide 18 text

try! PDF Integration

Slide 19

Slide 19 text

PDFKit import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } }

Slide 20

Slide 20 text

PDFKit import PDFKit private lazy var pdfDocument = PDFDocument(somePDFURL) import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } }

Slide 21

Slide 21 text

PDFKit private lazy var pdfView: PDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument return view }() view.addSubview(pdfView) import PDFKit import UIKit class ViewController: UIViewController { private lazy var pdfDocument = PDFDocument(somePDFURL) override func viewDidLoad() { super.viewDidLoad() } }

Slide 22

Slide 22 text

PDFKit import PDFKit import UIKit class ViewController: UIViewController { private lazy var pdfDocument = PDFDocument(somePDFURL) private lazy var pdfView: PDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument return view }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(pdfView) } }

Slide 23

Slide 23 text

✅ 画面にPDFを表示する

Slide 24

Slide 24 text

PencilKitを使ってPDFに描画できるようにするには?

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

PDFPageOverlayViewProvider class ViewController: UIViewController { private lazy var pdfView: PDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument return view }() }

Slide 30

Slide 30 text

PDFPageOverlayViewProvider view.pageOverlayViewProvider = self extension ViewController: PDFPageOverlayViewProvider { func pdfView(_: PDFView, overlayViewFor page: PDFPage) -> UIView? { } } class ViewController: UIViewController { private lazy var pdfView: PDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument return view }() }

Slide 31

Slide 31 text

PDFPageOverlayViewProvider // 各ページに被せるキャンバスを作成 private lazy var canvasViews = (0..<ページ数).map { _ in PKCanvasView() } // ページ番号に対応するキャンバスを返す canvasViews[page.ページ番号] class ViewController: UIViewController { private lazy var pdfView: PDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument view.pageOverlayViewProvider = self return view }() } extension ViewController: PDFPageOverlayViewProvider { func pdfView(_: PDFView, overlayViewFor page: PDFPage) -> UIView? {

Slide 32

Slide 32 text

PDFPageOverlayViewProvider class ViewController: UIViewController { private lazy var pdfView: PDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument view.pageOverlayViewProvider = self return view }() // 各ページに被せるキャンバスを作成 private lazy var canvasViews = (0..<ページ数).map { _ in PKCanvasView() } } extension ViewController: PDFPageOverlayViewProvider { func pdfView(_: PDFView, overlayViewFor page: PDFPage) -> UIView? { // ページ番号に対応するキャンバスを返す canvasViews[page.ページ番号]

Slide 33

Slide 33 text

描画ツールも設定する class ViewController: UIViewController { private lazy var pdfView: PDFView = ... private lazy var canvasViews = ... override func viewDidLoad() { // PDFViewの設定... } }

Slide 34

Slide 34 text

描画ツールも設定する private lazy var toolPicker = PKToolPicker() for canvasView in canvasViews { toolPicker.addObserver(canvasView) } // 今回のfirst responderはPDFView toolPicker.setVisible(true, forFirstResponder: pdfView) pdfView.becomeFirstResponder() class ViewController: UIViewController { private lazy var pdfView: PDFView = ... private lazy var canvasViews = ... override func viewDidLoad() { // PDFViewの設定... } }

Slide 35

Slide 35 text

描画ツールも設定する pdfView.addGestureRecognizer(canvasView.drawingGestureRecognizer) class ViewController: UIViewController { private lazy var pdfView: PDFView = ... private lazy var canvasViews = ... private lazy var toolPicker = PKToolPicker() override func viewDidLoad() { // PDFViewの設定... for canvasView in canvasViews { toolPicker.addObserver(canvasView) } // 今回のfirst responderはPDFView toolPicker.setVisible(true, forFirstResponder: pdfView) pdfView.becomeFirstResponder() } }

Slide 36

Slide 36 text

描画ツールも設定する class ViewController: UIViewController { private lazy var pdfView: PDFView = ... private lazy var canvasViews = ... private lazy var toolPicker = PKToolPicker() override func viewDidLoad() { // PDFViewの設定... for canvasView in canvasViews { toolPicker.addObserver(canvasView) pdfView.addGestureRecognizer(canvasView.drawingGestureRecognizer) } // 今回のfirst responderはPDFView toolPicker.setVisible(true, forFirstResponder: pdfView) pdfView.becomeFirstResponder() } }

Slide 37

Slide 37 text

❓ PDFに描画する (完成…?) 最後のページにしか描画できない (動画は上のページにも描こうとしています) WARNING: "Drawing did change that is not in text."

Slide 38

Slide 38 text

❓ PDFに描画する (完成…?) 最後のページにしか描画できない (動画は上のページにも描こうとしています) WARNING: "Drawing did change that is not in text." pdfView.addGestureRecognizer(...) 1. Recognizerの認識範囲はPDFView全体 2. 複数追加されたRecognizerの範囲が重複する 3. 最後に追加されたRecognizerのみが発火する → 最後のページしか正常に認識されない 😭

Slide 39

Slide 39 text

❓ PDFに描画する (完成…?) 最後のページにしか描画できない (動画は上のページにも描こうとしています) WARNING: "Drawing did change that is not in text." pdfView.addGestureRecognizer(...) 1. Recognizerの認識範囲はPDFView全体 2. 複数追加されたRecognizerの範囲が重複する 3. 最後に追加されたRecognizerのみが発火する → 最後のページしか正常に認識されない 😭 → 描画ごとに発火させるRecognizerを切り替える必要がある

Slide 40

Slide 40 text

Override func hitTest(_:with:) タップしたページのキャンバスのRecognizerのみを有効化させる class CanvasPDFView: PDFView { override func hitTest(_ point:CGPoint,with e:UIEvent?) -> UIView? { return super.hitTest(point, with: e) } }

Slide 41

Slide 41 text

Override func hitTest(_:with:) タップしたページのキャンバスのRecognizerのみを有効化させる if let activePage = page(for: point, nearest: true) { } class CanvasPDFView: PDFView { override func hitTest(_ point:CGPoint,with e:UIEvent?) -> UIView? { return super.hitTest(point, with: e) } }

Slide 42

Slide 42 text

Override func hitTest(_:with:) タップしたページのキャンバスのRecognizerのみを有効化させる protocol CanvasPDFViewDelegate: AnyObject { func switchActivePage(to page: PDFPage) } var interactionDelegate: (any CanvasPDFViewDelegate)? interactionDelegate?.switchActivePage(to: activePage) class CanvasPDFView: PDFView { override func hitTest(_ point:CGPoint,with e:UIEvent?) -> UIView? { if let activePage = page(for: point, nearest: true) { } return super.hitTest(point, with: e) } }

Slide 43

Slide 43 text

ViewController側で有効なRecognizerを切り替える class ViewController: UIViewController { private lazy var pdfView: PDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument view.pageOverlayViewProvider = self return view }() }

Slide 44

Slide 44 text

ViewController側で有効なRecognizerを切り替える class ViewController: UIViewController { private lazy var pdfView: CanvasPDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument view.pageOverlayViewProvider = self return view }() }

Slide 45

Slide 45 text

ViewController側で有効なRecognizerを切り替える view.interactionDelegate = self extension ViewController: CanvasPDFViewDelegate { func switchActivePage(to page: PDFPage) { let activeCanvasView = canvasView[page.ページ番号] for canvasView in canvasViews { let isActiveView = (canvasView == activeCanvasView) canvasView.drawingGestureRecognizer.isEnabled = isActiveView } class ViewController: UIViewController { private lazy var pdfView: CanvasPDFView = { let view = PDFView(frame: view.frame) view.document = pdfDocument view.pageOverlayViewProvider = self return view }() }

Slide 46

Slide 46 text

✅ PDFに描画する

Slide 47

Slide 47 text

Keynote 1. PencilKitとは? ✅ 2. PencilKitでアプリを作る ✅ 3. PDFに描画する ✅ 4. 描画をPDF注釈として保存する 5. PencilKitを使ってみた感想 ↓↓サンプルレポジトリ ↓↓ github.com/ras0q/iosdc2024

Slide 48

Slide 48 text

try! PDF Annotation

Slide 49

Slide 49 text

PKCanvasView → PDFView

Slide 50

Slide 50 text

PKCanvasView → PDFView

Slide 51

Slide 51 text

注釈用クラスのイニシャライザ class CanvasPDFAnnotation: PDFAnnotation { private let drawing: PKDrawing init(drawing: PKDrawing, page: PDFPage) { self.drawing = drawing var pdfBounds = drawing.bounds pdfBounds.origin.y = page.bounds(for: .mediaBox).height - drawing.bounds.height - drawing.bounds.origin.y super.init(bounds: pdfBounds, forType: .ink) self.page = page } }

Slide 52

Slide 52 text

注釈用クラスのイニシャライザ var pdfBounds = drawing.bounds pdfBounds.origin.y = page.bounds(for: .mediaBox).height - drawing.bounds.height - drawing.bounds.origin.y class CanvasPDFAnnotation: PDFAnnotation { private let drawing: PKDrawing init(drawing: PKDrawing, page: PDFPage) { self.drawing = drawing super.init(bounds: pdfBounds, forType: .ink) self.page = page } }

Slide 53

Slide 53 text

注釈用クラスのイニシャライザ これは? var pdfBounds = drawing.bounds pdfBounds.origin.y = page.bounds(for: .mediaBox).height - drawing.bounds.height - drawing.bounds.origin.y class CanvasPDFAnnotation: PDFAnnotation { private let drawing: PKDrawing init(drawing: PKDrawing, page: PDFPage) { self.drawing = drawing super.init(bounds: pdfBounds, forType: .ink) self.page = page } }

Slide 54

Slide 54 text

View Coordinates → PDF Coordinates 座標系をy軸反転させる必要がある boundsの基準点も変わる 図形が反転するわけではない

Slide 55

Slide 55 text

再掲: 注釈用クラスのイニシャライザ var pdfBounds = drawing.bounds pdfBounds.origin.y = page.bounds(for: .mediaBox).height - drawing.bounds.height - drawing.bounds.origin.y class CanvasPDFAnnotation: PDFAnnotation { private let drawing: PKDrawing init(drawing: PKDrawing, page: PDFPage) { self.drawing = drawing super.init(bounds: pdfBounds, forType: .ink) self.page = page } }

Slide 56

Slide 56 text

キャンバスの描画を注釈として追加する 注釈の追加/更新時に PDFAnnotation#draw() が呼ばれる class CanvasPDFAnnotation: PDFAnnotation { override func draw(with box: PDFDisplayBox,in context: CGContext) { super.draw(with: box, in: context) } }

Slide 57

Slide 57 text

キャンバスの描画を注釈として追加する 注釈の追加/更新時に PDFAnnotation#draw() が呼ばれる UIGraphicsPushContext(context) context.saveGState() defer { context.restoreGState() UIGraphicsPopContext() } class CanvasPDFAnnotation: PDFAnnotation { override func draw(with box: PDFDisplayBox,in context: CGContext) { super.draw(with: box, in: context) } }

Slide 58

Slide 58 text

キャンバスの描画を注釈として追加する 注釈の追加/更新時に PDFAnnotation#draw() が呼ばれる let image = drawing.image(from: drawing.bounds, scale: 1.0) class CanvasPDFAnnotation: PDFAnnotation { override func draw(with box: PDFDisplayBox,in context: CGContext) { super.draw(with: box, in: context) UIGraphicsPushContext(context) context.saveGState() defer { context.restoreGState() UIGraphicsPopContext() } } }

Slide 59

Slide 59 text

キャンバスの描画を注釈として追加する 注釈の追加/更新時に PDFAnnotation#draw() が呼ばれる context.draw(image.cgImage!, in: bounds) // PDF座標系のbounds class CanvasPDFAnnotation: PDFAnnotation { override func draw(with box: PDFDisplayBox,in context: CGContext) { super.draw(with: box, in: context) UIGraphicsPushContext(context) context.saveGState() defer { context.restoreGState() UIGraphicsPopContext() } let image = drawing.image(from: drawing.bounds, scale: 1.0) } }

Slide 60

Slide 60 text

キャンバスの描画を注釈として追加する 注釈の追加/更新時に PDFAnnotation#draw() が呼ばれる class CanvasPDFAnnotation: PDFAnnotation { override func draw(with box: PDFDisplayBox,in context: CGContext) { super.draw(with: box, in: context) UIGraphicsPushContext(context) context.saveGState() defer { context.restoreGState() UIGraphicsPopContext() } let image = drawing.image(from: drawing.bounds, scale: 1.0) context.draw(image.cgImage!, in: bounds) // PDF座標系のbounds } }

Slide 61

Slide 61 text

PDF注釈の使用例 ページ全体の描画を1つの注釈に 各ストロークの描画をそれぞれの注釈に let annotation = CanvasPDFAnnotation( drawing: canvasView.drawing, page: page ) page.addAnnotation(annotation) for stroke in canvasView.drawing.strokes { let annotation = CanvasPDFAnnotation( drawing: PKDrawing(strokes: [stroke]), page: page ) page.addAnnotation(annotation) }

Slide 62

Slide 62 text

PDFView → Raw PDF File

Slide 63

Slide 63 text

PDFView → Raw PDF File

Slide 64

Slide 64 text

注釈をファイルに保存する PDFDocument#dataRepresentation() から Data を抽出 ファイルURLを指定し書き込む let data = pdfDocument.dataRepresentation() let documentURL = pdfDocument.documentURL try data.write(to: documentURL)

Slide 65

Slide 65 text

✅ 描画をPDF注釈として保存する

Slide 66

Slide 66 text

Keynote 1. PencilKitとは? ✅ 2. PencilKitでアプリを作る ✅ 3. PDFに描画する ✅ 4. 描画をPDF注釈として保存する ✅ 5. PencilKitを使ってみた感想 ↓↓サンプルレポジトリ ↓↓ github.com/ras0q/iosdc2024

Slide 67

Slide 67 text

PencilKitを使ってみた感想

Slide 68

Slide 68 text

ご清聴ありがとうございました! サンプルレポジトリ も是非ご覧ください! Presenter: Ras (@ras0q )

Slide 69

Slide 69 text

References PencilKit | Apple Developer Documentation PDFKit | Apple Developer Documentation What’s new in PDFKit - WWDC22 - Videos - Apple Developer iOSのPDFKitを利用してPDFを編集する | Zenn