最初に原因を書くと、UITabBar内のsubviewsの順番が実際の見た目と異なることがあったからでした。
対応としては、subviewsの要素をframe.minX
でソートしました。
とこれだけだと何を言っているかわからないと思うので(私はわからなかった・・・)、詳しく書いていきます。
環境
- Xcode13.3.1
- M1 Mac
やりたかったこと
UIKitでタブバーを実装していたのですが、アイコンをタップしたら特定のアニメーションを行うというよくある実装をしたかったです。
最初に実装したコード
タブバーのアイコンがタップされた時にアニメーションするビューはUIView
型のビューを取得する必要がありました。
ただ既存のプロパティ(selectedItem
やselectedImage
)には、UIView
型で取得できるものが見つからなかったので、
やむなくsubviewsからindexを指定してビューを取得することにしました。
※もしそれ以外の方法あればすごく知りたいので、ぜひ教えていただけるとうれしいです
// 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
も並んでいました。
コンソールで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>>
これでめでたく、タップしたアイコンがアニメーションするようになりました。
おわりに
発生頻度もそんなに高くなかったので、見逃していた可能性も高い現象だったなと思います。
再現するうちに原因を特定できてよかった・・・
そもそもsubviewsを取得してindex指定でレビューを取ってくる方法はあまり使用すべきではなさそうですが、いろんなサイトで調べてみてもこの方法が多く他に良い方法も思いつかなかったので、こうなりました。
他にもっといい方法あるよ、そもそもsubviewsにindexでアクセスする必要ないよ、などありましたらぜひコメントいただけると嬉しいです〜
参考