5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOSAdvent Calendar 2020

Day 6

iOS14 で実装された PIP の操作感を UIKit で再現してみた

Last updated at Posted at 2020-12-05

はじめに

今回は、 iOS14 で実装された純正ピクチャ・イン・ピクチャ(PIP と表現)の挙動を再現するにあたってこだわったアニメーション周りの処理についてご紹介いたします。
ご紹介する挙動を皆さんにもより早く体感していただけるよう、この度自作PIPの実装をサポートライブラリを作成 いたしました! iOS14 未満でもPIPをプロダクトで利用することが可能となりますので、ぜひそちらも参考にしてみてください。

🎈FloatingShowable

前提条件

この記事は FloatingShowable を軸に執筆いたしました。このライブラリの機能を簡潔にご紹介いたします。

  • UIViewController を FloatingShowable に準拠させるだけでPIPに対応
  • 上下左右の4箇所に静止
  • PIP から指を離した時、加速度を加味して静止位置を決定
  • PIP の移動可能領域が指定可能

アニメーション

  1. 表示
  2. 非表示
  3. ドラッグ&ドロップして所定位置に移動
  4. 可動領域が変化したときの移動

1, 2, 4 に関しては比較的単純なアニメーションになります。

        UIView.animate(
            withDuration: transitionAnimationWithDuration,
            delay: .zero,
            options: .curveEaseOut,
            animations: { [weak self] in
                self?.view.alpha = 1
            }
        )

options に指定する値に関しては、表示・非表示に .curveEaseOut を指定しています。表示・非表示の開始がゆっくりになると、それを「処理が重くて処理落ちしている」という感覚を得ることがあるためです。

可動領域が変化したときの移動に関しては .curveEaseInOut を指定しています。可動領域を変化させるということは、PIP が重なってはいけないコンテンツが表示または移動したということです。つまり、「逃げる」という挙動になるため、アニメーションの始まりにも滑らかさを出しています。

3 に関してはバネのような効果を追加したアニメーションになります。

       UIView.animate(
           withDuration: gravitateAnimationWithDuration,
           delay: .zero,
           usingSpringWithDamping: gravitateSpringWithDampingRatio,
           initialSpringVelocity: gravitateSpringWithVelocity,
           options: .curveEaseOut,
           animations: { [weak self] in
                guard let self = self else { return }
                self.updateFrame(position: self.position)
            }
       )

各種パラメータは頑張ってチューニングした箇所になりますので、ぜひライブラリ側も参考にしてみてください!
iOS14 純正PIP をじっくり観察してわかることの一つに、このバネ効果があります。停止時に、加速度に合わせて小さく跳ね返っていることがわかると思います。

現在は加速度に関係なく一定のバネ効果を得られるようになっていますが、今後、加速度に合わせたバネ効果となるように改善してい行きます!

加速度

厳密には、ジェスチャー時に得られる速度で判定を行います。

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addGestureRecognizer(
            UIPanGestureRecognizer(target: self, action: #selector(onTransition(sender:)))
        )
    }

    @objc private func onTransition(gesture: UIPanGestureRecognizer) {
        switch gesture.state {
        case .changed:
            ...
        case .ended:
            position = updatePosition(velocity: gesture.velocity(in: nil))
            ...
        default:
            break
        }
    }

gesture.velocity(in: nil) で速度を取得しています。この値のハンドリングも、ぜひライブラリ側を参考にしてみてください!
得られる値は CGPoint です。つまりX軸とY軸向きの速度が取れるということです。これによって今移動しようとしている方向とその速さが計算できます。

PIP の静止位置は速度だけでは決定できません。 今PIPがどこにあるか を考慮する必要があります。
「画面を上下左右向きの4つに分割した時、現在左上にいて、加速度が右下向きに一定定数以上であれば静止位置は右下」
このような判定処理を行います。

さいごに

最後まで読んでいただき、ありがとうございます!
今回は日頃の開発での学びをアウトプットするという方法を記事とライブラリという2軸で行ってみました。これが皆さんの学びのきっかけになれることを願っています。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?