UIViewの異なる2つのAnimationをループさせて動かす

はじめに

UIViewPropertyAnimatorを使ったアニメーションのループについて考えたのでまとめようと思います。

アプローチの仕方はいくつかありますが、日本語の記事で書かれていたのはタイマーで処理を毎回呼び出すというものだけでした。

以下のコードを初期状態として考えていきます。Viewが描画された際に↓方向に200pt移動します。

このアニメーションと反対の動き200pt↑に移動するもを追加して、2つの異なるアニメーションをループさせます

class ViewController: UIViewController {

    @IBOutlet weak var myView: UIView!

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear, animations: {
            self.myView.center.y += 200
        })
        animator.startAnimation()
    }
}

タイマーを使ったループ

文字通りTimerをで定期的にイベントを発火させることで半永久的に処理を行う方法です。

アニメーションが完了していない時でもTimerは必ず呼び出されるので、複雑な処理をしていて少し処理が遅れてしまったりするとずれてしまうことがあるんじゃないか?と思いました。

私がサンプルで検証している限りでは問題なかったので手軽に実装するなら全然ありだとおもました。

class ViewController: UIViewController {

    @IBOutlet weak var myView: UIView!
    
    var animateTimer: Timer!
    var animateMoveUpFlug: Bool = false

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        animateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {_ in
            self.moveAnimation()
        }
    }
    
    func moveAnimation() {
        if animateMoveUpFlug {
             moveUpAnimation()
        } else {
            moveDownAnimation()
        }
    }

    private func moveUpAnimation() {
        let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear, animations: {
            self.myView.center.y -= 200
        })
        animator.startAnimation()
        animateMoveUpFlug = false
    }
    
    private func moveDownAnimation(){
        let animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear, animations: {
            self.myView.center.y += 200
        })
        animator.startAnimation()
        animateMoveUpFlug = true
    }
}

再帰を使ったループ

アニメーションの完了を検知して、次のアニメーションを実行する方法です。

UIViewPropertyAnimatorには以下のようなメソッドが用意されているのでこのメソッドを使用して完了時に自分自身を呼び出すことで半永久的に処理を続けることができます。

open func addCompletion(_ completion: @escaping (UIViewAnimatingPosition) -> Void)

コード


class ViewController: UIViewController {

    @IBOutlet weak var myView: UIView!

    var animateMoveUpFlug: Bool = false

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        moveAnimation()
    }
    
    func moveAnimation() {
        var animator: UIViewPropertyAnimator
        if animateMoveUpFlug {
            animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear, animations: {
                       self.myView.center.y -= 200
                   })
        } else {
            animator = UIViewPropertyAnimator(duration: 1.0, curve: .linear, animations: {
                self.myView.center.y += 200
            })
        }
        animator.startAnimation()
        //完了時に再帰呼び出しを行う
        animator.addCompletion{_ in
            self.animateMoveUpFlug.toggle()
            self.moveAnimation()
        }
    }
}

おまけ

UIViewPropertyAnimatorを使ってループさせるコードを紹介しましたが、UIView.animateでアニメーションをするとすごく簡単に実現することが可能です。

optionで.repeatと.autoreverseが存在しているのでこれらを指定してあげればループします。

UIView.animate(withDuration: 0.8, delay: 0.0, options: [.repeat, .autoreverse, .curveLinear], animations: {
   UIView.setAnimationRepeatCount(Float.infinity)
   myView.center.y += 200
})

UIViewPropertyAnimatorにもoptionを渡すことは可能ですが、手元で試したところautoreverseがうまく機能しませんでした。

調べてみるとUIViewPropertyAnimatorではoptionの一部が機能しないぞ!という記事がいくつかあったのでUIViewPropertyAnimatorでoptionをうまく使ってループさせるのは難しそうでした。

参考文献

UIViewのアニメーションを永続的に行う方法

How can I repeat animation (using UIViewPropertyAnimator) certain number of times?

iOSアプリ開発でアニメーションするなら押さえておきたい基礎