MILLEN BOX

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

UIViewの一部をUIImageとして切り取る方法 [swift2.1] [Context] [CGAffineTransform]

先日はUIImageを切り取る方法について投稿しました。

anthrgrnwrld.hatenablog.com

この記事中の最後でも注意点としても書きましたが、この方法だと 切り取る範囲についてはあくまでUIImageを対象として考えないといけない です。
UIImageはUIImageViewに対し拡大・縮小して表示しているケースが多いので、見たまんまの感覚でrectを指定すると、想定と異なった範囲の切り取り画像になってしまいます。

この問題の解決の為、以下のような方法を考えました。

  1. 対象のViewと切り取り範囲(Rect)を元にコンテキストを作成
  2. 1をUIImageに保存

上記発想の元作成したものについて今回は書きたいと思います。
出来上がったもののgif画像とGithubは以下です。

f:id:anthrgrnwrld:20151120080245g:plain

▶︎GitHub - anthrgrnwrld/clipView

参考リンクは以下です。

▶︎ iPhone アプリ研究会 UIViewの一部をUIImageとして取得する方法

自分ポイント1

切り取り関数のソースを以下に示します。
parameterとしては対象のViewと切り取りRectを指定し、Returnは切り取り後のUIImageとなります。
中の処理のポイントについては 自分ポイント2 にて説明します。

    /**
     対象のViewを指定したrectで切り取りUIImageとして取得する

     - parameter view:切り取り対象のview
     - parameter rect:切り取る座標と大きさ
     - returns: 切り取り結果を返す
    */
    func clipView(view: UIView?, rect: CGRect?) -> UIImage? {
        
        guard let targetView = view else {
            return nil
        }
        
        guard let frameRect = rect else {
            return nil
        }
        
        // ビットマップ画像のcontextを作成.
        UIGraphicsBeginImageContextWithOptions(frameRect.size, false, 0.0)
        let context = UIGraphicsGetCurrentContext()!
        
        //Affine変換
        let affineMoveLeftTop = CGAffineTransformMakeTranslation(-frameRect.origin.x, -frameRect.origin.y)
        CGContextConcatCTM(context, affineMoveLeftTop)
        
        // 対象のview内の描画をcontextに複写する.
        targetView.layer.renderInContext(context)
        
        // 現在のcontextのビットマップをUIImageとして取得.
        let clippedImage = UIGraphicsGetImageFromCurrentImageContext()
        
        // contextを閉じる.
        UIGraphicsEndImageContext()
        
        return clippedImage
    }

自分ポイント2

Parameterの切り取りRectを基にコンテキストを作成します。
コンテキストについてはスクリーンショットを保存する方法の記事で少し出てきました。

anthrgrnwrld.hatenablog.com

スクリーンショットの保存の時にはコンテキストに写す範囲として画面全体としていましたが、今回は必要範囲が決まっています。
そのような場合には以下の様な手順が必要になります。

  1. コンテキスト(一時保存場所?)の作成 → 切り取りサイズの指定
  2. 切り取り座標に従いAffine変換する → 切り取り位置の指定
  3. コンテキストに対象Viewを複写する
// ビットマップ画像のcontextを作成.
UIGraphicsBeginImageContextWithOptions(frameRect.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
        
//Affine変換
let affineMoveLeftTop = CGAffineTransformMakeTranslation(-frameRect.origin.x, -frameRect.origin.y)
CGContextConcatCTM(context, affineMoveLeftTop)
        
// 対象のview内の描画をcontextに複写する.
targetView.layer.renderInContext(context)

そしてコンテキストのビットマップをUIImageとして保存します。

let clippedImage = UIGraphicsGetImageFromCurrentImageContext()

ちなみに UIGraphicsBeginImageContextWithOptions を今回使用しましたが、 UIGraphicsBeginImageContext という関数もあります。
しかし今回使用した UIGraphicsBeginImageContextWithOptions の方がRetinaディスプレイを考慮した作りとなっているため、通常はこちらを使用した方が良いと思います。
(参考)
▶︎ 自前で描画した内容がUIImageで ぼやける時の処置 | 秋山ブログ

自分ポイント3

セグコントロールが押下された時の動作のソースを貼っときます。

    /**
     SegControlが押下された時に呼ばれる
    */
    @IBAction func pressClipSegControl(sender: AnyObject) {
        
        var rect: CGRect?       //切り取るrect値格納用
        
        //セグコントロールとclipTypeを紐付け。そしてその値がnilになる場合(= Non Clip)には元のイメージを表示する
        let clipValues : [clipType?] = [nil, .type100x100]
        
        guard let clipValue = clipValues[clipSegControl.selectedSegmentIndex] else {
            imageView.image = UIImage(named: "mountain.jpg")
            return
        }
        
        //セグコントロールが.type100x100(= Clip(100x100))の時、それに従ったrectの値を入れる
        switch clipValue {
        case .type100x100:
            rect = CGRectMake(137, 284, 100, 100)
        }
        
        //imageViewからframeRectで切り取り、結果をUIImageで取得する
        guard let clippedImage = clipView(imageView, rect: rect) else {
            imageView.image = UIImage(named: "mountain.jpg")
            return
        }
        
        //切り取り結果を拡大表示
        imageView.image = clippedImage
        
        
    }

clipTypeはViewController.swiftの頭で以下のように定義しています。

    //clipTypeをenumで定義しておく(一個だけだが練習)
    enum clipType :Int {
        case type100x100
    }

これでUIImageを切り取る方法と比べると直感的に切り取ることが出来るようになりました。