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を切り取る方法と比べると直感的に切り取ることが出来るようになりました。

Status Barの表示を消す方法 [swift2.1]

Status Barの表示をしたくないアプリが出てきましてその設定方法について。
Status Barというのは、iOS機器の画面上部に日付やバッテリー残量、電波強度などの情報を表示しているBarのことです。

Xcodeでプロジェクトファイルの中を覗いていると、「Status Bar Style」という項目に「Hide status bar」というチェックボックスがありました。
ここの部分のチェックを付けたり外したりしながら設定を弄ってみましたが効果なし(シミュレーター上)。
んん〜?と思って調べたところ、Info.plistについても弄る必要がある模様。

▶︎iOS7でステータスバーを非表示に…奮闘記\(^o^)/|杏z 学習帳

自分ポイント1

XcodeでInfo.plistファイルを選択し、View controller-based status bar appearance を追加しましょう。
設定は NO にします。

f:id:anthrgrnwrld:20151116074633p:plain

自分ポイント2

本記事の頭で説明しましたプロジェクトファイルの「Status Bar Style」→「Hide status bar」のチェックボックスを入れます。

f:id:anthrgrnwrld:20151116074823p:plain

これでStatus Barの表示がされなくなりました。

UIImageの一部を切り取る方法 [swift2.1]

UIImageの一部を切り取る方法について。

表示している写真に対し、セグメントコントロールでクリップする/しないを切り替えるアプリを作成しました。
f:id:anthrgrnwrld:20151115103321g:plain

githubは以下です。

▶︎GitHub - anthrgrnwrld/clipImage

参考にしたページは以下です。

▶︎UIImageの一部を切り抜く方法 | 目くじら日記

▶︎画像をトリミングしてUIImageViewにセットする - Qiita

▶︎SwiftでUIImageを回転、リサイズ、クリッピング、塗りつぶし - Qiita

自分ポイント1

下の記載がクリッピング用のメソッドです。
parameterとしては対象のUIImageと切り取りRectを指定し、Returnは切りとり後のUIImageとなります。

    /**
     UIImageを切り取る
     
     - parameter image:切り取り対象(UIImage)
     - parameter rect:切り取る大きさと座標位置(CGRect)
     - returns: 切り取り結果(UIImage)
    */
    func clipImage(image: UIImage?, rect: CGRect?) -> UIImage? {
        
        guard let originalImage = image else {
            return nil
        }
        
        guard let rct = rect else {
            return image
        }
        
        // ソース画像からCGImageを取り出し、指定された範囲を切り抜いたCGImageを生成
        let cripImageRef = CGImageCreateWithImageInRect(originalImage.CGImage, rct)
        
        guard let imgrf = cripImageRef else {
            return image
        }

        //生成したCGImageをUIImageとする
        let crippedImage = UIImage(CGImage: imgrf)
        
        return crippedImage
        
    }

自分ポイント2

