2015年7月29日水曜日

2Dアニメ機能のライブラリを試作(本編1)

2Dアニメーションのライブラリを試作した話の続きです。UIViewのサブクラスという形を選択し、いろいろなViewと組み合わせて使えることを想定して作りました。

いよいよソースコードの紹介なのですが、クラス本体の紹介の前に、データ定義を紹介します。指定する項目が多すぎて、かなり長いSwiftコードになってしまいました。

 

最初に作ったデータ定義は、たった1つの構造体で、多くの項目が含まれていました。テストコードを作るのが書きづらく感じ、すぐに分割することにしました。

まず2つに分けようと思ったのですが、上手に分ける基準が見付かりません。分割の試行錯誤を重ねているうちに、3つに分けるのが適切ではないかと思うようになりました。表示する要素(文字列や画像など)に関する情報、アニメーションの共通情報(時間や繰り返し回数など)、2Dアニメーションで指定する情報(位置や回転や透明度など)の3つです。

3つに分けたのですが、構造体名の番号はゼロから始まります。3つめが2Dアニメーションの指定情報で、2番を割り当てられるからです。また、3Dアニメーションで指定する情報の構造体を追加したとき、3番が割り当てられるのも良い点だと考えました。

 

データ定義の構造体を紹介する前に、構造体の中に含まれる項目のうち、列挙型に適したものを選んで、最初に定義しました。次のように、3つあります。

// UI部品の種類
enum ZavType: Int {
    case ZtView = 0
    case ZtImg = 1
    case ZtText = 2
}
// アニメーションの種類
enum ZavAnimate: Int {
    case Za2D = 0 // 2Dアニメ
    case Za3D = 1 // 3Dアニメ
    case ZaEtc = 2 // 特殊アニメ(画像処理など)
}
// 動作速度変化(Timing Functions)の種類
enum ZavTF: Int {
    case ZpLinear = 0
    case ZpIn = 1
    case ZpOut = 2
    case ZpInOut = 3
}

最初は、アニメーションとして動かす表示要素の種類です。汎用的に使うUIVew、画像のUIImage、文字列のStringの3つだけです。UIImageもStringも、最終的にはUIViewの形で生成するのですが、もとの形のままでライブラリへ渡せたほうが使いやすいので、このように作ってみました。

2つめのアニメーションの種類は、今のところ、3種類を考えています。2Dと3Dとその他です。その他は、画像処理を加えながらのアニメーションを考えていますが、細かな内容はまだ未定です。3種類のうち、最初は2Dだけを作りました。

3つめの動作速度変化の種類は、iOSのAPIに用意されている設定項目「Timing Functions」です。時間の変化に比例して動くLinearのほかに、動き出しがゆっくりのIn、終わりがゆっくりのOutなどがあります。この項目が指定できると、アニメーションに速度の変化が付けられて、表現力が向上します。必要だと感じて入れました。

 

準備が終わったので、いよいよデータ定義の構造体です。3つあるので、順番に取り上げます。

最初の構造体は、表示する要素(文字列や画像など)の関する情報です。

struct ZavDef0 {
    var sType: ZavType
    var sAnim: ZavAnimate
    var sObj: AnyObject? = nil
    var sText: String = "dummy"
    var sTSize: CGFloat = 24
    var sTColor: UIColor = UIColor.blackColor()
    var sFuncI: ((UIView) -> Void)? = nil   // 表示部品に対する初期化時の加工処理:処理せずはnil
    init(sType:ZavType, sAnim:ZavAnimate, sObj:AnyObject?, sFuncI:((UIView) -> Void)?) {
        self.sType = sType
        self.sAnim = sAnim
        self.sObj = sObj
        self.sFuncI = sFuncI
    }
    init(sType:ZavType, sAnim:ZavAnimate, sObj:AnyObject?) {
        self.sType = sType
        self.sAnim = sAnim
        self.sObj = sObj
    }
    init(sAnim:ZavAnimate, sTSize:CGFloat, sText:String, sTColor:UIColor, sFuncI:((UIView) -> Void)?) {
        self.sType = .ZtText
        self.sAnim = sAnim
        self.sText = sText
        self.sTSize = sTSize
        self.sTColor = sTColor
        self.sFuncI = sFuncI
    }
    init(sAnim:ZavAnimate, sTSize:CGFloat, sText:String, sTColor:UIColor) {
        self.sType = .ZtText
        self.sAnim = sAnim
        self.sText = sText
        self.sTSize = sTSize
        self.sTColor = sTColor
    }
}

