SwiftUI でViewをUIImageに変換する方法

# 追記！
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に変換していきます。

![スクリーンショット 2019-12-01 15.06.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/6f476d8a-39fe-0b84-2b69-e84dc0a0a70d.png)

```swift
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を返せば良いらしい👀

```swift
func background<Background>(_ background: Background, alignment: Alignment = .center) -> some View where Background : View
```

https://developer.apple.com/documentation/swiftui/view/3278516-background

`@Binding`で親ViewのCGRectを指定します。
ここでGeometryReaderの出番です！
GeometryReaderで親Viewのgeometryを取得し、`createView`メソッドでViewのサイズと位置情報をBindingしたrectに与えます。
Viewを返さなくてはいけないので透明な`Rectangle`Viewを返します。

```swift
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をかませます。

```swift
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メソッドを追加します。



```swift
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を取得することができます。

```swift
UIApplication.shared.windows[0].rootViewController?.view!.getImage(rect: self.rect)
```

```swift
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)
            }
        }
    // <<<
    }
}
```


![スクリーンショット 2019-12-01 15.01.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/2a841c1b-999b-d865-d9f1-66623106a65f.png)

以上のようになり。ボタンをタップすると

Imageが表示されます。ありがとうございました🙇‍♂️
![スクリーンショット 2019-12-01 14.55.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171702/50aea6f6-0e6d-ea7e-41d1-7aa9a2217982.png)

## まとめ
SwiftUIのViewからUIImageを取得するためには
1. View全体を得るためにbackgroundを使う
2. GeometryReaderを使ってCGRectを取得する
3. そのCGRectを使ってUIViewからUIImageを取得する

完成したコード
https://gist.github.com/tsuzukihashi/41fbb0c594e26e317cfcec878e9948f4
## 問題点

 この方法は完全にViewだけをUIImageにしているのではなく、Viewの範囲をとってUIImageに変換しています。
したがって、このViewの上に別のViewがかぶっていた場合でも、かぶった範囲が一緒にUIImageになってしまいます。


## 参考にした記事URL

[iOS:最前面に画面を出す/最前面の画面を知る](https://qiita.com/fuwamaki/items/5c72a9a1187e78034aa2) 
[SwiftUIの肝となるGeometryReaderについて理解を深め](https://qiita.com/masa7351/items/0567969f93cc88d714ac)
[SwiftUIでChildViewのFrameを取得する](https://qiita.com/1amageek/items/7dbb0d5000ed06923206)