UIKit Dynamicsで物理演算を使った自然なアニメーション


UIKit Dynamics

Viewに物理演算を用いたアニメーションを設定できます。
ドキュメントはこちら

UIDynamicAnimator

Viewに物理に基づいたアニメーションを提供する根幹となるもの

    var dynamicAnimator: UIDynamicAnimator!
    override func viewDidLoad() {
        super.viewDidLoad()
        dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
    }
} 

重力を与える

storyboard上に配置した赤いViewに条件を追加します。
重力を与えるにはUIGravityBehaviorを使用します。

        let gravityBehavior = UIGravityBehavior(items: [redView])
        dynamicAnimator.addBehavior(gravityBehavior)  

ここでデフォルトの重力の大きさは1.0で、大きさ1.0は1000 point/second^2の加速度を意味します。
デフォルトの重力の向きは(0.0, 1.0)です。

パラメータ

パラメータとして次のようなものが設定できます。

プロパティ・メソッド名 説明
gravityDirection CGVector 重力ベクトル(大きさと方向)
angle CGFloat 重力ベクトルの方向(ラジアン)
magnitude CGFloat 重力ベクトルの大きさ
setAngle(_:, magnitude:) (CGFloat, CGFloat) -> Void 重力ベクトルの方向(ラジアン)と大きさ

他にsetAngle(_ angle: CGFloat, magnitude: CGFloat)メソッドを使用しても設定できます。
例えばaddBehavior(_:)する前に以下のように設定すると、

        gravityBehavior.gravityDirection = CGVector(dx: 2.0, dy: 2.0)
        gravityBehavior.magnitude = 1.0

一行目は右下に向かって重力の大きさ2.828..($2\sqrt{2} $)で動くような設定になります。
二行目でmagnitudeを1.0にすると、その方向を保ったままベクトルの大きさが1.0になるので最終的に重力ベクトルは(0.707..., 0.707...)となります。

衝突境界

衝突境界を追加したい場合にはUICollisionBehaviorを使用します。

デバイスの枠を境界とする

translatesReferenceBoundsIntoBoundaryはreferenceViewを境界として扱うかどうかというBool値です。
referenceViewは最初にUIDynamicAnimatorを初期化した時に設定したViewです。
先ほどこれにself.viewを設定したので、この場合デバイスの画面の端が境界になると考えられます。

        let collisionBehavior = UICollisionBehavior(items: [redView])
        collisionBehavior.translatesReferenceBoundsIntoBoundary = true
        dynamicAnimator.addBehavior(collisionBehavior)

自由に境界を設定

CGPointでどこからどこまでを境界とみなすかを指定します。
第一引数で指定するIDは後でこの境界だけ削除する場合などに使います。

        let collisionBehavior = UICollisionBehavior(items: [redView])
        collisionBehavior.addBoundary(withIdentifier: "floor" as NSCopying,
                              from: CGPoint(x: self.view.bounds.width/2 - 20,y: self.view.bounds.height/2 + 200),
                              to: CGPoint(x: self.view.bounds.width/2 + 20, y: self.view.bounds.height/2 + 200))
        dynamicAnimator.addBehavior(collisionBehavior)

この他にUIBezierPathを使用して境界を設定することもできます。

パラメータ

collisionModeで何を衝突対象とするのかを指定できる。
CollisionBehavior.Modeで指定できるのは以下。

CollisionBehavior.Mode 説明 動作
.items UICollisionBehaviorに紐付いているアイテム同士だけが衝突する
.boundaries 設定した境界のみが衝突対象となり、アイテム同士は衝突しない
.everything アイテム同士も境界も衝突する

デフォルトは.everything。

弾性係数・抵抗・摩擦

ある物体に対する弾性係数や抵抗など初期条件になるようなものを設定するにはUIDynamicItemBehaviorを使用します。

        let dynamicItemBehavior = UIDynamicItemBehavior(items: [redView])
        dynamicItemBehavior.elasticity = 1.0
        dynamicItemBehavior.resistance = 0
        dynamicItemBehavior.friction = 0
        dynamicAnimator.addBehavior(dynamicItemBehavior)

パラメータ

設定できる項目には以下のようなものがあります。

プロパティ・メソッド名 説明
density CGFloat 相対的な質量密度(1.0の密度を持つ100 x 100ポイントのitemに、大きさ1.0の力を加えると、100 point/secondで加速する)
elasticity CGFloat 弾性率の大きさ(1.0で完全弾性衝突)
friction CGFloat スライドする時にかかる摩擦の大きさ(1.0で強い摩擦、それ以上の値を指定することも可)
resistance CGFloat 抵抗の大きさ(CGFLOAT_MAXが最高値、1.0を設定した時は力が加えられなくなるとすぐに停止する)
allowsRotation Bool 回転を許可するか(デフォルトでtrue)
angularResistance CGFloat 角抵抗の大きさ
isAnchored Bool アイテムの位置が固定されているか
addLinearVelocity(_:, for:) (CGPoint, UIDynamicItem) -> Void 速度を与える(1秒あたりに動くpoint数を指定)
addAngulerVelocity(_:, for:) (CGFloat, UIDynamicItem) -> Void 角速度を与える(1秒あたりに動くラジアンを指定)

外力

アイテムに外力を加えるにはUIPushBehaviorを使用します。
鉛直投げ上げをするとき、プログラムは以下のようになります。

        let pushBehavior = UIPushBehavior(items: [redView], mode: UIPushBehavior.Mode.instantaneous)
        pushBehavior.pushDirection = CGVector(dx: 0.0, dy: -5.0)
        dynamicAnimator.addBehavior(pushBehavior)

ここで1.0の力とは、連続で1.0の力を与えたとき、密度値が1.0の100ポイントx 100ポイントのビューが100 point/second^2の加速度を持つような大きさをいいます。この値をUIKit Newtonと呼びます。

パラメータ

プロパティ名・メソッド名 説明
mode UIPushBehavior.Mode 力を加えるのが連続的(.continuous)か1度だけ(instantaneous)なのか
pushDirection CGFloat 力のベクトル(大きさと方向)
angle CGFloat 力のベクトルの方向(ラジアン)
magnitude CGFloat 力のベクトルの大きさ
setAngle(_:, magnitude:) (CGFloat, CGFloat) -> Void 力のベクトルの方向(ラジアン)と大きさ
setTargetOffsetFromCenter(_:for:) (UIOffset, UIdynamicItem) -> Void 力が物体のどこにかかるか。指定しない場合は中心にかかる

その他

他にもバネのような動きを実現するUISnapBehaviorや2物体の関係を扱うUIAttachimentBehavior、電場や磁場を設定できるUIFieldBehaviorがあります。

備考

先ほどの自由落下させて完全弾性衝突するプログラムのredViewの開始位置に印をつけました。
誤差があり線を超えたり、線まで届かなかったりします。
大まかな動きを再現するには良いですが、ぴったり数ポイント分の動きを実現したい場合この誤差は無視できないように思います。