最初の2項目は、列挙型で定義した内容なので、説明不要でしょう。

続くsObjは、UIViewまたはUIImageを受け取るために使います。文字列では使いません。文字列のときにnilとなるので、Optional型にして、デフォルト値としてnilを入れてあります。

続く3項目は、文字列の場合の指定です。文字列、文字サイズ、文字色が含まれます。

最後は、ライブラリ内で生成した表示部品に対して、何かの加工を加えるために用意しました。文字列またはUIImageを指定したときも、ライブラリの内部ではUIViewの形で生成します。この生成されたUIViewに対して加工を加えるため、関数のデータ型を「((UIView) -> Void)?」としました。Optional型を選んだのは、nilのときは機能オフにしたいからです。

構造体の中身の後には、初期化の処理を並べてあります。最初の初期化は、画像またはUIViewで使います。関係する項目だけしか含まれてないので、余計なダミー値を指定する必要はありません。2つ目の初期化は、関数を外したバージョンで、関数なしのときに使います。関数なしの場合がほとんどなので、こちらを多く使うでしょう。3つめと4つめの初期化は、文字列の場合に使います。こちらも関数の有無で2つ用意しました。

今まで作ったデータ定義の構造体では、デフォルトで生成される初期化を使っていました。理由は、初期化の処理を書かなくて済むからです。しかし今回は、指定する項目数が多いので、減らす目的で初期化を多く付けてみました。実際に使ってみると、今回のほうが、格段に使いやすいです。過去に作ったライブラリでも、同じような初期化を追加したくなりました。後で追加しようと思っています。

 

続いては、2番目のデータ定義の構造体です。アニメーションの共通情報(時間や繰り返し回数など)を指定するために使います。

// 共通動作定義:各種アニメーションに共通する項目
struct ZavDef1 {
    var sRepeat: Float = 1      // 繰り返し回数
    var sReverse: Bool = false  // アニメ終了後に逆再生
    var sTimeDo: NSTimeInterval // アニメの実行時間(秒)
    var sDelay: NSTimeInterval  // アニメ開始までの遅延(秒)
    var sHid1: Bool = false     // アニメ開始前に隠す
    var sHid2: Bool = false     // アニメ終了後に隠す
    var sFunc1: ((UIView) -> Void)? = nil   // アニメ開始時に実行する処理:処理せずはnil
    var sFunc2: ((UIView) -> Void)? = nil   // アニメ終了時に実行する処理:処理せずはnil
    init(sRepeat:Float, sReverse:Bool, sTimeDo:NSTimeInterval, sDelay:NSTimeInterval, sHid1:Bool, sHid2:Bool, sFunc1:((UIView) -> Void)?, sFunc2:((UIView) -> Void)?) {
        self.sRepeat = sRepeat
        self.sReverse = sReverse
        self.sTimeDo = sTimeDo
        self.sDelay = sDelay
        self.sHid1 = sHid1
        self.sHid2 = sHid2
        self.sFunc1 = sFunc1
        self.sFunc2 = sFunc2
    }
    init(sRepeat:Float, sReverse:Bool, sTimeDo:NSTimeInterval, sDelay:NSTimeInterval, sHid1:Bool, sHid2:Bool) {
        self.sRepeat = sRepeat
        self.sReverse = sReverse
        self.sTimeDo = sTimeDo
        self.sDelay = sDelay
        self.sHid1 = sHid1
        self.sHid2 = sHid2
    }
}

それぞれの項目の役割はコメントとして付けてあるので、簡単に理解できると思います。補足が必要な項目だけ、追加した理由を中心に解説します。

アニメの開始前と終了後に隠す設定は、複雑な動きを実現するために用意しました。それぞれに表示部品に指定できるアニメーションは、回転や異動などを組み合わせても、1種類の動きしか指定できません。もし途中から違う動きをさせたい場合、もっと複雑な指定方法を組み込むことになります。そうせずに、複雑な動きを実現する方法として、アニメーションの前後に消す指定を用意しました。

