LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-07-16

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

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

環境

  • Xcode13.3.1
  • M1 Mac

やりたかったこと

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

app.gif

最初に実装したコード

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

ただ既存のプロパティ(selectedItemselectedImage)には、UIView型で取得できるものが見つからなかったので、
やむなくsubviewsからindexを指定してビューを取得することにしました。

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

// 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 }
    }
}

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

発生したこと

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

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

原因

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

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

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

image.png

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

▿ 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の要素も並んでいることを前提にした実装になっています。

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になります。

▿ 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の実装を修正します。

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の要素の順番が一致しています。

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でアクセスする必要ないよ、などありましたらぜひコメントいただけると嬉しいです〜

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0