mitsu's techlog

Customer Reliability Engineerになりました。備忘録も兼ねて技術ネタを適当に。技術以外はこっち→http://mitsu9.hatenablog.com/

UIViewAnimationOptionsの定義から同時に指定できるオプションを理解する。

はじめに

UIViewAnimationOptionsについて

現在アニメーションについて勉強中なのですが、iOSにはUIViewAnimationOptionsという便利なものがあります。

これはOptionを指定するだけでかっこいいアニメーションを自動でしてくれるものです。

例えばTransitionCurlDownを指定するとremoveFromSuperview()に対して以下のようなアニメーションを付けてくれます。

f:id:rayc5:20150620062110g:plain

これは以下のように実装することで使えます。

import UIKit

class ViewController: UIViewController {
    
    var container: UIView!
    var redView: UIView!
    var blueView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // set container frame and add to the screen
        let frame = CGRect(x: 50, y: 50, width: 300, height: 300)
        container = UIView(frame: frame)
        container.backgroundColor = UIColor.grayColor()
        self.view.addSubview(container)
        
        // set redView and blueView; because blueView is added in animation, don't addSubview
        redView = UIView(frame: CGRectMake(0, 0, container.frame.width, container.frame.height))
        redView.backgroundColor = UIColor.redColor()
        container.addSubview(redView)
        
        blueView = UIView(frame: redView.frame)
        blueView.backgroundColor = UIColor.blueColor()
        
        // set button as a trigger of animation
        let button = UIButton(frame: CGRectMake(50, 400, 300, 30))
        button.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
        button.setTitle("start animation", forState: .Normal)
        self.view.addSubview(button)
        
        button.addTarget(self, action: "tapButton:", forControlEvents: .TouchUpInside)
    }
    
    func tapButton(sender: AnyObject) {
        var opt = UIViewAnimationOptions.TransitionCurlDown 
        
        var views = (frontView: redView, backView: blueView)
        
        UIView.transitionWithView(container, duration: 1.0, options: opt, animations: {
            // remove the front object...
            views.frontView.removeFromSuperview()
            
            // ... and add the other object
            self.container.addSubview(views.backView)
            
            }, completion: { finished in
                // any code...
        })
    }
}

UIViewAnimationOptionsの複数指定

このUIViewAnimationOptionsなのですが、以下のように複数指定することもできます。

var opt = UIViewAnimationOptions.TransitionCurlDown | .Repeat

ちなみにこの場合Repeatを設定したので、同様のアニメーションが永遠にリピートされます。

f:id:rayc5:20150620062848g:plain

本題

UIViewAnimationOptionsには横にflipする.TransitionFlipFromLeftと縦にflipする.TransitionFlipFromBottomのオプションがあります。

そこで僕は、この2つのオプションを指定すれば、縦と横が混ざって斜めにflipするようなかっこいいアニメーションが作れるのではと思ったんですねー。

そして少しの期待に胸をふくまらせながら動かしてみると、TransitionFlipFromBottomと全く同じ動きをしました。わかってはいましたがそう上手くいくわけはありません。この2つのオプションを同時に行うのは無理なのでどちらか一方ということでTransitionFlipFromBottomが選ばれたのでしょう。

そして次にしてみたことはTransitionCurlUpとTransitionCurlDownです。全く正反対のオプションを指定するとどうなるのか気になったんですね。

その結果は不思議なことにTransitionFlipFromBottomと同じ動きになったんです。TransitionCurlUpとTransitionCurlDownの2つのオプションから誰がTransitionFlipFromBottomを予想できるでしょうか。

この不思議な結果を受けて、詳細にUIViewAnimationOptionsについて調べて見ることにしました。

内容

UIView Class Referenceで詳細を調べました。

そこでは、UIViewAnimationOptionsはObjective-Cで以下のように実装されていることが書いてあります。

enum {
   UIViewAnimationOptionLayoutSubviews            = 1 <<  0,
   UIViewAnimationOptionAllowUserInteraction      = 1 <<  1,
   UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2,
   UIViewAnimationOptionRepeat                    = 1 <<  3,
   UIViewAnimationOptionAutoreverse               = 1 <<  4,
   UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5,
   UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6,
   UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7,
   UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8,
   UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9,
   