複数の動きを連続して見せたいときは、次のように指定します。まず最初の動きを指定して、決まった時間だけ動かします。当然、終了時には隠す設定です。この続きは、別なアニメーション指定を追加して実現します。同じ表示部品を使ったアニメーション指定を、終了した時間に開始します。こちらは、開始前に隠す設定です。すると、2つのアニメーションは繋がって再生され、1つの表示部品が、連続した違う動きをしているように見えます。同様に指定すれば、3つ以上の動きを連続して指定できるというわけです。これが、アニメーションの前後に隠す設定を付けた理由です。

最後の項目である2つの関数は、アニメーションの開始と終了時に実行します。表示部品が渡されるので、表示状態を変更することもできます。また、アプリ側の処理を起動するのにも使えます。

 

最後は、3番目のデータ定義の構造体です。2Dアニメーションで指定する情報(位置や回転や透明度など)を入れています。

// アニメ定義:2Dアニメーションの指定項目
struct ZavDef2 {
    var sX1: CGFloat = 0.0      // 位置
    var sX2: CGFloat = 0.0
    var sY1: CGFloat = 0.0
    var sY2: CGFloat = 0.0
    var sZoom1: CGFloat = 1.0   // 拡大縮小
    var sZoom2: CGFloat = 1.0
    var sRoll1: CGFloat = 0.0   // 回転
    var sRoll2: CGFloat = 0.0
    var sOpa1: Float = 1.0      // 透明度
    var sOpa2: Float = 1.0
    var sTF: ZavTF = .ZpLinear  // 動作速度変化
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sZoom1:CGFloat, sZoom2:CGFloat, sRoll1:CGFloat, sRoll2:CGFloat, sOpa1:Float, sOpa2:Float, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sZoom1 = sZoom1
        self.sZoom2 = sZoom2
        self.sRoll1 = sRoll1
        self.sRoll2 = sRoll2
        self.sOpa1 = sOpa1
        self.sOpa2 = sOpa2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sZoom1:CGFloat, sZoom2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sZoom1 = sZoom1
        self.sZoom2 = sZoom2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sRoll1:CGFloat, sRoll2:CGFloat, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sRoll1 = sRoll1
        self.sRoll2 = sRoll2
        self.sTF = sTF
    }
    init(sX1:CGFloat, sX2:CGFloat,sY1:CGFloat, sY2:CGFloat, sOpa1:Float, sOpa2:Float, sTF:ZavTF) {
        self.sX1 = sX1
        self.sX2 = sX2
        self.sY1 = sY1
        self.sY2 = sY2
        self.sOpa1 = sOpa1
        self.sOpa2 = sOpa2
        self.sTF = sTF
    }
}

表示位置、拡大縮小、回転位置、透明度の4つの項目は、それぞれ開始時と終了時の値を指定します。

ちなみに今回の構造体では、開始が1で終了が2と、変数名を統一してあります。最後に1桁の数字を加える方法なら、変数名を邪魔しない形で、同じ変数名に違いを付けられます。

最後の項目となる動作速度変化だけは、開始と終了という2つの値を持たないので、1桁の数字は付きません。この項目は、2番目の構造体に入れようとも考えたのですが、表示部品の細かな動きを指定する項目なので、3番目の構造体に入れました。

この構造体でも、複数の初期化を用意してあります。単純な動きを指定する際、最小限の項目だけ記述すれば済むようにとの配慮です。まだまだ加えられるので、実際に使ってみて、使う機会の多い組み合わせを、後から追加すると思います。

 

データ定義の構造体を紹介するだけでも、かなり長くなってしまいました。ここで一旦、区切ります。続きは次回の投稿にて。

 

(使用開発ツール:Xcode 6.4, SDK iOS 8.4)

2015年7月27日月曜日

2Dアニメ機能のライブラリを試作(全体設計編)

iOSのAPIには、かなり簡単に使えるアニメーション機能が含まれています。おかげで、いろいろな箇所へアニメーションを付加することが可能です。

それだけにライブラリ化したい重要な候補なのですが、アニメーションが使われる機能や場面が様々で、汎用的なライブラリ化が難しい対象でもあります。

