Creative Coding in Swift / C4フレームワークで描画とアニメーション

2016.04.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんぬづは、そろそろ実家の歳の離れた弟に会いたくなってきた田中です。

さて自分ごとで恐縮ですが、まずは宣伝、今度登壇します。

AKIBA.swift

スクリーンショット 2016-04-22 14.06.52

東京では初登壇です!

try! Swift登壇者の@cjwirthさんや、try! Swift主催の@k_katsumiさんもスピーカーなので、ガクブルですが頑張ります!

はい、宣伝はぼちぼちにして今日はSwiftのフレームワークの紹介をしていきますよ。

C4

今日ご紹介しますのは、SwiftでCreative CodingをするオープンソースフレームワークのC4。 C4はUIKitとCoreAnimationを元に作られています。 opneFrameworksやProcessingなどの他のCreative Coding Frameworkに慣れた人にとってはとても簡単で、初心者でも扱いやすいということがコンセプトになっているようです。

Tutorialも多く用意されていて、中にはSlackのロゴアニメーションや、Skypeのローディングなどが紹介されていて、実践的です。

インストール

公式ページを見ると、インストール方法がカンフーマスターで例えられていくつか紹介されています。 中国武術をやっている身としてはとても親近感。

スクリーンショット 2016-04-22 14.13.06

フレームワークをダウンロードしてきてプロジェクトに追加する基本的なやり方は、低難度で親しみやすさダントツのJC(ジャッキー・チェン。女子中学生ではない)から、最高難度がターミナルから直接git叩いてインストールしてくれって書いてあるブルース・リー。

僕は今回iOS開発者ならお馴染みの、Cocoapodsを使ってインストールしました。

use_frameworks!

pod 'C4', '~> 1.0'

上記をPodfileに記述してpod install。

サンプル

今回サンプルとしてこんなものをつくってみました。

どこかで見覚えのある九つの色、のついた円を順々にアニメーションさせて、最後に消えていくオレンジの円は他の円と違うアニメーションを加えました。

以下がコードの全文です。 なかなか長くなっていますが、繰り返し書いているためです。 パターンとしては多くありません。

import UIKit
import C4

class ViewController: CanvasController {

