Open6

SwiftUI.Buttonのタップ領域を広げようとした話

だじだじ

SwiftUI.Buttonのタップ領域を広げようとして試行錯誤したけど、あんまりうまくいかなかった…けど知見はあったのでメモしておきます。

だじだじ

前提としてButtonのタップ領域は、(普通に書いた場合)Button自体のフレームではなくlabelのフレームになります。

var button: some View {
    Button {
        print("ボタン押したで")
    } label: {
        Text("ラベル") // タップ領域を広げたいならこっちにpaddingをつける
    }
    .padding() // labelとButtonの間のスペースはタップできない
}

タップ領域 = labelのフレームということは、Buttonのフレームからはみ出してタップ領域を定義することができません。どうしましょう。

だじだじ

方法1. 無理やり貫通させる

実はlabelはButtonの大きさを貫通することができます。
labelを大きくしてもButtonも大きくなるだけなので、frame()でButtonの大きさを固定する必要があります。

var button: some View {
    Button {
        print("ボタン押したで")
    } label: {
        Text("ラベル")
            .frame(width: 300, height: 300) // labelを大きく
    }
    .frame(width: 100, height: 100) // labelよりも小さく
}

labelとButtonにボーダーをつけると、labelが貫通しているのがわかりやすいです。
VStackなどで隣に他のViewを並べると、labelは隣のViewにも重なってレイアウトの邪魔をしません。

問題点1

概ねやりたいことができているのですが、Button自体の大きさを固定したため、大きさに関するアクセシビリティが軒並み解決できなくなってしまいます。
ローカライズによって文字数が増えた場合や、Dynamic Typeによって文字が大きくなった場合に、Buttonのフレームがさらに大きいものを要求してくるかもしれませんが対応できません。(Dynamic Typeは@ScaledMetricでなんとかなるかも?)

問題点2

Appleが意図したものかわからないので、いずれ使えなくなるリスクをがあります。

だじだじ

Buttonの大きさに影響させずにlabelを大きくする方法として、background()を利用する手法があります。

var button: some View {
    Button {
        print("ボタン押したで")
    } label: {
        Text("ラベル")
            .background { // Buttonの大きさには影響しない(Buttonをはみ出す)
                Color.red
                    .frame(width: 300, height: 300)
            }
    }
}

しかしこのbackground部分のViewはタップすることができません。残念。
background部分をタップできれば全て解決ですが…

だじだじ

方法2. background + ButtonStyleで不思議なことをする

ButtonStyleによって↑の問題は半分くらい解決することができます。(半分の理由は後ほど…)
理由は分かりませんが、ButtonStyle内でlabelにbackgroundをつけた場合、backgroundにもタップ判定が生まれます。

// 透明なView
struct TransparentView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        view.backgroundColor = .clear
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        // do nothing
    }
}

struct CustomButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .background { // 拡張したいタップ領域
                TransparentView() // Coler.clearなどは実態がなくなるため代替
                    .frame(width: 300, height: 300)
            }
    }
}

var button: some View {
    Button {
        print("ボタン押したで")
    } label: {
        Text("ラベル")
    }
    .buttonStyle(CustomButtonStyle())
}

こちらは方法1と違い、Buttonのサイズを固定していないためアクセシビリティの問題をクリアしています。

問題点1

タップ領域はButtonの端から縦横に(最大)140ptほどしか拡張できません。 ここが半分と述べた理由です。

HIGによれば44pt以上がベストとのことなので、HIGのクリアという意味では問題ありませんが…

問題点2

方法1と同様に(もしくはさらに)不思議なことをしているので、将来的にバグが起きやすい可能性があります。

だじだじ

UIKitの時もそうでしたが、公式がAPIで用意して欲しいものですね。