仕方がないので、特定の用途では十分に使えるアニメーション機能として、ライブラリ化してみました。いつものように、UIViewのサブクラスという形でのライブラリ化です。UIViewの中で、指定したアニメーションが動くというわけです。

UIViewであれば、既存のViewに重ねる方法でアニメーションを加えられます。上へ重ねるだけでなく、下に敷いておけば、アニメーションの背景も簡単に実現できるでしょう。

とりあえず、2Dアニメーションの部分だけ試作してみました。どんな形でライブラリ化したのか、どこまでの機能を付けたのか、どれぐらい使いやすいのか、当初期待したほどは良くないのか、いつものように紹介します。

 

まず最初は、大まかな機能を決めます。基本的には、ある程度まで複雑なアニメーションを作れないと、実用にはならないでしょう。そのためには、複数のアニメ部品がそれぞれ独立して動き、全体のアニメーションを構成する形が必須です。

個々のアニメ部品は、それぞれ別々に動きを設定します。使える動きとしては、移動、拡大縮小、回転、透明度変更は必要でしょう。加えて、等速で動くだけでなく、ゆっくり止まるような動きも使いたいはずです。これは、iOSのAPIに含まれるTiming Functionsという指定で可能です。

個々のアニメ部品の動作では、遅延再生や繰り返し再生も必須でしょう。また、再生前と再生後のそれぞれで、部品表示の有無を選べる機能も大事です。これが使えると、いろいろと応用ができますから(この辺の話は使用例で)。

アニメ部品の動きを組み合わせた全体の動作でも、繰り返し再生は必要です。また、一時停止と再生ができると、ユーザーとの対話に役立ちそうです。さらに、キャンセルも必要でしょう。

アニメーションは、動きの指定だけでは実現できません。動かすアニメ部品を用意する必要があります。部品としては、文字列、画像、UIViewの3つを考えました。文字列は、文字列のまま渡すと、文字列の画像が作られて動かせる形です。画像は、画像そのものを動かす形しかないでしょう。UIViewは自由な部品を作れるので、利用範囲を広げるために必須だと考えました。これらをアプリ側が用意して、アニメ機能のライブラリに渡します。

動作の指定は、いつものように定義データとして用意します。その定義データとアニメ部品(文字列、画像、UIView)を組み合わせて、自由なアニメーションを作れるというライブラリを目指しました。

近い将来、3Dアニメーション機能も追加する予定です。そのとき、2Dと3Dを混在させても動くことが必須です。ですから、3D機能を追加しやすいように、また混在しても動くような内部構造で作る必要があります。

 

以上の話をまとめると、次のようになります。

2Dアニメーション機能に求める機能
・UIViewのサブクラスとして作る
・アニメーションの動作は、定義データとして用意する(簡単に使えること)
・定義データ以外には、文字列、画像、UIViewもアニメ部品として使える
・複数の要素を動かせ、それぞれが独立した動きを指定できる
・個々の要素の動きは単純でも構わないが、それを組み合わせて複雑な動きも作れる
・個々の要素の動作として、移動、拡大縮小、回転、透明度を指定できる
・個々の要素の動作として、Timing Functionsを指定できる
・個々の要素の動作として、遅延再生、繰り返しも可能
・個々の要素の動作として、再生前と動作後に、それぞれ非表示も可能
・組み合わせた全体の動作として、繰り返しも可能
・組み合わせた全体の動作として、一時停止と再開、キャンセルも可能
・内部構造では、3D要素を追加しやすく、また2Dと3Dの要素が混在して動くように最初から配慮する

個々のアニメ部品の動きは単純なものばかりですが、それらを組み合わせて使うと、意外に多くのアニメーションを実現できそうです。実際、凝った動きのアニメーションを使う機会はほとんどないですから、これぐらいの機能でも十分だと思います。

 

機能の整理がまとまったので、実現方法の検討に入ります。iOSのAPIに含まれるアニメーション機能は、いろいろな形で用意されているため、どれを使うのか選択しなければなりません。

改めて、いろいろと調べてみたところ、大事な点が分かりました。UIViewから呼び出せるアニメーション機能は、途中でキャンセルできないと判明したのです(もしかしたら間違っているかもしれません。その場合は、お知らせください)。これで、UIViewのアニメーション機能の採用は不可能になりました。

