SwiftUIレイアウトで複数ページのPDFを作成する

前回のPDFKitでPDFを読み込んで結合した際に透過部分が黒塗りになる現象が発生することがわかった。

- 前回までの記事
    - [SwiftUIのレイアウトでPDFを作成する](https://qiita.com/mbotsu/items/1c1a49efed96f8ae145f)
    - [SwiftUIのレイアウトでPDFを作成して結合する](https://qiita.com/mbotsu/items/4f091781901c84e69ab0)

どうやらPDFのページ追加時に`context.cgContext.clear(rect)`で毎回コンテキストをクリアする必要があるようだが、PDFKitで読み込んだ場合のコンテキストの操作がわからないので諦めた。

なのでPDF作成時に複数ページを書き込んでから保存するのが妥当と判断。修正したコードがこちら。
これでSwiftUIで複数ページのPDFが安定して作成できる筈だ。
前回のPDFKitのページ結合時の透過現象の解決方法はいつか解る筈。。。

## 修正(2023/09/25)

メインスレッドのTask内でPDFコンテキストに`rootViewController`でレンダリングすると表示中のViewがPDFと混ざることが確認できた。
なので`rootViewController`を用いたやり方は実用的とは言えないので没とした。
色々試した結果Task内で最新のViewの内容を`drawHierarchy`でコンテキストに書き出せることが判明した。

次に、何故Task内であれば`drawHierarchy`でコンテキストに書き込めるのか？
これも試していてようやく解った。
UIViewControllerの初期化ができていないのだ。
Taskに書いたことによって、非同期のタイミングでViewの初期化が走っていたようだ。
また、次に試していて解ったのが`drawHierarchy`を`afterScreenUpdates:true`で行うことでも初期化が実施されているようだ。
ただ非同期を外すと、何故かコンテキストには書き込まれない。これも不思議だ。

結果的に、`loadView`、`drawHierarchy`、`layer.render`を順に実行することでコンテキストに初期化したViewが書き込めることが判明した。

### 残った疑問としては、
- `drawHierarchy`、`layer.render`の順で実行することで、Viewの初期化とコンテキストの書き込みが何故成功するのか？
    - 要素の整理
        - `loadView`: Viewを読み込む
        - `drawHierarchy`: 初期化後のViewに更新（この時何故か書き込まない）
        - `layer.render`: コンテキストにViewを書き込む
- Task内であれば、`drawHierarchy`だけでコンテキストが何故書き込めるのか？
    - 非同期のタイミングで初期化。仕様通りコンテキストの書き込みも行う

~~つまり非同期でない時に`drawHierarchy`がコンテキストに書き込まないのがバグと考えるのが自然？~~
非同期の時は`viewDidLoad`が自動で走るようにレンダリングも行われると考えるのが自然か。わからんけど。カスタムクラスでも作って動作を見るのが妥当かな。

### 追及してみる

createPdfでviewsに渡す値をUIViewControllerに変更してテストしてみた。


```swift
class ViewController: UIViewController {
  init() {
    super.init(nibName: nil, bundle: nil)
    print("init")
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  override func viewDidLoad() {
    print("viewDidLoad Start")
    super.viewDidLoad()
    view.backgroundColor = .white
    let label = UILabel(frame: CGRectMake(0, 0, 100, 100))
    label.text = "Page1"
    view.addSubview(label)
    print("viewDidLoad End")
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    print("viewDidAppear")
  }
}

func _createPdf(_ vc: UIViewController, rect: CGRect,
                context: UIGraphicsPDFRendererContext){
  
  print("==1")
  vc.view.frame = rect
  print("==2")
  
  context.beginPage()
  context.cgContext.clear(rect)
  print("==3")
  vc.view.layer.render(in: context.cgContext)
  print("==4")
  
  vc.removeFromParent()
  vc.view.removeFromSuperview()
}
```

結果はframeでサイズを設定した時点でviewDidLoadが実行となった。
また、`loadView`、`drawHierarchy`を行わなくても書き込めた。
UIViewControllerの挙動としては本来このように動くのが正しいのだろう。

```
init
==1
viewDidLoad Start
viewDidLoad End
==2
==3
==4
```

戻って、元のコードで`drawHierarchy`を実行した場合には下記のメッセージが出ていた。
```
2023-09-22 08:00:51.668624+0900 PDFExample[3659:71578] [Snapshotting] View (0x13b51bb80, _TtGC7SwiftUI14_UIHostingViewVS_7AnyView_) drawing with afterScreenUpdates:YES inside CoreAnimation commit is not supported.
```

警告が出るということは何か間違えているのだろうということで、`snapshotView`を用いたところメッセージは出なくなった。
また、`snapshotView`を実行すると`loadView`も不要ということがわかった。


[snapshotview](https://developer.apple.com/documentation/uikit/uiview/1622531-snapshotview)のドキュメントによると「ぼかしなどのグラフィカルエフェクトを適用」したい場合は`drawHierarchy`をコールするのが良いと書かれているので単にViewを初期化したいのであれば`snapshotview`を呼ぶのが正解なのだろう。

以上から修正内容。
`drawHierarchy`を使わないのでコンテキストに何故書き込まないのか悩むこともなくなりスッキリした。

#### Before
```swift
func _createPdf(_ view: AnyView, rect: CGRect,
                context: UIGraphicsPDFRendererContext){
  
  let vc = UIHostingController(rootView: view)
  vc.view.frame = rect
  vc.loadView()
  
  context.beginPage()
  context.cgContext.clear(rect)
  vc.view.drawHierarchy(in: rect, afterScreenUpdates: true)
  vc.view.layer.render(in: context.cgContext)
  
  vc.removeFromParent()
  vc.view.removeFromSuperview()
}
```

#### After
```swift
func _createPdf(_ view: AnyView, rect: CGRect,
                context: UIGraphicsPDFRendererContext){
  
  let vc = UIHostingController(rootView: view)
  vc.view.frame = rect
  vc.view.snapshotView(afterScreenUpdates: true)
  
  context.beginPage()
  context.cgContext.clear(rect)
  vc.view.layer.render(in: context.cgContext)
  
  vc.removeFromParent()
  vc.view.removeFromSuperview()
}
```

### 修正したコードがこちら

https://gist.github.com/otmb/49ad207a0b22f2962c52c0e19c6c0241