【SwiftUI】Imageを放物線で投げる

# はじめに
SwiftUIを使ったゲームを作っていて、もの（ImageView）を投げる動作を作りたいと思いまして。

放物線でImageを移動させる方法が調べても出てこないので実装を考えて作ってみました。

なお、放物線といっていますが射法投射などの厳密なものではなく（数学苦手なのでわからない）
何となくふわっと移動すると捉えていただければと思います。

#1. 作ったもの
こちらになります。
ふわっと動いていますね。

|![Simulator Screen Recording - iPod touch (7th generation) - 2021-06-02 at 22.31.20.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/5f85b77f-65a0-4323-cb77-d8edc1824c31.gif)|
|:--|


# 2. まずは直線に動かしてみる。
まずは、横直線に動くアニメーションを作ってみます。

動作としては指定した値(width)の分Imageを横に移動させます。

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/34d055f4-e4e4-34c0-9d49-7daeef65fdff.png)

実装については、画像の通り移動量widthを10分割し、一回の移動量(dx)とします。
そしてtimerを使い一定の秒数毎にx座標をdx分移動するようにします。




コードはこちらです。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    // Viewの現在の座標
    @State private var x : CGFloat = 30
    @State private var y : CGFloat = UIScreen.main.bounds.size.height * 0.8
    
    func move(width: CGFloat) {
        // 1回の移動量
        let dx = width / 10
        
        // ImageViewの初期座標
        let ofX : CGFloat = 0
        let ofY : CGFloat = UIScreen.main.bounds.size.height * 0.8
        
        // 初期位置ofXからdxずつ10回移動する。
        var i = 1
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true){ timer in        
            self.x = ofX + CGFloat(i) * dx
            i += 1
            if i > 10 { timer.invalidate()}
        }
    }
    
    var body: some View {
        VStack {
            Image("en").resizable()
                .frame(width: 50.0, height: 50.0, alignment: .leading)
                .position(x: self.x, y: self.y)
                .animation(.easeOut)
            
            Button(action: {
                // ボタンを押すとwidthに指定した分横に移動させる。
                self.move(width: 300)
            }
            , label: {
                Text("Button")
            })
        }
    }
    
}

```

移動する動作はmove関数を作成し、移動量を引数として渡すようにしました。

`withTimeInterval`の値を変えることで移動する速さを調整できます。
ここも引数でもいいかもしれません。


#### 挙動を確認
|![Simulator Screen Recording - iPod touch (7th generation) - 2021-06-02 at 22.38.04.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/2ff8c806-56c7-dc61-d142-c917ed7b874b.gif)|
|:--|


## 3.円に沿って移動させる
続いていよいよ縦にふわっと移動させてみます。
ここでは円を考えてそれに沿って移動する実装としたいと思います。


#### 半径 r = width / 2の円を考える

![図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/b42441b4-c7dd-6168-4492-519cefa64061.png)

イメージはこちらです。
widthを直径とする円を考えてその円に沿ってy座標を更新します。

#### 式
上記の図は原点をrだけずらしているので円の式は

```math
 (x - r)^2 + y^2 = r^2
```
　
これをyについて解くと

```math
  y = \pm\sqrt{r^2 - (x - r)^2}
```

座標として欲しいのはこれのプラス側ですので絶対値を取れば良さそうです。


#### 実装

move()関数を次のように修正します。

```swift
func move(width: CGFloat, height: CGFloat) {
        // 一回の移動量
        let dx = width / 10
        
        // Viewの初期座標
        let ofX : CGFloat = 0
        let ofY : CGFloat = UIScreen.main.bounds.size.height * 0.8
        
        // 直径widthの半径r
        let r = width / 2

        var i = 1
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true){ timer in
            
            self.x = ofX + CGFloat(i) * dx
            self.y = ofY - abs(sqrt(pow(r,2) - pow(CGFloat(i)*dx - r,2)))
            i += 1
            if i > 10 { timer.invalidate()}
        }
    }
```

self.yに先ほど求めた式を代入しています。
注意点としてはiPhoneの画面は左上が原点となりますのでy軸を上に移動させたい時はy座標をマイナスしていきます。

そのためyの初期座標ofYから円の式で求めた数値を引いています。


```swift
self.y = ofY - abs(sqrt(pow(r,2) - pow(CGFloat(i)*dx - r,2)))
```

#### 挙動を確認
|![Simulator Screen Recording - iPod touch (7th generation) - 2021-06-02 at 22.16.28.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/03e257be-fc54-baf9-1758-a2e216ad10bc.gif)|
|:--|

ふわっと移動できました！！

ただ完全な円だと投げる動作としては不自然な感じがありますね。

#### 高さを微調整
対策としてはy座標に適当な数をかけてスケールすることによってそれっぽく見せています。

```swift
self.y = ofY - abs(sqrt(pow(r,2) - pow(CGFloat(i)*dx - r,2))) * 0.6
```

|![Simulator Screen Recording - iPod touch (7th generation) - 2021-06-02 at 22.31.20.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233342/519f8703-f460-b7a2-29dc-c4bcceaad1e3.gif)|
|:--|


以上です！

ちょっと無理矢理なやり方ですがいい方法がありましたらコメントいただけると嬉しいです！