🦋

SwiftUI: タップで1度、長押し中は繰り返し発火するボタン

2023/07/11に公開

キーボードのキーのように単に押した時は一度処理が走り、長押しした時はその最中繰り返し処理が走るようなボタンを実装したかったのですが、iOS 16までのSwiftUIにはそんな便利なAPIが生えていないので自作しました。(iOS 17にはbuttonRepeatBehavior()が導入されるっぽい)

要点

  • DragGestureを使う
  • ObservableObjectTimerと処理発火の管理をする
  • thresholdで長押し判定は調節できる(この例だと0.1×5=0.5秒)
class RepeatWhilePressingButtonModel: ObservableObject {
    @Published var isTouching: Bool = false

    private var timer: Timer?
    private let threshold: Int = 5
    private var counter: Int = 0

    func setup(onEvent handler: @escaping () -> Void) {
        guard timer == nil else { return }
        isTouching = true
        timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
            guard let self else { return }
            self.counter += 1
            if self.threshold < self.counter {
                handler()
            }
        }
    }

    func reset() {
        isTouching = false
        timer?.invalidate()
        timer = nil
        counter = 0
    }
}
struct RepeatWhilePressingButton: View {
    let systemName: String
    let onEventHandler: () -> Void

    @StateObject var model = RepeatWhilePressingButtonModel()

    var body: some View {
        Image(systemName: systemName)
            .frame(width: 32, height: 32)
            .padding(4)
            .background(Color.gray)
            .cornerRadius(8)
            .opacity(model.isTouching ? 0.5 : 1.0)
            .contentShape(Rectangle())
            .gesture(
                DragGesture(minimumDistance: 0.0, coordinateSpace: .global)
                    .onChanged { drag in
                        onEventHandler()
                        model.setup {
                            onEventHandler()
                        }
                    }
                    .onEnded { drag in
                        model.reset()
                    }
            )
    }
}

Discussion