MILLEN BOX

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

OekakiGestureRecognizer (touchDownを認識するGestureRecognizer) [UIGestureRecognizer]

現在簡単なお絵描き機能を持ったアプリを作成中ですが、その実現方法として当初UIPanGestureRecognizerを使用してタップ・タッチを検出していました。
しかしUIPanGestureRecognizerでお絵描き機能を実現した場合、画面に指を引っ付けて指を動かしたり離したりした時にタップが検出されます。
画面にタップした瞬間は検出されません。よって画面をタップして指を動かさない場合には、お絵描きの実行はされないのです。

しかし現実世界では紙に筆を落とした瞬間に墨の色が付くわけで。
今回はその実現のため、 UIGestureRecognizer を拡張した OekakiGestureRecognizer を作ってみましたのでメモしておきます。

Githubは以下です。

▶︎oekakiGestureRecognizer/OekakiGestureRecognizer.swift at master · anthrgrnwrld/oekakiGestureRecognizer · GitHub

OekakiGestureRecognizer

import UIKit
import UIKit.UIGestureRecognizerSubclass

class OekakiGestureRecognizer: UIGestureRecognizer
{
    
    var touchPoint: CGPoint?                                //タッチ座標を格納
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent)
    {
        
        let numberOfTouches = event.allTouches()!.count     //タッチした指の本数を取得
        
        if numberOfTouches != 1 {
            self.state = .Cancelled                         //タッチ本数が1本以外の場合はstateをCancelに
        }
        
        if self.state == .Possible {
            let touch = touches.first! as UITouch
            touchPoint = touch.locationInView(self.view)    //タッチ座標を取得・格納
            self.state = .Began                             //stateをBeganに
        }
    }
    
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
        let touch = touches.first! as UITouch
        touchPoint = touch.locationInView(self.view)        //タッチ座標を取得・格納
        self.state = .Changed                               //stateをChangedに
    }
    
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent) {
        let touch = touches.first! as UITouch
        touchPoint = touch.locationInView(self.view)        //タッチ座標を取得・格納
        self.state = .Ended                                 //stateをEndedに
    }
    
    override func touchesCancelled(touches: Set<UITouch>, withEvent event: UIEvent) {
        let touch = touches.first! as UITouch
        touchPoint = touch.locationInView(self.view)        //タッチ座標を取得・格納
        self.state  = .Cancelled                            //stateをCancelledに
    }
    
    override func locationInView(view: UIView?) -> CGPoint {

        guard var tchpnt = touchPoint else {
            fatalError("Can't get touchPoint")
        }
        
        //parameterのviewがUIScrollViewか否かで処理を分岐(拡大時対応の為)
        if let parameterView = self.view! as? UIScrollView {
            
            let contentSize = parameterView.contentSize
            let rate = self.view!.bounds.width / contentSize.width
            
            tchpnt.x = tchpnt.x * rate
            tchpnt.y = tchpnt.y * rate
            
            return tchpnt                                   //parameterのviewがUIScrollViewだった時

        } else {
            return tchpnt                                       //parameterのviewがUIScrollView以外だった時
        }


    }

    
}

正直検証しきれてないので想定していない動作で何が起こるか分かってませんが、
恐らく想定していない動作については「何も起こらない」ようになっている為、大きな問題にはならないんじゃないかと楽観的に考えています。

ScrollViewのdelegate [UIScrollView]

UIScrollViewにて画像の拡大縮小に対応した時、特定の動作(拡大縮小やスクロールなど)時のみ消したいViewがある!といったことがあるかもしれません。
今回はその実現方法のメモです。

    /**
     写真の拡大縮小に対応
     */
    func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
        return self.pictureView
    }
    
    /**
     写真の拡大縮小中に「とあるview」を隠す
     */
    func scrollViewWillBeginZooming(scrollView: UIScrollView, withView view: UIView?) {
        toaruView.hidden = true
    }
    
    /**
     写真の拡大縮小完了で「とあるview」を隠していたのを解除する
     */
    func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat) {
        toaruView.hidden = false
    }

    /**
     写真のスクロール中に「とあるview」を隠す
     */
    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        toaruView.hidden = true
    }
    /**
     写真のスクロール完了で「とあるview」を隠していたのを解除する
     */
    func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        toaruView.hidden = false
    }

