[iOS] SwiftでCore Animationしたい

2014/12/1

こんにちは。きんくまです。
早いものでもう12月です。本年も大変お世話になりました、ありがとうございました。
、、って、まだ1年を締めるにはまだ早いか!

さて、今回はSwiftでCore Animationしたいです!

できたもの

core_anim

マスクつきとか、ディレイとかいろいろ試してみました。
あと、Auto Layoutでうまくいくかわからなかったので、そのあたりもためしてみました。
赤い四角は画面のサイズに応じて画面の下端から浮かびあがるようにして、水色のところは真ん中から円形マスクで見えるようになります。

イージング

アニメーションといえばイージングが肝心かなめ!

というわけで以前にObjective-Cでイージングの記事を書いたことがあったのですが、それをSwiftにも移植してみました。
以前の記事
>>[iOS] Core AnimationのCAMediaTimingFunctionでRobert Pennerのイージングを近似

KKCustomMediaTimingFunction.swift

import UIKit

enum KKCMTFEasingCurve{
    case Lenear,
    
    EaseInSine,
    EaseOutSine,
    EaseInOutSine,
    EaseOutInSine,
    
    EaseInQuad,
    EaseOutQuad,
    EaseInOutQuad,
    EaseOutInQuad,
    
    EaseInCubic,
    EaseOutCubic,
    EaseInOutCubic,
    EaseOutInCubic,
    
    EaseInQuart,
    EaseOutQuart,
    EaseInOutQuart,
    EaseOutInQuart,
    
    EaseInQuint,
    EaseOutQuint,
    EaseInOutQuint,
    EaseOutInQuint,
    
    EaseInExpo,
    EaseOutExpo,
    EaseInOutExpo,
    EaseOutInExpo,
    
    EaseInCirc,
    EaseOutCirc,
    EaseInOutCirc,
    EaseOutInCirc
    
}

class KKCustomMediaTimingFunction : CAMediaTimingFunction{
    init(easingCurve:KKCMTFEasingCurve){
        let points:[Float] = KKCustomMediaTimingFunction.controlPointSettings(easingCurve)
        super.init(controlPoints: points[0], points[1], points[2], points[3])
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    private class func controlPointSettings(easingCurve:KKCMTFEasingCurve)->[Float]{
        var controlPoints:[Float]
        
        switch easingCurve {
        case .EaseInSine:
            controlPoints = [0.44, 0, 0.99, 0.98]
        case .EaseOutSine:
            controlPoints = [0, 0.44, 0.98, 0.99]
        case .EaseInOutSine:
            controlPoints = [0.36, 0, 0.64, 1]
        case .EaseOutInSine:
            controlPoints = [0, 0.36, 1, 0.64]
        case .EaseInQuad:
            controlPoints = [0.51, 0, 0.96, 0.9]
        case .EaseOutQuad:
            controlPoints = [0, 0.51, 0.9, 0.96]
        case .EaseInOutQuad:
            controlPoints = [0.43, 0, 0.57, 1]
        case .EaseOutInQuad:
            controlPoints = [0, 0.43, 1, 0.57]
        case .EaseInCubic:
            controlPoints = [0.55, 0, 0.7, 0.19]
        case .EaseOutCubic:
            controlPoints = [0, 0.55, 0.19, 0.7]
        case .EaseInOutCubic:
            controlPoints = [0.7, 0, 0.3, 1]
        case .EaseOutInCubic:
            controlPoints = [0, 0.7, 1, 0.3]
        case .EaseInQuart:
            controlPoints = [0.74, 0, 0.74, 0.19]
        case .EaseOutQuart:
            controlPoints = [0, 0.74, 0.19, 0.74]
        case .EaseInOutQuart:
            controlPoints = [0.85, 0, 0.13, 0.99]
        case .EaseOutInQuart:
            controlPoints = [0, 0.85, 0.99, 0.13]
        case .EaseInQuint:
            controlPoints = [0.79, 0, 0.75, 0.1]
        case .EaseOutQuint:
            controlPoints = [0, 0.79, 0.1, 0.75]
        case .EaseInOutQuint:
            controlPoints = [0.9, 0, 0.09, 1]
        case .EaseOutInQuint:
            controlPoints = [0, 0.9, 1, 0.09]
        case .EaseInExpo:
            controlPoints = [0.81, 0, 0.83, 0.11]
        case .EaseOutExpo:
            controlPoints = [0, 0.81, 0.11, 0.83]
        case .EaseInOutExpo:
            controlPoints = [0.97, 0, 0.02, 0.99]
        case .EaseOutInExpo:
            controlPoints = [0, 0.97, 0.99, 0.02]
        case .EaseInCirc:
            controlPoints = [0.67, 0, 0.99, 0.57]
        case .EaseOutCirc:
            controlPoints = [0, 0.67, 0.57, 0.99]
        case .EaseInOutCirc:
            controlPoints = [0.92, 0.15, 0.08, 0.82]
        case .EaseOutInCirc:
            controlPoints = [0.15, 0.92, 0.82, 0.08]
        case .Lenear:
            controlPoints = [0.00, 0.00, 1.00, 1.00]
        default:
            controlPoints = [0.00, 0.00, 1.00, 1.00]
        }
        return controlPoints
    }
}

でこれをつかってアニメーションを作ります。

Storyboardで等間隔にあけるのをやってみました。
これで、横位置でも画面サイズが変わっても見た感じ同じようにできました。

core_anim_storyboard

やり方は調べたらCocoaの日々さんで書かれてました。ありがとうございます。
>> Autolayoutでビューを等間隔に並べる

アニメーションのコードです。

ViewController.swift


import UIKit

class ViewController: UIViewController {
    @IBOutlet var square1:UIView?
    @IBOutlet var square2:UIView?
    @IBOutlet var square3:UIView?
    @IBOutlet var square4:UIView?
    @IBOutlet var square5:UIView?
    private var squares:[UIView]!
    
