[SwiftUI] GeometryReaderで特定のViewをキャプチャする
前回SwiftUIでお絵かきアプリの記事で指をドラッグすることでPathを作りお絵かきアプリを作りました。
作った絵は保存したくなりますよね?
というわけで今回はSwiftUIで特定のViewをキャプチャして画像にする方法を解説します。
サンプルコード
今回解説するサンプルコードはこちらです。
アプリを試してみたい方は
https://github.com/SatoTakeshiX/SwiftUICatalog
をクローンしてください。
キャプチャをする流れ
SwiftUIそのものにはまだViewからイメージデータに変換するメソッドはありません。
しかしUIKitではImageContext
を作成してUIViewのCALayerをレンダーすることでUIViewを画像データに変換することはできます。
そしてSwiftUIはUIHostingController
を通してSwiftUIからUIViewに変換できます。
これを利用すると、
- SwiftUIのViewをUIViewに変換
- UIViewでキャプチャする
- SwiftUIからキャプチャ画像を取得する
という手順を踏めばSwiftUIのViewを画像データに変換することができるのです。
お絵かきアプリ
今回のお絵かきアプリのキャプチャです。
Canvas
Viewに指を触れると文字がかけるViewです。
下の「保存」ボタンを押すとキャプチャがとられる仕組みです。
SwiftUIのViewをUIViewに変換
SwiftUIのViewをUIViewに変換します。
コードは次のとおりです。
extension DrawEditor {
func captureCanvas(canvasRect: CGRect,
endedDrawPoints: [DrawPoints],
tmpDrawPoints: DrawPoints,
startPoint: CGPoint,
selectedColor: DrawType) -> UIImage {
let window = UIWindow(frame: CGRect(origin: canvasRect.origin,
size: canvasRect.size))
let canvas = Canvas(endedDrawPoints: .constant(endedDrawPoints),
tmpDrawPoints: .constant(tmpDrawPoints),
startPoint: .constant(startPoint),
selectedColor: .constant(selectedColor),
canvasRect: .constant(canvasRect))
let hosting = UIHostingController(rootView: canvas)
hosting.view.frame = window.frame
window.addSubview(hosting.view)
window.makeKeyAndVisible()
return hosting.view.renderedImage
}
}
引数が多くてごちゃごちゃしていますがUIWindow
をCanvas
Viewの大きさのrectで作成します。
次に引数のパラメーター、これにはユーザーがドローイングした情報がはいっています、でCanvas
Viewを作成してUIHostingController
でSwiftUIをUIViewControllerに変換します。あとはwindowにこのView Controllerを貼り付けてView Controllerのviewに対してrenderedImage
を実行します。
UIViewでキャプチャする方法
ではまずUIViewでキャプチャする方法をみていきます。
次のようなextensionを作成しました。
extension UIView {
var renderedImage: UIImage {
// rect of capure
let rect = self.bounds
// create the context of bitmap
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context: CGContext = UIGraphicsGetCurrentContext()!
self.layer.render(in: context)
// get a image from current context bitmap
let capturedImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return capturedImage
}
}
UIGraphicsBeginImageContextWithOptions
でUIViewのサイズのコンテキストを作成し、CGContext
を取り出して、self.layer.render(in: context)
でコンテキストにUIViewのレイヤーを転写します。そしてUIGraphicsGetImageFromCurrentImageContext
でUIImageとして画像を取得しています。
SwiftUIからキャプチャ画像を取得する
ButtonのactionでSwiftUIのキャプチャをとってみます。
Button(action: {
let image = self.captureCanvas(canvasRect: self.canvasRect, endedDrawPoints: self.endedDrawPoints, tmpDrawPoints: self.tmpDrawPoints, startPoint: self.startPoint, selectedColor: self.selectedColor)
print(image)
}) { Text("保存")
}
「保存」ボタンを押すとキャプチャ画像が取得されます。
キャンバスの座標情報の受け渡し
キャンバスの座標情報はそのままでは親Viewは知り得ない情報なので、子Viewから親Viewに座標情報を受け渡す必要があります。
プロパティにcanvasRect
を追加し、Canvas
Viewを作成する際に$canvasRect
で渡します。
struct DrawEditor: View {
...
@State var canvasRect: CGRect = .zero
var body: some View {
VStack {
Canvas(endedDrawPoints: $endedDrawPoints,
tmpDrawPoints: $tmpDrawPoints,
startPoint: $startPoint,
selectedColor: $selectedColor,
canvasRect: $canvasRect)
Canvas
の方ではGeometryReader
をトップレベルのビューにし、クロージャーから渡された座標情報を.onAppear
メソッドでcanvasRect
に代入します。
struct Canvas: View {
...
@Binding var canvasRect: CGRect
var body: some View {
GeometryReader { geometry in
Rectangle()
.....
.onAppear {
self.canvasRect = geometry.frame(in: .global)
}
}
}
すると親ビューにcanvasRect
の情報が渡るのでこの座標情報からキャプチャをとることができます。
つまったこと
最初実装した際にはキャンバスのCGRectのみを使い、UIHostingController
にself
を渡して実装していました。
extension DrawEditor {
func captureCanvas(canvasRect: CGRect) -> UIImage {
let window = UIWindow(frame: CGRect(origin: canvasRect.origin,
size: canvasRect.size))
let hosting = UIHostingController(rootView: self)
hosting.view.frame = window.frame
window.addSubview(hosting.view)
window.makeKeyAndVisible()
return hosting.view.renderedImage
}
}
するとself.layer.render(in: context)
でエラーになりました。
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
というエラーでアプリが落ちてしまいます。
正直このエラーの原因がわからない状態です。子のViewの座標情報を親Viewからレンダリングしているのが悪かったのでしょうか?UIKitとSwiftUIの橋渡しでイメージコンテキストがおかしくなっているのかもしれません。
なので、先程紹介したとおり、「キャンバスのデータは保持しておいて、Canvas
を新たに作り直してキャプチャする」という方式を取りました。
let canvas = Canvas(endedDrawPoints: .constant(endedDrawPoints),
tmpDrawPoints: .constant(tmpDrawPoints),
startPoint: .constant(startPoint),
selectedColor: .constant(selectedColor),
canvasRect: .constant(canvasRect))
let hosting = UIHostingController(rootView: canvas)
まとめ
- SwiftUIだけだとViewのキャプチャはできない
- SwiftUIをUIViewに変換してUIViewでキャプチャをする
- SwiftUIでselfを渡すと
self.layer.render(in: context)
でエラー。Viewの情報を外から渡して作り直そう。
宣伝
インプレスR&D社より、「1人でアプリを作る人を支えるSwiftUI開発レシピ」発売中です。
「SwiftUIでアプリを作る!」をコンセプトにSwiftUI自体の解説とそれを組み合わせた豊富なサンプルアプリでどんな風にアプリ実装すればいいかが理解できる本となっています。
iOS 14対応、Widgetの作成も一章まるまるハンズオンで解説しています。
SwiftUIを学びたい方、ぜひこちらのリンクをチェックしてください!