    override func setup() {
        
        // MARK: - Circles
        let orange      = createCircle()
        let gray        = createCircle()
        let blue        = createCircle()
        let green       = createCircle()
        let red         = createCircle()
        let yellow      = createCircle()
        let pink        = createCircle()
        let lightblue   = createCircle()
        let purple      = createCircle()
        
        
        // MARK: - Colors
        let orangeColor = Color(red: 226.0/255.0, green: 122.0/255.0, blue: 4.0/255.0, alpha: 1)
        orange.fillColor      = orangeColor
        orange.strokeColor    = orangeColor
        
        let grayColor = Color(red: 125.0/255.0, green: 125.0/255.0, blue: 125.0/255.0, alpha: 1)
        gray.fillColor        = grayColor
        gray.strokeColor      = grayColor
        
        let blueColor = Color(red: 2.0/255.0, green: 104.0/255.0, blue: 183.0/255.0, alpha: 1.0)
        blue.fillColor        = blueColor
        blue.strokeColor      = blueColor
        
        
        let redColor = Color(red: 235.0/255.0, green: 66.0/255.0, blue: 44.0/255.0, alpha: 1.0)
        red.fillColor         = redColor
        red.strokeColor       = redColor
        
        let greenColor = Color(red: 4.0/255.0, green: 164.0/255.0, blue: 105.0/255.0, alpha: 1.0)
        green.fillColor       = greenColor
        green.strokeColor     = greenColor
        
        let yellowColor = Color(red: 241.0/255.0, green: 172.0/255.0, blue: 0.0/255.0, alpha: 1.0)
        yellow.fillColor      = yellowColor
        yellow.strokeColor    = yellowColor
        
        
        let pinkColor = Color(red: 232.0/255.0, green: 65.0/255.0, blue: 143.0/255.0, alpha: 1.0)
        pink.fillColor        = pinkColor
        pink.strokeColor      = pinkColor
        
        let lightblueColor = Color(red: 0.0/255.0, green: 143.0/255.0, blue: 215.0/255.0, alpha: 1.0)
        lightblue.fillColor   = lightblueColor
        lightblue.strokeColor = lightblueColor
        
        let purpleColor = Color(red: 113.0/255.0, green: 79.0/255.0, blue: 156.0/255.0, alpha: 1.0)
        purple.fillColor      = purpleColor
        purple.strokeColor    = purpleColor
        
        
        // MARK: - Positions
        orange.center = canvas.center
        gray.center = Point(CGPoint(x: canvas.center.x - 100, y: canvas.center.y))
        blue.center = Point(CGPoint(x: canvas.center.x + 100, y: canvas.center.y))
        
        red.center = Point(CGPoint(x: canvas.center.x, y: canvas.center.y - 120))
        green.center = Point(CGPoint(x: canvas.center.x - 100, y: canvas.center.y - 120))
        yellow.center = Point(CGPoint(x: canvas.center.x + 100, y: canvas.center.y - 120))
        
        pink.center = Point(CGPoint(x: canvas.center.x, y: canvas.center.y + 120))
        lightblue.center = Point(CGPoint(x: canvas.center.x - 100, y: canvas.center.y + 120))
        purple.center = Point(CGPoint(x: canvas.center.x + 100, y: canvas.center.y + 120))
        
        
        // MARK: - Add circles
        canvas.add(orange)
        canvas.add(gray)
        canvas.add(blue)
        canvas.add(red)
        canvas.add(green)
        canvas.add(yellow)
        canvas.add(pink)
        canvas.add(lightblue)
        canvas.add(purple)
        
        
        // MARK: - Animations
        let scale = Transform.makeScale(100, 100)
        let fadeOutColor = Color(red: 0, green: 0, blue: 0, alpha: 0)
        
        let orangeAnim = ViewAnimation(duration: 1.0) {
            orange.transform = scale
            orange.fillColor = fadeOutColor
        }
        let grayAnim = ViewAnimation(duration: 0.7) {
            gray.transform = scale
            gray.fillColor = fadeOutColor
        }
        let blueAnim = ViewAnimation(duration: 0.7) {
            blue.transform = scale
            blue.fillColor = fadeOutColor
        }
        
        let redAnim = ViewAnimation(duration: 0.7) {
            red.transform = scale
            red.fillColor = fadeOutColor
        }
        let greenAnim = ViewAnimation(duration: 0.7) {
            green.transform = scale
            green.fillColor = fadeOutColor
        }
        let yellowAnim = ViewAnimation(duration: 0.7) {
            yellow.transform = scale
            yellow.fillColor = fadeOutColor
        }
        
        let pinkAnim = ViewAnimation(duration: 0.7) {
            pink.transform = scale
            pink.fillColor = fadeOutColor
        }
        let lightblueAnim = ViewAnimation(duration: 0.7) {
            lightblue.transform = scale
            lightblue.fillColor = fadeOutColor
        }
        let purpleAnim = ViewAnimation(duration: 0.7) {
            purple.transform = scale
            purple.fillColor = fadeOutColor
        }
        
        let orangeShrink = ViewAnimation(duration: 0.5) {
            orange.transform = Transform.makeScale(0.3, 0.3)
        }
        orangeShrink.curve = .EaseIn
        
        let sequence = ViewAnimationSequence(animations: [
            pinkAnim, lightblueAnim, purpleAnim,
            redAnim, greenAnim, yellowAnim,
            grayAnim, blueAnim, orangeShrink, orangeAnim
            ])
        
        wait(1.0) {
            sequence.animate()
        }
    }
    
    private func createCircle() -> Circle {
        let rect = Rect(0, 0, 50, 50)
        
        return Circle(frame: rect)
    }

}

解説

C4の導入

まずC4を使うために

import C4

と記述して、C4をインポートします。

ミソとなるのはC4を扱うためにはUIViewControllerを継承して作られたCanvasViewControllerというクラスを扱うこと。基本的としてはこのクラスを使って、定義してあるsetup()の中でアニメーションや図形を扱うとよいでしょう。

