LoginSignup
23
14

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

Last updated at Posted at 2019-12-01

追記!

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

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を返さなくてはいけないので透明なRectangleViewを返します。

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

スクリーンショット 2019-12-01 15.01.00.png

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

Imageが表示されます。ありがとうございました🙇‍♂️
スクリーンショット 2019-12-01 14.55.32.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:最前面に画面を出す/最前面の画面を知る
SwiftUIの肝となるGeometryReaderについて理解を深め
SwiftUIでChildViewのFrameを取得する

23
14
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
14