UIGestureRecognizerに入門した

##はじめに

UIGestureRecognizerを使うことでタップやスワイプ、ピンチインといった操作に対応することができるようになります。リファレンスは[こちら](https://developer.apple.com/documentation/uikit/uigesturerecognizer)。これを継承しているクラスとして

- [UITapGestureRecognizer](https://developer.apple.com/documentation/uikit/uitapgesturerecognizer)
- [UIPinchGestureRecognizer](https://developer.apple.com/documentation/uikit/uipinchgesturerecognizer)
- [UIRotationGestureRecognizer](https://developer.apple.com/documentation/uikit/uirotationgesturerecognizer)
- [UISwipeGestureRecognizer](https://developer.apple.com/documentation/uikit/uiswipegesturerecognizer)
- [UIPanGestureRecognizer](https://developer.apple.com/documentation/uikit/uipangesturerecognizer)
- [UIScreenEdgePanGestureRecognizer](https://developer.apple.com/documentation/uikit/uiscreenedgepangesturerecognizer)
- [UILongPressGestureRecognizer](https://developer.apple.com/documentation/uikit/uilongpressgesturerecognizer)
- [UIHoverGestureRecognizer](https://developer.apple.com/documentation/uikit/uihovergesturerecognizer)

がありますが、ここではScreenEdgeとHoverとSwipeを除いた5つのクラスを使って以下のような動きを持つサンプルアプリを作成していきます。

![a9etg-g84nw.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549093/f5ae4175-b508-e1d0-5de2-3789d313d25f.gif)


## 準備

メインとなるビューに赤色の`SampleView`を貼り付けて挙動を確認します。

```swift
import UIKit

class ViewController: UIViewController {

    let sampleView = SampleView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(sampleView)
        // Do any additional setup after loading the view.
    }
}
```

基本的にはこの`SampleView`に変化を加えていきます。

```swift
class SampleView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
```

## UITapGestureRecognizer

**Tap**なので最も基本的なタップ動作です。タップされることで色を変化させています。

### AddGestureRecognizerでタップ判定を付与する

`tapObject(_ : UITapGestureRecognizer)`という関数を指定しています。

```swift
override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .red
        addGestureRecognizer((UITapGestureRecognizer(target: self, action: #selector(tapObject(_:)))))
    }
```

### タップされた時に動く関数を作る

背景色をランダムに変えています。

```swift
@objc func tappedObject(_ sender: UIHoverGestureRecognizer) {
        self.backgroundColor = UIColor.randomColor
}
```

`randomColor`については以下のように拡張しています。

```swift
extension UIColor {
    static var randomColor: UIColor {
        let r = CGFloat.random(in: 0 ... 255) / 255.0
        let g = CGFloat.random(in: 0 ... 255) / 255.0
        let b = CGFloat.random(in: 0 ... 255) / 255.0
        return UIColor(red: r, green: g, blue: b, alpha: 1.0)
    }
}
```

これで`SampleView`をタップされることで色が変化するViewにすることができました。

## UIPanGestureRecognizer

panは動詞でゆっくりと左右に動くという意味のようです。

### AddGestureRecognizerでtap判定を付与する

```swift
addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panObject(_:))))
```

### panを判定したときに動く関数を作る

panされた時にその距離だけ`sampleView`を動かす操作を実装します。

```swift
@objc func panObject(_ sender: UIPanGestureRecognizer) {
        self.transform = CGAffineTransform(translationX: sender.translation(in: self).x, y: sender.translation(in: self).y)
}
```

`translation`は`UIPanGestureRecognizer`クラスのプロパティで、パン開始時の点を原点とした時の座標を返します。

`tansform`はviewの持つCGAffinetransform型のプロパティで、viewに移動などの変換を加える時に使います。viewの持つ`transform`を`move`の分動かしたものに置き換えることで、panによる移動を表現できそうです。

しかしこのまま実行すると一度目の移動はうまくいくのですが、二度目以降のpanの際にviewの位置がリセットされてしまいます。

理由はviewの持つ`transform`に`move`だけ平行移動する行列をそのまま入れているため、毎度リセットされた状態からスタートしているからです。

### 座標を引き継ぐ

上述した問題を解決するには、viewの持つコマンドの中でviewの持つ`transform`をコピーしておく役割を持つプロパティを作成する必要があります。

```swift
@objc func panObject(_ sender: UIPanGestureRecognizer) {
        if sender.state == .began {
            ownTransform = self.transform
        }
        self.transform = ownTransform.translatedBy(x: sender.translation(in: self).x, y: sender.translation(in: self).y)
    }
```

`state`はUIGestureRecognizerが持つプロパティで、ジェスチャがどのようなものなのかということを返します。その`state`が`.began`、つまりジェスチャが開始された時に、`ownTransform`に自身のviewの`transform`をコピーしておきます。

そして、viewの`transform`を`ownTransform`、つまりこれまでviewがされた変換の情報を持つCGAffineTransformにさらに平行移動を施しています。

## UIPinchGestureRecognizer

### AddGestureRecognizerでpinch判定を付与する

```swift
addGestureRecognizer((UITapGestureRecognizer(target: self, action: #selector(tapObject(_:)))))
```

### pinchされた時に動く関数を作る

ピンチインで拡大、ピンチアウトで縮小を表現していきます。

```swift
@objc func pinchObject(_ sender: UIPinchGestureRecognizer) {
        if sender.state == .began {
            ownTransform = self.transform
        }
        self.transform = ownTransform.scaledBy(x: sender.scale, y: sender.scale)
}
```

`scale`は$$\frac{ピンチ中の指の幅タップされた時の幅}{ピンチ開始時の指の幅}$$です。

ここでも先程のパンによる平行移動と同様に`ownTransform`プロパティを使用しています。

## UIRotateGestureRecognizer

### AddGestureRecognizerでrotate判定を付与する

```swift
addGestureRecognizer(UIRotationGestureRecognizer(target: self, action: #selector(rotateObject(_:))))
```

### rotateを判定した時に動く関数を作る

二本指でタップされ回転された時、それに合わせてViewを回転させていきます。ここでも先程定義した`ownTransform`プロパティを使用しています。

```swift
@objc func rotateObject(_ sender: UIRotationGestureRecognizer) {
        let rotation = sender.rotation
        if sender.state == .began {
            ownTransform = self.transform
        }
        self.transform = ownTransform.rotated(by: rotation)
}
```

`rotation`はタップされた時の二本指を通る直線と現在の二本指を通る直線のなす角です。ここでも先程までと同様に、rotateが始まった時に`ownTransform`に自身の`transform`をコピーし、その後そのコピーに回転を加える作用を施したものをviewの`translation`としています。

## UILongPressGestureRecognizer

### AddGestureRecognizerでlongpress判定を付与する

```swift
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressedObject(_:))))
```

### longpressを判定した時動く関数を作る

0.5秒かけて2倍に拡大するアニメーションを作成しています。

```swift
@objc func longPressedObject(_ :UILongPressGestureRecognizer) {
        UIView.animate(withDuration: 0.5, delay: 1.0, animations: {
            self.transform = CGAffineTransform(scaleX: 2, y: 2)
        })
}
```

## ソースコード

ファイルを分けていないのでコピペで動きます。

```swift
import UIKit

class ViewController: UIViewController {

    let sampleView = SampleView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(sampleView)
        // Do any additional setup after loading the view.
    }
}

class SampleView: UIView {
    var ownTransform: CGAffineTransform!
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .red
        addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panObject(_:))))
        addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(pinchObject)))
        addGestureRecognizer(UIRotationGestureRecognizer(target: self, action: #selector(rotateObject(_:))))
        addGestureRecognizer((UITapGestureRecognizer(target: self, action: #selector(tapObject(_:)))))
        addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressedObject(_:))))
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    @objc func panObject(_ sender: UIPanGestureRecognizer) {
        if sender.state == .began {
            ownTransform = self.transform
        }
        self.transform = ownTransform.translatedBy(x: sender.translation(in: self).x, y: sender.translation(in: self).y)
    }
    
    @objc func tapObject(_ sender: UITapGestureRecognizer) {
        self.backgroundColor = UIColor.randomColor
    }
    
    @objc func rotateObject(_ sender: UIRotationGestureRecognizer) {
        let rotation = sender.rotation
        if sender.state == .began {
            ownTransform = self.transform
        }
        self.transform = ownTransform.rotated(by: rotation)
    }
    @objc func pinchObject(_ sender: UIPinchGestureRecognizer) {
        if sender.state == .began {
            ownTransform = self.transform
        }
        self.transform = ownTransform.scaledBy(x: sender.scale, y: sender.scale)
    }
    @objc func longPressedObject(_ :UILongPressGestureRecognizer) {
        UIView.animate(withDuration: 0.5, delay: 1.0, animations: {
            self.transform = CGAffineTransform(scaleX: 2, y: 2)
        })
    }
}
extension UIColor {
    static var randomColor: UIColor {
        let r = CGFloat.random(in: 0 ... 255) / 255.0
        let g = CGFloat.random(in: 0 ... 255) / 255.0
        let b = CGFloat.random(in: 0 ... 255) / 255.0
        return UIColor(red: r, green: g, blue: b, alpha: 1.0)
    }
}
```