以上です。

Core ImageでBlur画像を作成する(Ver.1 & 2) [CIFilter]

CoreImageにてBlur画像を作成する為の関数についてメモを残しときます。

1. ガウジアンFilter関数 Ver.1

以下は元画像(CIImage)からBlur画像を作成する関数です。
UIImageからCIImageのゲット方法は let imageCIImage = CIImage(CGImage: imageUIImage.CGImage!) です。

    /**
     ガウジアンFilterを適応する
     
     - parameter inputCIImage : 元CIICmage
     - parameter value : フィルターのかけ度合い
     - returns : ぼかし画像のサイズ
     */
    func applyGaussianBlurFilter(inputCIImage: CIImage, value: Float) -> CIImage {

        //ガウシンアン(ぼかし)フィルターの適応
        let filter = CIFilter(name: "CIGaussianBlur")
        filter?.setValue(inputCIImage, forKey: kCIInputImageKey)
        filter?.setValue((value), forKey: kCIInputRadiusKey)
        let outputCIImage = filter?.outputImage
        
        guard let _outputCIImage = outputCIImage else {
            fatalError("applyGaussianBlurFilter does not work.")
        }
        
        return _outputCIImage

    }

CIImageからUIImageへの復元は以下を参考にして下さい。
(変数が何を示しているかは関数名から想像お願いいたします...汗)

let rect = CGRect(origin: CGPointZero, size: imageUIImage.size)
let context = CIContext(options: nil)
let cgImage = context.createCGImage(outputCIImage, fromRect: rect)
let blurImage = UIImage(CGImage: cgImage)

2. ガウジアンFilter関数 Ver.2

Ver.1 のままだとガウジアンFilter適応後の画像端が透明色のグラデーションがかかってしまう問題があります。
対策としてClampフィルターを前処理として適応します。
この辺の詳細については以下を参照の事。
▶︎ objective c - Correct crop of CIGaussianBlur - Stack Overflow

以下、関数です。

    /**
     ガウジアンFilterを適応する
     
     - parameter inputCIImage : 元CIICmage
     - parameter value : フィルターのかけ度合い
     - returns : ぼかし画像のサイズ
     */
    func applyGaussianBlurFilter(inputCIImage: CIImage, value: Float) -> CIImage {
        
        //inputCIImageを何も考えずにCIGaussianBlurをかけると画面端に透過色が混じる
        //その対策の為、CIAffineClampを使い画面外側に拡大した元画像を配置し現象の対策を行う
        //URL: http://stackoverflow.com/questions/12839729/correct-crop-of-cigaussianblur
        let affineClampFilter = CIFilter(name: "CIAffineClamp")
        let xform = CGAffineTransformMakeScale(1.0, 1.0)
        affineClampFilter?.setValue(inputCIImage, forKey: kCIInputImageKey)
        affineClampFilter?.setValue(NSValue(CGAffineTransform: xform), forKey: "inputTransform")
        let outputAfterCalmp = affineClampFilter?.outputImage

        //ガウシンアン(ぼかし)フィルターの適応
        let filter = CIFilter(name: "CIGaussianBlur")
        filter?.setValue(outputAfterCalmp, forKey: kCIInputImageKey)
        filter?.setValue((value), forKey: kCIInputRadiusKey)
        let outputCIImage = filter?.outputImage
        
        guard let _outputCIImage = outputCIImage else {
            fatalError("applyGaussianBlurFilter does not work.")
        }
        
        return _outputCIImage

    }

CIImageからUIImageへの復元はrectの取得方法に変更があります。

let rect = inputCIImage.extent
let context = CIContext(options: nil)
let cgImage = context.createCGImage(outputCIImage, fromRect: rect)
let blurImage = UIImage(CGImage: cgImage)

所感

Ver.1で発生した「画面端透明問題」で結構ハマってしましました。
情報提供頂いた @akio0911 さんには感謝です!

Navigation Barをカスタマイズ [UINavigationBar]

かるーくかるーくメモを残します。

本日はNavigationBarのカスタマイズの方法について。
Githubは以下です。

▶︎GitHub - anthrgrnwrld/customNavigationBar

