anz blog

ButtonStyle を利用する時に if 文を使うと動かなくなる場合がある

2024-08-08 #Swift #SwiftUI

ボタンの見た目や振る舞いなどをいじる時に ButtonStyle をカスタムしてそれを適用することがあるかと思いますが、その時に if 分を使っていると iOS のバージョン次第では動かなくなるかも?というお話です。

環境

  • Xcode 15.4

詳細

とりあえず先に問題となるコードから。

struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        if configuration.isPressed {
            configuration.label
                .opacity(0.6)
        } else {
            configuration.label
        }
    }
}

やりたいことしてはボタン押下時の見た目をすこし変えるという感じです。
一見すると問題ないようにみえるのですが、Simulator の iOS 16.x で動かすと MyStyleButton が適用されたボタンの反応がとても悪くなります。
(タップしても反応しなかったりしたりみたいな感じです)
iOS 15.x だと全く反応しなくなります。
iOS 17.x では問題なく動作します。

※ もしかしたら実機では問題なく動くかもしれませんが、Simulator 上で動かないというだけで開発するうえで障りがあるので確認していません。

対応方法

冒頭やタイトルかわらもわかるとは思いますが、割と簡単で if 文を使わないということです。

struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .opacity(configuration.isPressed ? 0.6 : 1.0)
    }
}

こちらのほうがコードもスッキリしますし、ButtonStyle で if 文を使うときは注意が必要ですというお話でした。

いろいろ検証した話

ここからは自分が色々ゴニョゴニョやってみた話で特にみになるものではないですが、一応記録として。

.opacty() という modifier を通したものとそうでないものでなにか内部的に管理に違いがあって、その齟齬からこういう現象が発生するかと考え取っ払ってやってみました。

struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        if configuration.isPressed {
            configuration.label
        } else {
            configuration.label
        }
    }
}

こういう感じ。結果はかわらず iOS 16 以下だと期待した動作になりませんでした。
ということで、 if で挟むことでなにか変わるのかということで、一旦 VStack で囲んで見ました。

struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        VStack {
            if configuration.isPressed {
                configuration.label
                    .opacity(0.6)
            } else {
                configuration.label
            }
        }
    }
}

こちらは iOS 16 以下でも期待した挙動になりました。とはいえ、流石にViewの構造に与える影響が大きすぎるので採用はもちろんしませんが。
(ちなみに、 Group ではだめです)

if で挟むだけでも内部的なIDが変わるとかで、こういう挙動になるのかもしれません。iOS 17+ からはそこらをうまく吸収しだしたとか?
ここらへんの View Identity の話をちゃんと理科していると、特に不思議でもなんでもない挙動なのかもしれない

ButtonStyle を調べると多くのところで問題が発生しない書き方で紹介されている(少なくとも僕は問題がある方で書かれているのは発見できなかった)のにも関わらず、なんで if 文で書いていたのかということですが、
単純に複数の modifier を弄りたいとなったときに都度都度判定させるのが個人的にあまり好きなかったからですね。

struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .opacity(configuration.isPressed ? 0.6 : 1.0)
            .background(configuration.isPressed ? .gray : .clear)
    }
}

こういうの見るとなんとなく分岐をまとめたくなるというか(笑)