[macOS  Swift]  跳ね返るボールのアニメーション

## Timerの定時間隔起動を利用してアニメーションを実現する
#### macOS Mojava 10.14.6 / Xcode 11.3.1 / Swift 5.0

<img src="http://mikomokaru.sakura.ne.jp/data/B54/bauncing_ball.png" alt="[bauncing_ball]" width="350">

###実装のポイント
図形（赤いボール）はNSViewクラスのdrawメソッドにより表示される。drawメソッドはタイマーの定時間隔起動により、ごく短い間隔で起動するように指定する。

１回の図形の移動は一つのベクトルとして表現することができる。ベクトルの距離は速度に相当する。 本例では、ベクトルの距離を４ピクセル、処理の起動間隔を100分の1秒としているが、この組み合わせあたりが限界かもしれない。起動間隔をいくら短くしても処理のオーバーヘッドがあるので数字通りに速くなることはない。

ボールが壁にぶつかったとき、その軸側のベクトル成分の正負を逆転させることでボールの進行方向を変えている。正確には、次のスパンで壁にぶつかると判断した時点で方向を切り替えるので、ほとんどの場合、ごくわずかの差ではあるが壁に到達する前に方向転換が行われることになる。（少なくとも見た目ではわからない）

<img src="http://mikomokaru.sakura.ne.jp/data/B54/bauncing_ball2.png" alt="[bauncing_ball2]" width="400">


ビューのサブクラス

```swift
import Cocoa
class UAView: NSView {
    let diatance :Double = 4.0  //ベクトルの距離（固定）
    var timer: Timer? = nil     //タイマーオブジェクト
    var x :CGFloat = 0          //図形のx座標
    var y :CGFloat = 0          //図形のy座標
    var vx :CGFloat = 0         //ベクトルのx成分
    var vy :CGFloat = 0         //ベクトルのy成分
    let diameter: CGFloat = 30  //ボールの直径
    
    override var isFlipped:Bool {
        get {return true}
    }
    //---- イニシャライザ ----
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.wantsLayer = true
        self.layer?.backgroundColor = NSColor.black.cgColor
        self.layer?.borderWidth = 1.0
        self.changeDirection() //方向をランダムに変える
        //定時起動
        self.timer = Timer.scheduledTimer(timeInterval: 0.01,
                                          target: self,
                                          selector: #selector(reDisplay),
                                          userInfo: nil,
                                          repeats: true)
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    //---- 図形の描画 ----
    override func draw(_ dirtyRect: NSRect) {
        NSColor.red.set()
        let path = NSBezierPath()
        path.appendOval(in: NSRect.init(x: x, y: y, width: diameter, height: diameter))
        path.fill()
    }
    //---- 描画位置の計算 ----
    @objc func reDisplay(){
        x += vx
        y += vy
        if (x > self.frame.width - diameter || x < 0){
            vx = -vx
        }
        if (y > self.frame.height - diameter || y < 0){
            vy = -vy
        }
        self.needsDisplay = true
    }
    //---- 方向をランダムに変える ----
    func changeDirection(){
        //距離4のベクトル成分をランダムに求める
        let angle = Int(arc4random_uniform(UInt32(360)))
        self.vx = CGFloat(diatance * sin(Double(angle) * Double.pi / 180))
        self.vy = CGFloat(diatance * cos(Double(angle) * Double.pi / 180))
    }
}
```

AppDelegate

```swift
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    @IBOutlet weak var window: NSWindow!
    let view = UAView.init(frame: CGRect.init(x: 10, y: 40, width: 300, height: 300))
    let button = NSButton(frame: CGRect.init(x: 10, y: 10, width: 120, height: 20))
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        window.contentView?.addSubview(view)
        window.contentView?.addSubview(button)
        button.title = "changeDirection"
        button.bezelStyle = .roundRect
        button.target = view
        button.action = #selector(view.changeDirection)
    }
}
```