1. Navigation Barをアニメーションを使って隠す

  • 隠す
self.navigationController!.setNavigationBarHidden(true, animated: true)
  • 表示する
self.navigationController!.setNavigationBarHidden(false, animated: true)

2. Navigation Barを透明にする

  • 透明化
self.navigationController!.navigationBar.setBackgroundImage(UIImage(), forBarMetrics: .Default)
self.navigationController!.navigationBar.shadowImage = UIImage()
  • 透明化解除
self.navigationController!.navigationBar.setBackgroundImage(nil, forBarMetrics: .Default)
self.navigationController!.navigationBar.shadowImage = nil

3. Navigation BarのTInt Colorを変更する(例えば赤に)

self.navigationController!.navigationBar.tintColor = UIColor.redColor()

Unwind Segueのアニメーションをカスタマイズ [UIStoryboardSegue]

ブログの更新ができてない。
もうこうなったらつぶやくように更新していこうと思います。

本日はUnwind Segueのアニメーションをカスタマイズする方法をベタっとメモしておきます。

Githubは以下です。

▶︎GitHub - anthrgrnwrld/myUnwindSegue

1. Storyboard

通常使用する場合から得に変わったことはしません。
参考にキャプチャ画像を貼っておきます。

f:id:anthrgrnwrld:20160616080443p:plain

...いや、忘れてました。 UnwindSegueのクラスとして今回カスタマイズ用に定義したクラス名を指定する必要があります。
(今回は3で作成するmyUnwindSegueを指定)
f:id:anthrgrnwrld:20160616123921p:plain

2. ViewController (起動時に表示されるViewController)

コードは以下です。
得に通常と変わったことはありません。

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func returnFirstView(segue: UIStoryboardSegue) {
        print("\(#function) is called!")        
    }


}

3. myUnwindSegue (UnwindSegueで使用するカスタムクラス)

新規ファイルを作成し、以下のようにUnwindSegue時に実行されるアニメーションを記載する。

import UIKit

class myUnwindSegue: UIStoryboardSegue {
    
    let flag = false
    
    override func perform() {
        
        /***** 準備 ******/
        //ViewControllerのインスタンスを取得する
        let firstVCView = destinationViewController.view as UIView!
        let secondVCView = sourceViewController.view as UIView!
        
        //画面の縦横を取得する
        let screenHeight = UIScreen.mainScreen().bounds.size.height
        let screenWidth = UIScreen.mainScreen().bounds.size.width
        
        //戻った先のビューを画面外に設置する。
        firstVCView.frame = CGRectMake(-screenWidth, 0.0, screenWidth, screenHeight)
        
        //戻った先のビューを現在の画面の上にのせる
        let window = UIApplication.sharedApplication().keyWindow
        window?.insertSubview(firstVCView, aboveSubview: secondVCView)
        
        
        /****** アニメーション *****/
        UIView.animateWithDuration(0.5, animations: { () -> Void in
            //現在のビューを画面外に移動させる。
            secondVCView.frame = CGRectOffset(secondVCView.frame, screenWidth, 0.0)
            //戻った先のビューを画面上に移動させる。
            firstVCView.frame = CGRectOffset(firstVCView.frame, screenWidth, 0.0)
            
        }) { (Finished) -> Void in
            //現在の画面を閉じる
            self.sourceViewController.dismissViewControllerAnimated(false, completion: nil)
        }
    }
    
}

以上です。

手軽にBlurViewが使えるSABlurImageViewを使ってみました [swift2.1] [SABlurImageView]

ものすごく久しぶりの投稿です。。。。
罪悪感でいっぱい。。。。

先日(と言っても随分前ですが、、、)iOS8で追加されたUIVisualEffectViewを使ってBlur効果を実現しました。

▶︎iOS8で追加されたUIVisualEffectViewを使ってBlur効果を作成する [UIVisualEffectView] [UIBlurEffect] - MILLEN BOX

しかしこの方法には問題があります。iOS端末の設定によってはBlurEffectがかかりません。以下記事参照。

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

そこで今回はiOS端末の設定に依存しないBlurRffectの掛け方をシェアしようと思います。

f:id:anthrgrnwrld:20160202204238g:plain