自分ポイント1で再生した関数をセグコントロールをタップした時に呼び出します。

    /**
     Clip用の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 {
            setImage()
            return
        }

        //セグコントロールが.type100x100(= Clip(100x100))の時、それに従ったrectの値を入れる
        switch clipValue {
        case .type100x100:
            let origin = CGPoint(x: 100, y: 250)        //座標位置(四隅の左上)は(100,100)
            let size = CGSize(width: 100, height: 100)  //切り取りサイズは100x100
            rect = CGRect(origin: origin, size: size)   //originとsizeからrectを割り出す
        }
        
        //上で作成したrectに従って指定したUIImageを切り取る。今回きりとるUIImageはimageViewFromLibrary.image
        let clippedImage = clipImage(imageViewFromLibrary.image, rect: rect)    //clippedImageには切り取り結果が入る

        //clippedImageがnilの場合には元のイメージを表示する
        guard let clpImg = clippedImage else {
            setImage()
            return
        }
        
        //きりとったUIImageを表示する
        imageViewFromLibrary.image = clpImg
    }

clipTyeについてはenumで以下のように定義しておきます。

    enum clipType :Int {
        case type100x100
    }

注意点

今回の方法の場合、切り取り座標と切り取りサイズの指定はあくまでUIImageに対して指定してあげないといけません。
スクリーンの座標で考えると「思ってたんと違う」ということになってしまうので注意してください。

(関連記事を書きました!)

anthrgrnwrld.hatenablog.com

実行中の実機がどのRetinaのタイプか判断する方法 [swift2.1]

備忘録代わりに小ネタ記事を投稿します。

2015/11/15現在の最新iOSがサポートしている実機の解像度はNon Retina, Retina(2x), Retina(3x)の3種類存在します。

アプリが動いている実機のRetinaのタイプを知りたい時には、以下の方法で知ることができます。

let scale = UIScreen.mainScreen().scale 

上記scale内に1.0, 2.0, 3.0いずれかの数値が入ります。
そしてその値によってどのRetinaのタイプかを判断可能です。

実機の判断方法については色んな方法がありますが、今回の方法についても手段の一つとして覚えておきます。

関数の説明文を書こう Xcode7 対応版 [Xcode7.1]

また間が空きましたが久々の更新です。
ちょっとハマってしまったことがありまして間隔が空いてしまっています。
ハマるとブログに書きたいネタは増えるんだけど、ハマりの解消作業もしたい。というようなジレンマ中です。

今日もちょっと小ネタです。
以前関数の説明文についてこんな記事を書きました。

anthrgrnwrld.hatenablog.com

ルールに沿って関数ヘッダの説明文を書けば、すごくおしゃれで便利だよ〜という内容です。

で、時は流れまして、、、
本日、この記事の通りに関数の説明文を書いたんですが、option+Clickで関数の説明が思ったのと違う風になっていました。
具体的にはparameterやreturnsなどが分かれて表示されていたのが、Description内に全ての記述が記載されるようになってしまっていたのです。
ん〜??と思って調べたところ、やっぱりXcode7(Swift2?)で記述ルールに変更があったんですね。

qiita.com

自分ポイント

Xcode6までは

/**
説明

:param: a: 引数a
:param: b: 引数b
:returns: 返り値
*/

というような記載をしていました。
それがXcode7からは以下のような書き方に変更になった模様です。

/**
説明

- parameter a: 引数a
- parameter b: 引数b
- returns: 返り値
*/

また何とMarkdown記法にも対応しています!

具体的な関数を例として、以下に使用例を書いておきます。

    /**
    sizeとcenter座標の指定からorigin座標を算出する(_=rectを算出する_)関数
     
    - parameter size:算出するrectのsize
    - parameter center:算出するrectのcenter座標
    - returns: sizeとcenterから算出されたorigin座標を含めたrectを返す
    */
    func makeRectFromSizeAndCenter(size :CGSize, center :CGPoint) -> CGRect {
        
        let origin = CGPoint(x: center.x - (size.width) / 2, y: center.y - (size.height) / 2)   //sizeとcenterから座標位置を特定
        let rect = CGRectMake(origin.x, origin.y, size.width, size.height) //blurView用Rect
        
        return rect
    }

この書き方で今まで通り、関数の呼び出しの部分で option + Click すると、自分で追加した説明文が参照されるようになりました!
Markdown対応なので"=rectを算出する"の部分が斜字体になっているのもポイントです。

f:id:anthrgrnwrld:20151112075009g:plain

やっぱり説明文をちゃんと書いていると落ち着きますね。

新しいメソッドを作ったら取り敢えずすること [swift2.0] [print]

前回ぶつかった問題に関連してほぼ初めてオープンソースライブラリを使ってみています。
んん?と思うところがあっても、ブラックボックスでないので、中の動作を追えておもしろいです。
しかし本来の目的を忘れてどっぷりハマってしまうこともしばしば...。
気を付けないといけないです。

本日は新しいメソッドを作成した時に私が個人的にやっていることを残しておきます。

