MILLEN BOX

音楽好きの組み込みソフトエンジニアによるプログラミング(主にiOSアプリ開発)の勉強の記録

UIView(withアニメーション)からUIImageへの変換する時の注意点

お疲れ様です。

UIViewからUIImageへの変換って割とよく行う処理かと思います。
今まで私は以下みたいなUIViewのExtensionペタッと貼り付けて、任意のタイミングで使用していました。

fileprivate extension UIView {
    
    /**
   対象のViewをキャプチャしUIImageで保存
   
   - returns : キャプチャ画像 (UIImage)
   */
    fileprivate func getCaptureImageFromView() -> UIImage? {
        
        let contextSize = self.frame.size
        
        UIGraphicsBeginImageContextWithOptions(contextSize, false, 0.0)       //コンテキストを作成
        self.layer.render(in: UIGraphicsGetCurrentContext()!)              //画像にしたいViewのコンテキストを取得
        
        let captureImage = UIGraphicsGetImageFromCurrentImageContext()        //取得したコンテキストをUIImageに変換
        UIGraphicsEndImageContext()                                         //コンテキストを閉じる
        
        return captureImage
        
    }
    
    
}

上記を機嫌よく使っていたのですが、以下のような問題が発生しました。

アニメーションが入ったViewをキャプチャしたとき思てたんと違う

今回、アニメーション付きのViewを内部で保持している親Viewをキャプチャーしようと思いました。
アニメーションしているViewと瞬間の表示位置も正しく、です。

しかし結果は…
どのタイミングでキャプチャーしてもアニメーションViewがずっとおんなじ場所じゃん!!
なんで?ねぇ何で??

コードを見直してみました。
そしてあることに気がつきました。

self.layer.render(in: UIGraphicsGetCurrentContext()!)

ん??このコンテキストを取得している部分、対象ViewのCALayerに対してレンダリングしてるな…。
一方、今回のアニメーションは animate(withDuration:animations:completion:) で行なっていました。
この方法ってViewの座標位置を示すポイント値は変化せずにViewがアニメーションしているんですよね。
しかしコンテキストを取得した対象は対象ViewのCALayer…。
一つの仮説が生まれました。

viewが保持している座標位置に従ってコンテキストを取得している!

上記が正しいのかどうかちゃんと調査してませんが、多分そんなに遠い憶測ではないと思っています。

それはそうと、うーん、これは困りました…。
今回はどうしても animate(withDuration:animations:completion:) でアニメーションしているViewを任意のタイミングでキャプチャしたかったんです。
(試してませんが、アニメーションをCABasicAnimationで行なった場合にはちゃんとアニメーションしている位置に従ってキャプチャできると思われます。
がしかし、そこでView側にて制限を加えたくなかったのです。)

他の人の書いたUIView→UIImageのコードを眺めるじーっと眺めてみることにしました。
その時間、約一時間くらい。
解決策がないか探って見ました。

すると、、、
おんなじ様に見えていたコードが、よく見ると違うものが混じっていることに気づいてきました。
そして解答に行き着いたのです。 drawHierarchy(in:afterScreenUpdates:) に。

どうやらiOS7から追加されたメソッドの模様。
リファレンスには以下の記載があります。

Renders a snapshot of the complete view hierarchy as visible onscreen into the current context.

なんかいけそうな感じがする!

引数inには指定したViewのRectを入れるみたい。
この時、view.frame の場合には正味そのViewのみのキャプチャを行い、view.boundsの場合には、内包されているviewも対象になるみたい。今回はview.boundsがいいですね。

afterScreenUpdatesは対象のViewに変化があったかどうかを確認可能にするか否か(ややこしや)を指定するものみたいです。
今回はfalseとしました。

上記を考慮し、以下の様にUpdateしました。

fileprivate extension UIView {
    
    /**
   対象のViewをキャプチャしUIImageで保存
   
   - returns : キャプチャ画像 (UIImage)
   */
    fileprivate func getCaptureImageFromView() -> UIImage? {
        
        let contextSize = self.frame.size
        
        UIGraphicsBeginImageContextWithOptions(contextSize, false, 0.0)       //コンテキストを作成
        
        self.drawHierarchy(in: self.bounds, afterScreenUpdates: false)
        
        let captureImage = UIGraphicsGetImageFromCurrentImageContext()        //取得したコンテキストをUIImageに変換
        UIGraphicsEndImageContext()                                         //コンテキストを閉じる
        
        return captureImage
        
    }
    
    
}

これでanimate(withDuration:animations:completion:) で作成したアニメーションも任意の表示位置のキャプチャが出来る様になりましたとさ。