今回作成したプロジェクトのGitHubは以下です。

▶︎GitHub - anthrgrnwrld/saBlurEffect

どうやって実現する?

さてBlurEffectの実現方法ですが、実は先人の方々が既に作ってくれているのです。
その中で今回は、オープンソースとして公開されているSABlurImageViewというクラスを使用したいと思います。

▶︎GitHub - marty-suzuki/SABlurImageView: You can use blur effect and it's animation easily to call only two methods.

以下のQiitaにも記事がありました。

▶︎iOSアプリ開発でブラーエフェクトを手間なく使う方法 - Qiita

このクラスを使ったサンプルも上記リンクにはあったのですが、方法がちょっと私がしたい使い方ではありませんでした。
サンプルの方法だとUIImageView全体にBlurEffectをかけるようなイメージで書かれていました。
しかし、私はこれのように、UIImageViewの特定部分のみEffectをかけるような形で使用したかったのです。

そこで「特定部分のみEffectをかける」ということを実現するために、UIImageやUIViewから特定部分をUIImageとして切り出す方法を学び、以下の記事達を書きました。

▶︎UIImageの一部を切り取る方法 [swift2.1] - MILLEN BOX

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

そして今日、晴れて(やっと)SABlurImageViewのオレオレな使い方を公開する運びとなりました。

SABlurImageViewを使用する準備

SABlurImageView をダウンロードします。
ダウンロード後生成された"SABlurImageView"フォルダ直下にある"SABlurImageView"フォルダ(同じ名前だから注意)を、自分のプロジェクトにガツンと放り込みます。
これで使用準備は完了です。

f:id:anthrgrnwrld:20160202075931p:plain
※ testSABlurImageViewが今回私が作成したプロジェクト名、そしてSABlurImageViewフォルダが上記手順にて追加したものです。

ちなみにCocoaPodsでもSABlurImageViewは使用出来ますが、私の宗教上の理由により今回は上記方法にて利用しています。(単に覚えるのが面倒くさいだけ。。。)

SABlurImageViewの使用方法

単にUIImage全体にBlurEffectをかけるだけであれば、引数にUIImageを渡してあげてSABlurViewをインスタンス化し、addSubViewすれば行けます。

effectView = SABlurImageView(image: effectImage)
effectView.addBlurEffect(25, times: 2)    //Blur度合とかける回数を指定
self.addSubView(effectView)

しかし今回私は「渡したUIImage」の「特定範囲にのみ」Effectをかけたかった...ということで、以下の addVirtualEffectView という関数を作成しました。
ポイントはev.frame = frameRectにてblurをかける大きさを指定しているところでしょうか。
(追記)またeffectViewについてはViewControllerのプロパティとして private var effectView : SABlurImageView? //Effect用View という形で予め宣言済みです。(Github参照)

    /**
     エフェクトを適用する
     
     - parameter view:blurをかける対象のView
     - parameter rect:blurをかける位置とサイズ(CGRect)
     - parameter blurLevel:blurレベル
    */
    func addVirtualEffectView(view :UIView?, rect :CGRect?, blurLevel :SABlurEffectStyle?) {
        //print("\(__FUNCTION__) is called!")
        
        //effectViewの内容を一旦初期化する
        initEffectView()
        
        guard let blrlv = blurLevel else {
            return
        }
        
        guard let targetView = view else {
            return
        }
        
        guard let frameRect = rect else {
            return
        }
        
        //blurをかけたい部分をUIImageとして取り出す(=effectImage)
        let effectImage = clipView(view, rect: frameRect)

        
        //blurをかけたい部分(=effectImage)をBlur生成用関数(=SABlurImageView)にセットする
        effectView = SABlurImageView(image: effectImage)
        
        guard let ev = effectView else {
            print("\(__LINE__) \(__FUNCTION__) is called!")
            return
        }

        //Blurの大きさ,座標,形を指定する
        ev.frame = frameRect
        ev.layer.masksToBounds = true
        ev.layer.cornerRadius = 20.0
        
        //blurのかけ具合を指定する
        switch blrlv {
        case .Weak:
            ev.addBlurEffect(25, times: 2)
        case .Default:
            ev.addBlurEffect(50, times: 2)
        case .Strong:
            ev.addBlurEffect(100, times: 3)
        }
        
        //blurがかかったViewを追加する
        targetView.addSubview(ev)
        
    }

