UIImageViewをアニメーションさせながら360°回転させたい

## はじめに
業務では受託案件を担当しているということもあり，
アニメーションを使うことはほぼないです。(提案すればあるいは)
個人アプリでもあまり構想に入っていなくて使ってないです。
妄想や気になるテーマを実際に実装してみる個人アプリがあるのですが，
あるテーマの実装中にふと使いたくなって確認してみたら少し困ったので備忘録です。

追加しようと思ったのは `UIImageView` をアニメーションで 1 回転させる
というただそれだけの実装で，回転の方はアフィン変換の 
`CGAffineTransform` を使い角度は 360° を指定すればいいかな。
アニメーションの方は `UIView` のアニメーションのメソッド
(`animateWithDuration:animations:completion:`)を使えば
すぐ実現できるだろうと思ったけどうまくいかなかった。

## 要件
あるアイテムのセルをタップしたらお気に入り状態になり，
星の画像がアニメーションで 1 回転しながら黄色くなってほしい。

![qiita_animation_base_fix.png](https://qiita-image-store.s3.amazonaws.com/0/88266/2f8598f9-2bb7-c9b6-59e4-dbf7bf487ac9.png "qiita_animation_base_fix.png")

**※妄想なのでお気に入り登録しても各路線のニュースは届きません**

`CGAffineTransform(rotationAngle: CGFloat.pi/180*360)` で
360° を指定してもアニメーションしているように見えない。
いきなり 360° ではなく複数回，角度に分けて実装したら
アニメーションありで回転してくれました。

## 実装
### 開発環境
* Xcode 9.2
* Swift 4

### 360° 回転するだろうと思った実装
下記のようなコードで最初は大丈夫だろうと思っていた。

```swift:HogeViewController.swift
let isFavorite: Bool = /* お気に入り状態の値取得 */
if (isFavorite) {
    // お気に入りだった場合，枠のみの星アイコンを指定
    cell.favoImageView.image = UIImage(named: "ic_vacant_star")
} else {
    // アニメーションしながら 1 回転->360°を指定
    UIView.animate(withDuration: 0.5, animations: {
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*360)
    }, completion: { (_) in
        // アニメーション終了後に満たされた星アイコンを表示
        cell.favoImageView.image = UIImage(named: "ic_filled_star")
    })
}
/* お気に入り状態の更新処理 */
```

#### 結果
星に色はつくけれどもアニメーションしているように見えない。

![qiita_animation_direct_360.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/dce1a4da-40ec-0fad-99aa-07ad48c3b2d9.gif "qiita_animation_direct_360.gif")

ちなみに 180° を指定すると・・・
ちゃんとアニメーションはしている。

![qiita_animation_180.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/6198b627-6264-0329-7f77-8d66a0699267.gif "qiita_animation_180.gif")


### 360度回転させる実装
180° のアニメーションはできているから，
180° 回転させた後 360° を指定して 2 回に分けてみたらどうか。

```swift:HogeViewController.swift
let isFavorite: Bool = /* お気に入り状態の値取得 */
if (isFavorite) {
    cell.favoImageView.image = UIImage(named: "ic_vacant_star")
} else {
    UIView.animate(withDuration: 0.5, animations: {
        // 一旦 180° 回転させる
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*180)
    // 後は同じで 360° を指定する
    cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*360)
    }, completion: { (_) in
        cell.favoImageView.image = UIImage(named: "ic_filled_star")
    })
}
/* お気に入り状態の更新処理 */
```

#### 結果
回転しているように見える。求めていた挙動が得られた。

![qiita_animation_180_360.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/fea4a911-b6fd-a404-6d6a-cf64944218de.gif "qiita_animation_180_360.gif")


### [+α]360°時計回りに回して逆時計回りに 360° 回す
アニメーションの逆再生のオプションである，
`UIViewAnimationOptionAutoreverse` を使う。

```swift:HogeViewController.swift
let isFavorite: Bool = /* お気に入り状態の値取得 */
if (isFavorite) {
    cell.favoImageView.image = UIImage(named: "ic_vacant_star")
} else {
    UIView.animateKeyframes(withDuration: 0.5, delay: 0.0, options: [.autoreverse], animations: {
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*180)
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*360)
    }, completion: { (_) in
        cell.favoImageView.image = UIImage(named: "ic_filled_star")
    })
}
/* お気に入り状態の更新処理 */
```

#### 結果
どうやるんだろうと思っただけでさすがに UX 悪そうなので見送り・・・

![qiita_animation_360_M360.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/888397d6-20b2-d124-abfd-3dd0b9839ea3.gif "qiita_animation_360_M360.gif")

### [+α]複数回回転させる ~単純に~
ぐるんぐるん回転させてみたい。
例えば 3 回転させる場合は，単純に考えると
アニメーションが終わったらアニメーションさせてを繰り返す。
でもさすがにこれだと・・・

```swift:HogeViewController.swift
let isFavorite: Bool =  /* お気に入り状態の値取得 */
if (isFavorite) {
    cell.favoImageView.image = UIImage(named: "ic_vacant_star")
} else {
    UIView.animate(withDuration: 0.5, animations: {
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*180)
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*360)
    }, completion: { (_) in
        UIView.animate(withDuration: 0.5, animations: {
            cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*180)
            cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*360)
        }, completion: { (_) in
            UIView.animate(withDuration: 0.5, animations: {
                cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*180)
                cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*360)
            }, completion: { (_) in
                cell.favoImageView.image = UIImage(named: "ic_filled_star")
            })
        })
    })
}
/* お気に入り状態の更新処理 */
```

#### 結果
こうなるだろうなという結果で回転後の次の回転まで若干ラグい。

![qiita_animation_360_3_3_3.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/b9f4f987-053d-6880-f3e3-66ec9ef6cdbf.gif "qiita_animation_360_3_3_3.gif")


### [+α]複数回回転させる ~Repeatオプション~
アニメーションのオプションで `UIViewAnimationOptionRepeat` を使ってみる。

```swift:HogeViewController.swift
let isFavorite: Bool =  /* お気に入り状態の値取得 */
if (isFavorite) {
    cell.favoImageView.image = UIImage(named: "ic_vacant_star")
} else {
    UIView.animate(withDuration: 0.5, delay: 0.0, options: [.repeat], animations: {
        // リピート回数は 3 回にする
        UIView.setAnimationRepeatCount(3)
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*180)
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*360)
    }, completion: { (_) in
        cell.favoImageView.image = UIImage(named: "ic_filled_star")
    })
}
/* お気に入り状態の更新処理 */
```

#### 結果
3 回転というか 180° 回転を 3 回しているように見える。
でも最後の位置は正しい・・・

![qiita_animation_360*3.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/95cae170-6443-f3f1-692c-92a848d28b62.gif "qiita_animation_360*3.gif")

それぞれの `CGAffineTransform` を作って，
`concatenating` でまとめてもなんかおかしい。
もうちょっと調べてみる。。。

### 他と組み合わせた最終的な実装
試行錯誤して最終的に少しインパクト与えるために途中で大きくして
アニメーション終わったら元のサイズに戻す実装も追加してみて
下記のような感じにしました。

```swift:HogeViewController.swift
let isFavorite: Bool =  /* お気に入り状態の値取得 */
if (isFavorite) {
    cell.favoImageView.image = UIImage(named: "ic_vacant_star")
} else {
    UIView.animate(withDuration: 0.5, animations: {
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*180)
        cell.favoImageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi/180*360)
        cell.favoImageView.image = UIImage(named: "ic_filled_star")
        cell.favoImageView.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
    }) { (_) in
        cell.favoImageView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
    }
}
/* お気に入り状態の更新処理 */
```

#### 結果
なんか最低限のインパクトはあるような気がする。
もっと複雑なものも作ってみたいけど今回はこんなところでいいかな。

![qiita_animation_finalcut.gif](https://qiita-image-store.s3.amazonaws.com/0/88266/3c1c7b66-68f4-6a62-e758-91649f183e32.gif "qiita_animation_finalcut.gif")


## おわりに
今回は UIImageView をアニメーションで
360° 回転させてみる実装について書きました。
こうすればもっと良くなるよ，こう実装すべきなど
ありましたらアドバイスお願いします！

アニメーションってすごく奥深いなぁと思うと同時に
多用は UX 的に良くないなーと思いました。

ご覧いただきありがとうございます。

## 参考
* [Rotate UIButton 360 degrees - Swift](https://stackoverflow.com/questions/34102331/rotate-uibutton-360-degrees-swift)
* [iOSアプリ開発でアニメーションするなら押さえておきたい基礎](https://qiita.com/hachinobu/items/57d4c305c907805b4a53)
* [CGAffineTransform の init(rotationAngle:)](https://developer.apple.com/documentation/coregraphics/cgaffinetransform/1455666-init)