SwiftUIで Listの同じ行に複数のButtonを置くと両方とも反応してしまい個別にタップできない

SwiftUIで List の同じ行に複数の Button を置くと、すべてのButtonのアクションイベントが発生してしまい、それぞれのボタンを個別にタップできない。

本記事では、個別にタップする方法と、よりボタンらしく見せるために ButtonSyle を利用する方法を紹介する。

f:id:ch3cooh393:20200730212356p:plain

実行環境

  • Xcode 11.6
  • iOS 13.6

問題の挙動

SwiftUIで List の同じ行に複数の Button を置くと、すべてのButtonのアクションイベントが発生してしまい、それぞれのボタンを個別にタップできない。

UIKitでのUITableViewのような画面をSwiftUIで作る場合には List を使う。

import SwiftUI

struct SampleButtonOnListView : View {

    var body: some View {
        List {
            Section {
                Button(action: {
                    print("button 1")
                }, label: { Text("button 1") })
                
                HStack {
                    Button(action: {
                        print("button 3-1")
                    }, label: { Text("button 3-1") })
                    Button(action: {
                        print("button 3-2")
                    }, label: { Text("button 3-2") })
                }
            }
        }
    }
}

実行すると下図のようなUIで表示される。下図では2行目の「button 3-2」のボタンを押しているが、「button 3-1」と「button 3-2」のアクションが両方とも発火している。

f:id:ch3cooh393:20200730203858p:plain

ログには下記のように出力されており、それぞれのButtonのアクションを実行することができない。

button 3-1
button 3-2

List の背景をアクティブにせず、ボタンを個別にポチポチする方法はあるのか?

解決編

簡単な方法としてはButtonを使わないようにする。Text に対してonTapGestureを使うことで、見た目そのままで個別にタップできるようになる。

struct SampleButtonOnListView : View {

    var body: some View {
        List {
            Section {
                Button(action: {
                    print("button 1")
                }, label: { Text("button 1") })

                HStack {
                    Text("button 3-1")
                        .onTapGesture {
                            print("button 3-1")
                        }

                    Text("button 3-2")
                        .onTapGesture {
                            print("button 3-2")
                        }
                }
            }
        }
    }
}

タッチイベントを Text に取られるので、行が反応して背景がハイライト色になることもなくなる。

f:id:ch3cooh393:20200730210346p:plain

しかしこれはあくまでも Text なのでボタンを押した時にハイライト色に変化したりはしない。これでは List の上に Button を置いているとはとても言えない。

List上にButtonを置いて、押されている時にはきちんとハイライト色に変える

やや冗長ではあるが、Buttonに対してonTapGesture Modifierを設定して、ボタンスタイルを適用する。

Button(action: {
    print("button 3-1")
}, label: { Text("button 3-1") })
    .onTapGesture {
    }
    .buttonStyle(MyButtonStyle())

全体としては下記のように変更した。

import SwiftUI

struct SampleButtonOnListView : View {

    var body: some View {
        List {
            Section {
                Button(action: {
                    print("button 1")
                }, label: { Text("button 1") })

                HStack {
                    Button(action: {
                        print("button 3-1")
                    }, label: { Text("button 3-1") })
                        .onTapGesture {
                        }
                        .buttonStyle(MyButtonStyle())
                    
                    Button(action: {
                        print("button 3-2")
                    }, label: { Text("button 3-2") })
                        .onTapGesture {
                        }
                        .buttonStyle(MyButtonStyle())
                }
            }
        }
    }
}

実行すると下図のように青い角丸ボタンが表示される。「button 3-1」を押している間、背景の青色がやや半透明になる。

f:id:ch3cooh393:20200730210357p:plain

MyButtonStyleでは以下のように指定している。configuration.isPressed で押しているかどうかを取得することができるので適切な色に変えればよい。

struct MyButtonStyle: ButtonStyle {

  func makeBody(configuration: Self.Configuration) -> some View {
    
    configuration.label
        .font(.system(size: 17))
        .padding()
        .background(configuration.isPressed ? Color.blue.opacity(0.5) : Color.blue)
        .foregroundColor(configuration.isPressed ? Color.white.opacity(0.5) : Color.white)
        .cornerRadius(16.0)
  }
}

ButtonStyleを使う方法についてはまた後日紹介したい。