タブバーでタップしたアイコンと違うアイコンがアニメーションしてしまう場合の対応

最初に原因を書くと、UITabBar内のsubviewsの順番が実際の見た目と異なることがあったからでした。
対応としては、subviewsの要素を`frame.minX`でソートしました。

とこれだけだと何を言っているかわからないと思うので（私はわからなかった・・・）、詳しく書いていきます。

## 環境

- Xcode13.3.1
- M1 Mac

## やりたかったこと

UIKitでタブバーを実装していたのですが、アイコンをタップしたら特定のアニメーションを行うというよくある実装をしたかったです。

![app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/0195457a-db3c-eab4-9429-e22d50c5f18c.gif)


## 最初に実装したコード

タブバーのアイコンがタップされた時にアニメーションするビューは`UIView`型のビューを取得する必要がありました。

ただ既存のプロパティ（[`selectedItem`](https://developer.apple.com/documentation/uikit/uitabbar/1623453-selecteditem)や[`selectedImage`](https://developer.apple.com/documentation/uikit/uitabbaritem/1617072-selectedimage)）には、`UIView`型で取得できるものが見つからなかったので、
やむなくsubviewsからindexを指定してビューを取得することにしました。

**※もしそれ以外の方法あればすごく知りたいので、ぜひ教えていただけるとうれしいです :bow:**

```swift
// MARK: - UITabBarControllerDelegate
extension MainTabBarController: UITabBarControllerDelegate {
    func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
        // ①タップされたビューを取得する 
        guard let item = tabBarController.tabBar.selectedItem,
              let index = tabBarController.tabBar.items?.firstIndex(of: item),
              let imageView = tabBarController.tabBar.barItemImageView(index: index)
        else {
            return
        }
        // ②アニメーションする
        iconBounceAnimation(view: imageView)
    }
    
    private func iconBounceAnimation(view: UIView) {
        // アニメーション処理
    }
}

extension UITabBar { 
    func barItemImageView(index: Int) -> UIImageView? {
        let targetIndex = index + 1
        guard subviews.indices.contains(targetIndex) else {
            return nil
        }
        return subviews[targetIndex].recursiveSubviews.compactMap { $0 as? UIImageView }.first
    }
}

extension UIView {
    var recursiveSubviews: [UIView] {
        subviews + subviews.flatMap { $0.recursiveSubviews }
    }
}
```

ちなみに上記のコードは以下を参考にさせていただきました。

https://dev.classmethod.jp/articles/uitabbar_and_uitabbarcontroller/


## 発生したこと

**タップしたアイコンとは違うアイコンがアニメーションする**という現象が発生しました。

ただ困ったことに毎回発生する訳ではなく、再現条件が特定できなかったです
誰かわかれば教えてください・・・

## 原因

原因は、UITabBar内のsubviewsの順番が実際の見た目と異なることがあったことでした。

前述したように再現ができなかったので、View Hierarchyのスクショを載せることができないのが残念ですが、頑張って説明してみます。

大体の場合は、左から右に並んでいる通りに`UITabBarButton`も並んでいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/cf71db84-47ac-3658-f5c0-4ac32a5f33ce.png)

コンソールで`UITabBar`のsubviewsを出力してみるとこんな感じです。

```shell
▿ 5 elements
  - 0 : <_UIBarBackground: 0x7fedfd20b920; frame = (0 0; 390 83); userInteractionEnabled = NO; layer = <CALayer: 0x6000039d5060>>
  - 1 : <UITabBarButton: 0x7fedfd0113d0; frame = (2 1; 94 48); opaque = NO; layer = <CALayer: 0x6000039a3900>>
  - 2 : <UITabBarButton: 0x7fedfd01ff10; frame = (100 1; 93 48); opaque = NO; layer = <CALayer: 0x6000039b83e0>>
  - 3 : <UITabBarButton: 0x7fedfd021ba0; frame = (197 1; 94 48); opaque = NO; layer = <CALayer: 0x6000039b8b80>>
  - 4 : <UITabBarButton: 0x7fedfd20d520; frame = (295 1; 93 48); opaque = NO; layer = <CALayer: 0x6000039d5c40>>
```

`barItemImageView`メソッドの処理を思い出してほしいのですが、
`index + 1`でsubviewsにアクセスしにいっています。つまり**見た目と同じ並びでsubviewsの要素も並んでいることを前提にした実装**になっています。

```swift
func barItemImageView(index: Int) -> UIImageView? {
    let targetIndex = index + 1  // UIBarBackgroundの分+1する
    guard subviews.indices.contains(targetIndex) else {
        return nil
    }
    return subviews[targetIndex].recursiveSubviews.compactMap { $0 as? UIImageView }.first
}
```