自分ポイント

私は新規でメソッドを作成した場合、その頭で以下のようなprintを初めに書いてしまいます。

func testFunc() {    //新規作成したメソッド
    print("\(__FUNCTION__) is called!")    //いつも書くprint文
}

__FUNCTION__という記載ですが、その時実行しているメソッド名(String型)です。 つまり上記ソースコードの場合の出力結果は以下のようになります。

testFunc is called!

これを書いておけば、空メソッドであっても、少なくとも呼び出されているということだけは確認することができます。
役に立つかどうかについて「?」の方も多いかとは思いますが、小心者の私にとってはこれだけでも大きな安心材料になってます。
ログがうるさくなればコメントアウトするだけですし。

いいところとしては、何も考えずにコレを書いとくだけで、どこで出力されたprintかが大体分かることです。
print("test001")みたいなのを複数に書いちゃうと、テスト用とはいえどこでの出力かがわからなくなってきます。

ちなみに同一メソッド内の複数箇所に記載した場合、差異は出ません。
そこの部分についても差異を確認したいという場合、\(__LINE__)というのを追加して上げて下さい。
そのprintを記載した行数が追加表示されるので、どこのprintかを一意に判別できます。

ソース

func testFunc() {    //新規作成したメソッド
    print("(\(__LINE__)) \(__FUNCTION__) is called!")    //いつも書くprint文
    print("(\(__LINE__)) \(__FUNCTION__) is called!")    //いつも書くprint文
}

出力

(29) testFunc is called!
(30) testFunc is called!

他にもswiftでは違うパターンの同様の記載方法(マクロといいます)に対応しています。
下記リンクをご参考頂ければと思います。

▶︎Swiftでデバッグ出力(日時、メソッド名、行番号) - 定食屋おろポン

以上です。ありがとうございました。

UIVisualEffectViewを使ったblur効果が環境によって動かないぞ?と思ったら [UIVisualEffectView] [UIBlurEffect]

こんにちは。AppleTVが欲しいです。
こんなん見せられたらたまらんです。

今日は前回投稿した iOS8で追加されたUIVisualEffectViewを使ったBlur効果 の続きです。
前回どのようなことをやったのかについては以下を参照して下さい。

anthrgrnwrld.hatenablog.com

作成後ぶつかった問題

シミュレーターで動作確認 → 動く
実機(iPhone6)で動作確認 → 動かない
という現象が発生しました。
こんな感じになります(イメージ)↓
f:id:anthrgrnwrld:20151030080531p:plain

はじめは実装部分を疑い、表示Viewの順番を変えたり、EffectViewの作成方法を変えたりしましたが、解決に至らず。
あたまの中は「?」マークだらけでした。

解決に至ったきっかけ

以下のページを見たことでピンときました。

http://506506.ntt.com506506.ntt.com

"It seems like UIVisualEffectView is meant for toolbars and similar, " のところ。
「あ〜そうか。アノ設定に引っ張られているのかも」と思いましたね。

現象が発生する理由

ところで私のiPhone6はiOS9にアップデートしています。 そしてアップデート後、動作が重くなった感じ、色々設定を弄っています。
この時に変更した設定 [設定→一般→アクセシビリティコントラストを上げる→透明度を下げる] をONにしたことが、今回の環境による動作差異に現れているのではと疑いました。
以下はシミュレーターでの設定箇所です↓
f:id:anthrgrnwrld:20151030081353p:plain
一旦この設定をOFFに戻し、アプリを立ち上げ直して確認してみると、 想定通りのBlur効果がかかることが確認できました。
またシミュレーターで同設定をONにした場合、Blur効果がかからないことも確認しました。

Appleの立場にたってよく考えたら「そりゃそうか」ってところなんですが、画像編集アプリを作ろうと考えていた私にとっては、なかなかいい勉強となった次第です。
画像編集アプリなどのblur効果は恐らく違う方法をとってやってるんでしょうね。
また調査しなければいけないことが増えました!