MILLEN BOX

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

swiftで円を描く [UIGestureRecognizer] [drawInRect] [Context] [swift 2.2]

画像イメージを貼り付け表示とかではなく、swiftで円を描く方法のメモです。
今回は前回作成した OekakiGestureRecognizer を使ってタップした瞬間に円を描くサンプルを作成しました。

参考URL: OekakiGestureRecognizer (touchDownを認識するGestureRecognizer) [UIGestureRecognizer] - MILLEN BOX

Githubは以下です。

▶︎GitHub - anthrgrnwrld/oekakiGestureRecognizer

ソースをベタっと貼り付けときます

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        let myOekaki = OekakiGestureRecognizer(target: self, action: #selector(ViewController.drawGesture(_:)))
        self.view.addGestureRecognizer(myOekaki)
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    
    
    internal func drawGesture(sender: AnyObject) {
        
        guard let oekakiGesture = sender as? OekakiGestureRecognizer else {
            print("Oekaki Gesture Error happened.")
            return
        }
        
        switch oekakiGesture.state {
            
        case .Began:
            
            let size = self.imageView.frame.size                                //コンテキストのサイズ
            let touchPoint = oekakiGesture.locationInView(self.imageView)       //タッチ座標を取得
            
            //Drawを実行 -> 結果をUIImageにて取得
            let imageAfterDraw = drawCircle(self.imageView.image, size: size, center: touchPoint)
            self.imageView.image = imageAfterDraw
            
        default:
            break
            
        }
    
    }
    
    func drawCircle(canvas: UIImage?, size: CGSize, center: CGPoint) -> UIImage {
        
        let radius: CGFloat = 20.0                                          //Drawする円の半径
        let red: CGFloat = 1.0                                              //Drawする色 R
        let blue: CGFloat = 0.0                                             //Drawする色 B
        let green: CGFloat = 0.0                                            //Drawする色 G
        let alpha: CGFloat = 1.0                                            //Drawする色 Alpha
        
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)            //コンテキストを取得
        
        //タップした時に描く円のRect
        let circleRect = CGRectMake(center.x - radius, center.y - radius, radius * 2, radius * 2)
        

        canvas?.drawInRect(CGRectMake(0, 0, size.width, size.height))        //コンテキストにimageViewの内容を写す
        let circlePath = UIBezierPath(ovalInRect: circleRect)               //円を描く
        let color = UIColor(red:red, green:green, blue:blue, alpha:alpha)   //透明色を格納
        color.setFill()
        circlePath.fill()
        let retImage = UIGraphicsGetImageFromCurrentImageContext()          //UIImageを取得する
        UIGraphicsEndImageContext()                                         //コンテキストを閉じる
        
        return retImage                                                     //UIImageをReturnする
        
    }


}

こんな風に北斗七星が書けるよ!
f:id:anthrgrnwrld:20160701192840p:plain

ふとした時に役に立つかもです。

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

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