ところがたまに以下のように、実際の見た目とsubviewsの要素の並びが一致しないことがあります。

`frame`の一番左の値を見ていただくとわかるかと思うのですが、
先ほどの出力では`2 100 197 295`と小さい順に並んでいたと思うのですが、
以下の出力では`197 100 2 295`と小さい順ではないですよね。

この数値は、frameの[`minX`](https://developer.apple.com/documentation/coregraphics/1455948-cgrectgetminx)になります。

```shell
▿ 5 elements
  - 0 : <_UIBarBackground: 0x7fedfd20b920; frame = (0 0; 390 83); userInteractionEnabled = NO; layer = <CALayer: 0x6000039d5060>>
  - 1 : <UITabBarButton: 0x7fedfd021ba0; frame = (197 1; 94 48); opaque = NO; layer = <CALayer: 0x6000039b8b80>>
  - 2 : <UITabBarButton: 0x7fedfd01ff10; frame = (100 1; 93 48); opaque = NO; layer = <CALayer: 0x6000039b83e0>>
  - 3 : <UITabBarButton: 0x7fedfd0113d0; frame = (2 1; 94 48); opaque = NO; layer = <CALayer: 0x6000039a3900>>
  - 4 : <UITabBarButton: 0x7fedfd20d520; frame = (295 1; 93 48); opaque = NO; layer = <CALayer: 0x6000039d5c40>>
```

アプリの見た目のアイコンの順番は特に問題ないのですが、subviewsの要素の順番は必ずしも見た目と一致する訳ではないようです。
ただアニメーションするビューの取得はsubviewsのindexを指定して取得する実装になっているため、タップしたアイコンと違うアイコンがアニメーションするという現象が発生してしまいました。

## 対応策

`index + 1`でsubviewsにアクセスする前に、アプリの見た目のアイコンの順番と実際にindexでアクセスするsubviewsの要素の順番を一致させます。

そのために、**`frame.minX`でソート**します。

以下のように`barItemImageView`の実装を修正します。

```swift
extension UITabBar {
    var orderedTabBarItemViews: [UIView] {
        // frame.minXでソートする
        subviews.sorted(by: { $0.frame.minX < $1.frame.minX })
    }
    
    func barItemImageView(index: Int) -> UIImageView? {
        let targetIndex = index + 1
        guard orderedTabBarItemViews.indices.contains(targetIndex) else {
            return nil
        }
        return orderedTabBarItemViews[targetIndex].recursiveSubviews.compactMap { $0 as? UIImageView }.first
    }
}
```

念の為、`orderedTabBarItemViews`を出力してみると、こんな感じで、見た目とsubviewsの要素の順番が一致しています。

```shell
po orderedTabBarItemViews
▿ 5 elements
  - 0 : <_UIBarBackground: 0x7fedfd20b920; frame = (0 0; 390 83); userInteractionEnabled = NO; layer = <CALayer: 0x6000039d5060>>
  - 1 : <UITabBarButton: 0x7fedfd0113d0; frame = (2 1; 94 48); opaque = NO; layer = <CALayer: 0x6000039a3900>>
  - 2 : <UITabBarButton: 0x7fedfd01ff10; frame = (100 1; 93 48); opaque = NO; layer = <CALayer: 0x6000039b83e0>>
  - 3 : <UITabBarButton: 0x7fedfd021ba0; frame = (197 1; 94 48); opaque = NO; layer = <CALayer: 0x6000039b8b80>>
  - 4 : <UITabBarButton: 0x7fedfd20d520; frame = (295 1; 93 48); opaque = NO; layer = <CALayer: 0x6000039d5c40>>
```

これでめでたく、タップしたアイコンがアニメーションするようになりました。:blush:

## おわりに

発生頻度もそんなに高くなかったので、見逃していた可能性も高い現象だったなと思います。
再現するうちに原因を特定できてよかった・・・

そもそもsubviewsを取得してindex指定でレビューを取ってくる方法はあまり使用すべきではなさそうですが、いろんなサイトで調べてみてもこの方法が多く他に良い方法も思いつかなかったので、こうなりました。

他にもっといい方法あるよ、そもそもsubviewsにindexでアクセスする必要ないよ、などありましたらぜひコメントいただけると嬉しいです〜


## 参考

https://dev.classmethod.jp/articles/uitabbar_and_uitabbarcontroller/

https://stackoverflow.com/a/51542606