    @IBOutlet var blueRect:UIView?
    var maskCircleLayer:CALayer!

    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.squares = [square1!, square2!, square3!, square4!, square5!]
        
        for square in squares{
            square.layer.opacity = 0
        }
        
        self.setupMaskLayer()
        self.blueRect?.layer.hidden = true
        NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "startAnimation", userInfo: nil, repeats: false)
    }
    
    override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
        startAnimation()
    }
    
    override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
        self.setupMaskLayer()
    }
    
    internal func startAnimation(){
        CATransaction.begin()
        let duration = 1.0
        
        for (index, square:UIView) in enumerate(squares){
            addSquareAnimation(square, duration:duration, delay: 0.2 * Double(index))
        }
        addMaskCircleAnimation()
        self.blueRect?.layer.hidden = false
        CATransaction.commit()
    }
    
    private func addSquareAnimation(targetView:UIView!, duration:CFTimeInterval, delay:CFTimeInterval){
        
        let targetLayer:CALayer = targetView.layer
        targetLayer.opacity = 0
        
        let group:CAAnimationGroup = CAAnimationGroup()
        group.duration = duration
        group.beginTime = CACurrentMediaTime() + delay
        
        let opacity:CABasicAnimation = CABasicAnimation(keyPath: "opacity")
        opacity.fromValue = NSNumber(float: 0.0)
        opacity.toValue = NSNumber(float: 1.0)
        
        let position:CABasicAnimation = CABasicAnimation(keyPath: "position")
        position.timingFunction = KKCustomMediaTimingFunction(easingCurve: KKCMTFEasingCurve.EaseInOutExpo)
        
        let containerRect:CGRect = self.view.frame
        
        let fromPoint:CGPoint = CGPointMake(targetLayer.position.x, containerRect.size.height - 20)
        position.fromValue = NSValue(CGPoint: fromPoint)
        group.animations = [opacity, position]
        
        group.fillMode = kCAFillModeBoth
        group.removedOnCompletion = false
        
        let viewConstraints = targetView.constraints()
        targetLayer.addAnimation(group, forKey: nil)
    }
    
    private func addMaskCircleAnimation(){
        let scaleAnim = CABasicAnimation(keyPath: "transform.scale")
        scaleAnim.duration = 1.7
        scaleAnim.timingFunction = KKCustomMediaTimingFunction(easingCurve: KKCMTFEasingCurve.EaseInOutCubic)
        scaleAnim.fromValue = NSNumber(float:0.0)
        scaleAnim.toValue = NSNumber(float:1.0)
        maskCircleLayer.addAnimation(scaleAnim, forKey: nil)
    }
    
    private func setupMaskLayer(){
        self.maskCircleLayer = CALayer()
        maskCircleLayer.delegate = self
        let containerSize = self.view.frame.size
        let sw = containerSize.width
        maskCircleLayer.frame = CGRectMake(0, 0, sw * 2.0, sw * 2.0)
        let blueHeight:CGFloat! = blueRect?.frame.size.height
        maskCircleLayer.position = CGPointMake(sw * 0.5, blueHeight * 0.5)
        maskCircleLayer.setNeedsDisplay()
        blueRect?.layer.mask = maskCircleLayer
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    override func drawLayer(layer: CALayer!, inContext ctx: CGContext!) {
        if layer === maskCircleLayer{
            CGContextSetFillColorWithColor(ctx, UIColor(red: 1, green: 0, blue: 0, alpha: 1).CGColor)
            CGContextFillEllipseInRect(ctx, CGRectMake(0, 0, layer.frame.size.width, layer.frame.size.height))
        }
    }
}

水色の四角をマスクする円形のマスクレイヤーは、今回はdrawLayerできちんと描画してますけど、cornerRadiusプロパティでも同じ効果が出せるみたいです。

Objectie-Cじゃなくって、Swiftでの書き方を調べながらやったので今回は時間がかかりました。でも、やり方がわかったので慣れればすぐに書けそうだと思いました。あとそれほどコード量も増えないから、こんな感じのアニメーションだったらライブラリなしでそのまま書いてもいいじゃないかなと思いました。

今回のプロジェクト一式(.zip)

そうだ。1点だけ。アプリを立ち上げたあとViewDidLoadで何もせずにアニメーションをはじめると座標がずれてうまくアニメーションができませんでした。なので、起動時に見せるアニメーションは0.1秒のタイマーをいれてあります。

LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る