2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIAdvent Calendar 2024

Day 8

SwiftUIでボタンの同時押しを制御するテクニック

Last updated at Posted at 2024-12-07

はじめに

SwiftUIで複数のボタンを並べた時に同時押しを制御して1つのタップイベントのみ受け取る方法について調べたことをまとめました。

環境

Xcode 16.1 (16B40)
Apple Swift version 6.0.2 (swiftlang-6.0.2.1.2 clang-1600.0.26.4)

1つのタップイベントのみ受け取る実装

設計の雛形をChat GPTとのやりとりを繰り返し作りました。
ボタンのタップ状態を管理するStateを追加しています。
具体的にはExclusiveButtonStateでタップしたボタンのidを保持し、idがリセットされるまで他のボタンのアクションを実行できないように制御する仕組みになります。
ExclusiveButtonと組み合わせて、この状態管理を実現しています。

final class ExclusiveButtonState: ObservableObject {
    @Published var activeButtonID: String?

    func isButtonActive(_ buttonID: String) -> Bool {
        return activeButtonID == buttonID
    }

    var canPress: Bool {
        return activeButtonID == nil
    }

    func setActiveButton(_ buttonID: String) {
        activeButtonID = buttonID
    }

    func resetActiveButton() {
        activeButtonID = nil
    }
}

struct ExclusiveButton<Content: View>: View {
    let id: String
    @ObservedObject var buttonState: ExclusiveButtonState
    let action: () -> Void
    let content: () -> Content // ボタンの外観を定義するクロージャ
    
    var body: some View {
        Button(action: {
            if buttonState.canPress {
                buttonState.setActiveButton(id)
                action()
            }
        }) {
            content() // 外部から提供されるView
        }
        .disabled(!buttonState.canPress && !buttonState.isButtonActive(id))
    }
}

ボタンを3つ並べたサンプルを作りました。
1つボタンを押すと1秒後にButtonのidのリセットを行うことで1つのアクションのみ受け付けるようにしました。

struct ExclusiveButtonView: View {
    @StateObject private var buttonState = ExclusiveButtonState()
    
    var body: some View {
        VStack(spacing: 20) {
            ExclusiveButton(id: "button1", buttonState: buttonState) {
                simulateLongTask()
            } content: {
                Text("Button 1")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            
            ExclusiveButton(id: "button2", buttonState: buttonState) {
                simulateLongTask()
            } content: {
                Text("Button 2")
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            
            ExclusiveButton(id: "button3", buttonState: buttonState) {
                simulateLongTask()
            } content: {
                Text("Button 3")
                    .padding()
                    .background(Color.red)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
        }
    }
    
    func simulateLongTask() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            buttonState.resetActiveButton()
        }
    }
}

#Preview {
    ExclusiveButtonView()
}

タップ時に1つのボタンのみハイライトさせたい

最初のコードでは、複数のボタンを同時に押すとすべてのボタンがハイライトされてしまいます。改善策として、ボタンのtouchDownタイミングでアクションを制御する必要があります。

下記の記事を参考にButtonStyleを定義し、touchDownを取得可能にします。

onTouchDownでアクションの制御をし、onTouchUpでbuttonのidをリセットし、タップを有効に戻します。
ハイライト時のUIはButtonStyleで実装します。

struct TouchTrackingButtonStyle: ButtonStyle {
    let onTouchDown: () -> Void
    let onTouchUp: () -> Void

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .onChange(of: configuration.isPressed) { isPressed in
                if isPressed {
                    onTouchDown()
                } else {
                    onTouchUp()
                }
            }
            .opacity(configuration.isPressed ? 0.6 : 1.0) // ハイライト効果として透明度を変更
            .contentShape(Rectangle())
    }
}

struct ExclusiveButton<Content: View>: View {
    let id: String
    @ObservedObject var buttonState: ExclusiveButtonState
    let action: () -> Void
    let content: () -> Content // ボタンの外観を定義するクロージャ
    
    var body: some View {
        Button {
            // 空のアクション(TouchTrackingButtonStyleで管理するため)
        } label: {
            content() // 外部から提供されるView
        }
        .buttonStyle(TouchTrackingButtonStyle(
            onTouchDown: {
                if buttonState.canPress {
                    buttonState.setActiveButton(id)
                    action() // Touch Downでのアクション
                }
            },
            onTouchUp: {
                buttonState.resetActiveButton()
            }
        ))
        .disabled(!buttonState.canPress && !buttonState.isButtonActive(id))
    }
}

ButtonStyleを拡張することでアニメーションも追加できます。

struct AnimatedTouchTrackingButtonStyle: ButtonStyle {
    let onTouchDown: () -> Void
    let onTouchUp: () -> Void
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .onChange(of: configuration.isPressed) { isPressed in
                if isPressed {
                    onTouchDown()
                } else {
                    onTouchUp()
                }
            }
            .opacity(configuration.isPressed ? 0.6 : 1.0) // ハイライト効果として透明度を変更
            .scaleEffect(configuration.isPressed ? 1.1 : 1.0) // 押下時に拡大
            .animation(.easeInOut(duration: 0.2), value: configuration.isPressed)
            .contentShape(Rectangle())
    }
}

まとめ

SwiftUIにおいて複数ボタンの中で1つのタップイベントのみを受け付ける機能を実現できました。また、タップ時に1つのボタンだけがハイライトされるよう、ButtonStyleをカスタマイズすることでUIも改善しました。

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?