   UIViewAnimationOptionCurveEaseInOut            = 0 << 16,
   UIViewAnimationOptionCurveEaseIn               = 1 << 16,
   UIViewAnimationOptionCurveEaseOut              = 2 << 16,
   UIViewAnimationOptionCurveLinear               = 3 << 16,
   
   UIViewAnimationOptionTransitionNone            = 0 << 20,
   UIViewAnimationOptionTransitionFlipFromLeft    = 1 << 20,
   UIViewAnimationOptionTransitionFlipFromRight   = 2 << 20,
   UIViewAnimationOptionTransitionCurlUp          = 3 << 20,
   UIViewAnimationOptionTransitionCurlDown        = 4 << 20,
   UIViewAnimationOptionTransitionCrossDissolve   = 5 << 20,
   UIViewAnimationOptionTransitionFlipFromTop     = 6 << 20,
   UIViewAnimationOptionTransitionFlipFromBottom  = 7 << 20,
};
typedef NSUInteger UIViewAnimationOptions;

複数指定の際に " | " を使っていたあたりから予想できる通り、ビット演算を利用して様々なオプションが管理されていますね。

UIViewAnimationOptionTransition~に関するものだけに焦点を当てて考えます。

これらのオプションは0~7までの数字を20ビット分左シフトしています。シフトのことは一旦おいておいて、0~7までの数字を2進数表示すると、以下のようになります。

オプション名 10進数 2進数
TransitionNone 0 000
TransitionFlipFromLeft 1 001
TransitionFlipFromRight 2 010
TransitionCurlUp 3 011
TransitionCurlDown 4 100
TransitionCrossDissolve 5 101
TransitionFlipFromTop 6 110
TransitionFlipFromBottom 7 111

これを見ると先ほどのアニメーションが納得いきますね。確かにTransitionCurlUp | TransitionCurlDown は 011 | 100 = 111 なので TransitionFlipFromBottomになっています。

また、これを見ると、TransitionFlipFromLeft | TransitionFlipFromRight = TransitionCurlUpになることなんかもわかります。この場合はFlipFromLeft,Rightという横の動きを足すことでCurlUpという縦の動きに変わるので、文字通りに考えると意味不明の動きをすることになりますね。

この実装からわかるように基本的にTransition系のオプションを2つ以上指定することは推奨されていません。オプションの複数指定は、RepeatとTransition系のオプションを指定するという様に、違う種類のオプションを指定するときに使いましょう。

複数指定して良いオプションについて

UIViewAnimationOptionsの詳細を理解したため、どのオプションが同時に指定できるのかが定義からわかります。

まず最初に書かれている10個のオプション(UIViewAnimationOptionLayoutSubviews ~ UIViewAnimationOptionOverrideInheritedOptions)は、2進数の1桁目から10桁目までのそれぞれの桁がそれぞれのオプションのフラグになっているので、それぞれ独立していることがわかります。したがって、複数のオプションを指定しても大丈夫です。

次に書かれているUIViewAnimationOptionCurveEaseInOut~UIViewAnimationOptionCurveLinearのオプションはTransition系のときと同様に複数指定すると想定外の動作をしてしまいます。

例えば、EaseInとEaseOutを指定するとLinearになってしまいますね。

(これに関してはLinearとEaseInOutを交換すると、EaseIn | EaseOut = EaseInOutになり、もし複数指定しても文字通りに動作するのでそうしたら良いのになと思いましたが、デフォルトがEaseInOutなので仕方ないみたいです。)

最後に

フラグが多いときはそれぞれのフラグを変数にするとフラグまみれになります。これは初心者のコードに多いパターンなのではないでしょうか。

このようなときは、UIViewAnimationOptionsのようにenumにしてビットで管理すると、一つの変数で管理できるのでコードをシンプルになりいいですね。

しかし、ビット演算は使いこなすと強力な武器になりますが、わからずに使うと今回取り上げたようなことが起こり、混乱のもとになります。

自作enumで同じようなことをするときには、実装する側も使う側も、オプションが同時に適用できるのか、片方しか適用できないのかを理解し実装に反映させることで思わぬバグを防ぐことができそうです。