残った候補の中で、一番使いやすいのがCABasicAnimationでした。簡単に使えるものの、機能が意外に多いのが特長です。API上の位置付けとしては、Core Animationを使いやすくするために用意したAPIという感じでしょうか。

UIViewから呼び出せるアニメーション機能よりも、多くの機能があります。将来的な拡張も容易なので、その点でも安心して採用できます。当然ですが、途中のキャンセルも出来ますし、再生の一時停止や再開も簡単です。

 

以上の話をもとに、これから作るクラスの内部構成を作ってみたら、次のようになりました。

2Dアニメーション機能を持つViewクラスの内部構成
・UIViewのサブクラス
・内部データ:定義データ+表示部品、状態保持(再生中、一時停止中)
・初期化(Viewのサイズ、定義データ+表示部品)
・メソッド:
 ・再生開始(繰り返し回数)
 ・一時停止および再開
 ・再生キャンセル
 ・状態問い合わせ(再生中、一時停止中)
・透明ボタン:一時停止と再開
 ・初期状態ではオフ
 ・メソッド:透明ボタンのオンオフ

見て分かるように、機能としては単純です。アニメーション部分だけ作れれば、そのまま完了する感じですね。

アニメーションの再生では、複数の要素が含まれ、それぞれが異なる動作なので、そこさえキッチリと作れれば大丈夫そうです。また、一時停止や再開も、複数の要素を一緒に扱わなければなりません。

実際に作ってみたら、ソースコードは予想外に長くなりました。クラスの本体部分よりも、データ定義のソースが長くなったからです。使い勝手を良くしようと、いろいろな初期化を用意したためです。

 

とりあえず試作したのは、2Dアニメーションだけです。作り終わって一番苦労したと思った点は、アニメーションの処理ではありませんでした。アニメーションの動きを定義する、データ定義の設計に一番苦労したのです。何度か修正を繰り返しながら、やっと落ち着いたという感じでした。

具体的なソースコードは、次回からの投稿で紹介します。

2015年7月18日土曜日

環境設定機能ライブラリもモデルクラス

先日、友人のIT技術者と飲んだとき、このブログの内容に関して、鋭い指摘を受けました。それは、環境設定機能のライブラリも、モデルクラスに含まれるのではないかとの指摘です。

 

言われてみて気付きましたが、確かにそのとおりです。環境設定機能を実現したライブラリは、環境設定機能を汎用化したクラスですが、環境設定機能というモデルでもあります。

ソートや検索といった機能は、必要が無いので持っていません。しかし、定義されたデータ型で値を保持し、アプリとのやりとりを実現しています。また、設定内容をファイルへ保存したり、保存したファイルから環境設定を読み込んだりもできます。まさに、環境設定機能に特化したモデルクラスと言えるでしょう。

 

環境設定機能ライブラリを作ったときは、どのアプリでも簡単に使える環境設定機能を作ろうと考えただけでした。その時点では、モデルクラスまでライブラリ化しようとは考えていません。また、モデルクラスを作り始めたときは、過去に作ったライブラリのどれかが、モデルクラスに該当するなどとは思ってもいませんでした。そのため、友人に指摘されるまで、モデルクラスに該当するとは気付きませんでした。

モデルクラスのライブラリへ環境設定機能が加わったことで、ライブラリは合計3つになりました。また、今回の指摘によって、ライブラリ化するモデルクラスを探す意味で、視野が広がったと思います。どのアプリでも使いそうな機能を洗い出し、それをモデルクラスとしてライブラリ化できそうに感じています。友人の指摘に、大感謝です。

 

環境設定機能ライブラリですが、作ったときのまま改良していません。その後に作った、別なライブラリも増えました。それらを使って、改良できるはずです。

改良点として真っ先に浮かんだのは、ファイルの読み書きを間接参照にするライブラリの利用です。環境設定機能ライブラリでも、ファイルの読み書きを間接参照にして、特定の保存先に依存しない形に改良できます。修正は簡単なので、余裕ができたら実施する予定です。

 

今回の指摘は、一緒に飲んでるときの普通の話として出てきました。でも、このように鋭い指摘をくれる仲間は、本当にありがたいです。お互いに刺激し合いながら、今後も良い関係を続けたいと思います。