あとはこれとかこれとかこれとかでやったことのおさらいで実装の意味は理解できると思います。
更新、ちょっと空くと億劫になるのと、文章の書き方忘れてしまいますね。。。
もっとサクッと更新できるようにしていきたいです。

アプリが非アクティブになったことをViewControllerで検知する方法 [UIApplicationDelegate]

アプリがアクティブでなくなったことをViewControllerにて検知する方法について調べてみました。

自分ポイント1 - 非アクティブ時に実行されるメソッドについて

アプリが非アクティブになった時、AppDelegateの applicationWillResignActive が実行されます。
アプリが非アクティブになった時にやりたい処理は、このメソッドの内部に記載すればOKです。

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    ...

    func applicationWillResignActive(application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.

        //ここに非アクティブ時にやりたい処理を書く

    }

    ...

}

上記方法で「非アクティブになった時」にさせたい処理を書くこと原理上できますが、例えばその際にViewController内で宣言されたプロパティの値を変更したい場合はどうでしょうか。
そのプロパティはViewController内でのみ変更可能なため、上記方法では不可能です。
じゃあ、どのようにすれば??

自分ポイント2 - 非アクティブのViewControllerでの検知の仕方について

実はAppDelegateの親クラスUIApplicationDelegateのメソッドが実行される際、通知信号が出力されます。
今回はapplicationWillResignActiveが実行された際に出力される通知信号をViewControllerで受け取ってあげるように準備してあげれば、ViewControllerにて非アクティブを検知できます。
通知信号の受信の準備ですが、以下の順番でViewControllerにコードを追記していくと分かりやすいです。

  1. 通知信号を受け取った時に行うメソッドを、(処理内容の記載は空っぽで良いので)まず書いておきます。(rcvSignalOfNonActiveとします)
  2. ViewDidLoad辺りに以下のコードを追記します。このコードが「通知信号を受けとる準備」になります。
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "rcvSignalOfNonActive", name:UIApplicationWillResignActiveNotification, object: nil) 引数selectorには手順1で追加した通知信号を受け取った時に行うメソッドを書きます。
    引数nameに指定されているUIApplicationWillResignActiveNotificationは、applicationWillResignActiveが実行された際に出力される通知信号の名前です。

  3. 通知信号を受け取った時に行うメソッドrcvSignalOfNonActiveの内部処理を書きます。
    今回は特に何がしたいということはありませんので、print("\(__FUNCTION__) is called!")を書いて呼び出されたことを分かるようにしておきます。

  4. おっと忘れていました。
    ViewControllerが継承するクラスとしてUIApplicationDelegateを追記しておいてあげて下さい。

以下にコードを書いておきます。

import UIKit

class ViewController: UIViewController, UIApplicationDelegate {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        NSNotificationCenter.defaultCenter().addObserver(self, selector: "rcvSignalOfNonActive", name: UIApplicationWillResignActiveNotification, object: nil)  //UIApplicationWillResignActiveNotificationを受信した時のコールバック関数を登録
                
    }

    /**
     applicationWillResignActiveが呼ばれることによって出される信号UIApplicationWillResignActiveNotificationを受信した時に実行する関数
     ロックボタンやタイマーなどでスクリーンが非アクティブになった時に行いたい動作を記述する
     */
    func rcvSignalOfNonActive() {
        print("\(__FUNCTION__) is called!")
    }


}

実行後、ロックボタンなどを押すアクションをするとrcvSignalOfNonActiveメソッドが呼び出されるのが分かります。

自分ポイント3 - その他の通知信号

先にも書いていますが、通知信号が呼ばれるのは今回の非アクティブになるケースだけでなく、UIApplicationDelegateのメッソッドが実行された時にそれぞれ発生します。
そしてそれぞれ出力される通知信号は異なります。
参考URL1 : UIApplicationBackgroundRefreshStatusDidChangeNotification
参考URL2 : UIApplicationのNotificationsの一覧 - 強火で進め

これらの情報を使ってユーザーアクションに対する実行メソッドと信号を実際動かして見てみるの、面白そうです。