class viewController: CanvasViewController {
	override func setup() {

	}
}

CanvasViewControllerの定義を見に行くとsetup()が定義されており、このsetup()はUIViewControllerのviewDidLoad()の中で呼び出されていることがわかります。

public override func viewDidLoad() {
    canvas.backgroundColor = C4Grey
    ShapeLayer.disableActions = true
    self.setup()
    ShapeLayer.disableActions = false
}

図形の生成

tutorialを見るとわかるのですが、基本図形となるようなものはC4の中で定義されています。

今回はCircleを生成して、生成したインスタンスのサイズ、ポジション、色などを設定してcanvasにaddしています。 基本的なUIKitの使い方ととても似ているのでとっかかりやすいですね!

let circle = Circle(frame: Rect(0, 0, 50, 50))
// 図形の中の色
circle.fillColor = Color(red: 226.0/255.0, green: 122.0/255.0, blue: 4.0/255.0, alpha: 1)
// 図形の枠線の色
circle.strokeColor = Color(red: 226.0/255.0, green: 122.0/255.0, blue: 4.0/255.0, alpha: 1)
canvas.add(circle)

アニメーション

単一のアニメーションの生成は以下のようにします。

// アニメーションの生成。0.5秒の間にclodureの中の処理を行う
let animation = ViewAnimation(duration: 0.5) {
	// circleのサイズを0.3倍にする
    circle.transform = Transform.makeScale(0.3, 0.3)
    // alphaを0としているので、フェードアウトする
    circle.fillColor = Color(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
}

// アニメーション開始時を早くする
animation.curve = .EaseIn
// アニメーションを繰り返し行う
animation.repeats = true
// アニメーションが終了したら逆再生を行う
animation.autoreverses = true

// アニメーションの再生
animation.animate()

円形のサイズを広げていくようなアニメーションはリッチ(派手)なUIなんかでよく実装されているのを見ますが、自前で書こうとするとviewを二枚重ねてmaskを設定してゴニョゴニョ...となってなかなか面倒くさかったりします。CircleをmakeScaleして広げる上記はなかなか便利です。

アニメーションを連続させる

アニメーションAが終わったらアニメーションBを再生して、次にアニメーションCをつなげて...と順次実行していきたいというパターンはよくあると思います。

自前で実装しようとすると、completionHandlerで終了を判定してネストを深くしていくか、もしくは非同期処理をメソッドチェーンで書けるようなライブラリを別途導入することも考えますがここら辺は面倒だったりします。

安心してください、便利クラスが用意されてますよ。

// 初期化時のanimationsの引数の型は[ViewAnimation]。
let sequence = ViewAnimationSequence(animations: [animationA, animationB, animationC])
// シークエンスの再生
sequence.animate()

サンプルコードで使用した部分は大体このくらいです。

注意

Colorクラスの初期化時にrgbを指定するinitializerは二つ用意されています。

init(red: Double, green: Double, blue: Double, alpha: Double)
init(red: Int, green: Int, blue: Int, alpha: Double)

実装はそれぞれ以下のようになっています。

public init(red: Double, green: Double, blue: Double, alpha: Double) {
    colorSpace = CGColorSpaceCreateDeviceRGB()!
    internalColor = CGColorCreate(colorSpace, [CGFloat(red), CGFloat(green), CGFloat(blue), CGFloat(alpha)])!
}
public convenience init(red: Int, green: Int, blue: Int, alpha: Double) {
    self.init(red: Double(red) / 255.0, green: Double(green) / 255.0, blue: Double(blue) / 255.0, alpha: alpha)
}

Doubleを引数としている方はそのままの値なのですが、Intを引数としている方は/255.0されています。

そのため、Doubleを引数とする場合は/255.0を自分で記述し、Intを引数とする場合は記述しなくても良いというところが少しハマりポイントかと思われますので、注意してみてください。

まとめ

アプリにおいてはアニメーションを少しつけてあげるだけでも、ユーザーにとって楽しいアプリになったりします。

C4を使ってより良いUXを獲得していきましょう。?ファイトだよ!?