追記!
iOS 16から使える ImageRenderer
というAPIが追加されました!
iOS 15を切れる場合は、 ImageRenderer
を使いましょう!!!!!
https://developer.apple.com/documentation/swiftui/imagerenderer
もちべ💪
特定のSwiftUIのViewをShareSheetを使ってシェアするために、特定のViewをUIImageに変換する必要がありました。
最初はUIHostingControllerなどを使ってVIewからUIImageを取得しようとしましたが、上手くいかず...
SwiftUIの問題は、大抵GeometryReaderを使えば解決できるという巷の噂に身を任せたら解決できました☕️
とにかくコード見せてって人はこちらから
https://gist.github.com/tsuzukihashi/41fbb0c594e26e317cfcec878e9948f4
よくわかる解説
まず解説のために適当なViewを用意しました。このViewをUIImageに変換していきます。
struct TestPage: View {
var body: some View {
HStack {
Image(systemName: "sun.haze")
.font(.title)
.foregroundColor(.white)
Text("Hello, World!")
.font(.title)
.foregroundColor(.white)
}
.padding()
.background(Color.blue)
.cornerRadius(8)
}
}
次にUIImageに変換したいViewのサイズと位置を取得したいです。
今回で言うとHStackのCGRect
がわかれば良いでしょう。
ここで一つ目のポイントなのですが、ViewのCGRect
を取得するためにbackground
メソッドを利用します。
backgroundの引数はViewプロトコルに準拠していれば良いので、Viewを返しつつサイズを取得するようなRectangleGetter
を作成します。
以下がbackgroundメソッドの定義です。Viewを返せば良いらしい👀
func background<Background>(_ background: Background, alignment: Alignment = .center) -> some View where Background : View
@Binding
で親ViewのCGRectを指定します。
ここでGeometryReaderの出番です!
GeometryReaderで親Viewのgeometryを取得し、createView
メソッドでViewのサイズと位置情報をBindingしたrectに与えます。
Viewを返さなくてはいけないので透明なRectangle
Viewを返します。
struct RectangleGetter: View {
@Binding var rect: CGRect
var body: some View {
GeometryReader { geometry in
self.createView(proxy: geometry)
}
}
func createView(proxy: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.rect = proxy.frame(in: .global)
}
return Rectangle().fill(Color.clear)
}
}
先ほど作成した、TestPageに@State
のrectを用意します。
HStackのbackgroundにRectangleGetterをかませます。
struct TestPage: View {
//追加 >>>
@State private var rect: CGRect = .zero
// <<<
var body: some View {
HStack {
Image(systemName: "sun.haze")
.font(.title)
.foregroundColor(.white)
Text("Hello, World!")
.font(.title)
.foregroundColor(.white)
}
.padding()
.background(Color.blue)
.cornerRadius(8)
// 追加 >>>
.background(RectangleGetter(rect: $rect))
// <<<
}
}
取得したいViewの位置とサイズが分かったので、最前面の画面が取得できるUIApplication.shared.windows[0].rootViewController
からUIViewを取得します。
UIViewからUIImageに変換するのは従来のやり方でUIGraphicsImageRenderer
を利用します。
UIViewをextensionしてgetImageメソッドを追加します。
extension UIView {
func getImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
そして、TestPageの必要なタイミングでUIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)
を呼ぶことでUIImageを取得することができます。
UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)
struct TestPage: View {
@State private var rect: CGRect = .zero
//追加 >>>
@State var uiImage: UIImage? = nil
// <<<
var body: some View {
//追加 >>>
VStack {
// <<<
HStack {
Image(systemName: "sun.haze")
.font(.title)
.foregroundColor(.white)
Text("Hello, World!")
.font(.title)
.foregroundColor(.white)
}
.padding()
.background(Color.blue)
.cornerRadius(8)
.background(RectangleGetter(rect: $rect))
//追加 >>>
Button(action: {
self.uiImage = UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)
}) {
Text("Button")
.padding()
.foregroundColor(Color.white)
.background(Color.red)
.cornerRadius(8)
}
if uiImage != nil {
VStack {
Image(uiImage: self.uiImage!)
}.background(Color.green)
.frame(width: 500, height: 500)
}
}
// <<<
}
}
以上のようになり。ボタンをタップすると
まとめ
SwiftUIのViewからUIImageを取得するためには
- View全体を得るためにbackgroundを使う
- GeometryReaderを使ってCGRectを取得する
- そのCGRectを使ってUIViewからUIImageを取得する
完成したコード
https://gist.github.com/tsuzukihashi/41fbb0c594e26e317cfcec878e9948f4
問題点
この方法は完全にViewだけをUIImageにしているのではなく、Viewの範囲をとってUIImageに変換しています。
したがって、このViewの上に別のViewがかぶっていた場合でも、かぶった範囲が一緒にUIImageになってしまいます。
参考にした記事URL
iOS:最前面に画面を出す/最前面の画面を知る
SwiftUIの肝となるGeometryReaderについて理解を深め
SwiftUIでChildViewのFrameを取得する