🦋

SwiftUI: iOS標準キーボードのシフトキーと同じ挙動のボタン

2023/07/14に公開

SwiftUIにある.onTapgesture()モディファイアではタップとダブルタップの複雑な機能を実装することはできなかったため、DragGestureを用いて自作しました。

iOS標準ソフトウェアキーボードの挙動

  • タップで大文字/小文字入力の切り替えができる
  • ダブルタップでCapsLock状態(大文字入力固定)にできる
  • すでにCapsLock状態でタップをすると小文字入力になる
  • すでにCapsLock状態でダブルタップをすると大文字入力になる
    (ダブルタップの1度目のタップ時は小文字入力になっている)

public enum ShiftState {
    case off
    case on
    case capsLock

    var systemName: String {
        switch self {
        case .off:
            return "shift"
        case .on:
            return "shift.fill"
        case .capsLock:
            return "capslock.fill"
        }
    }
    ・・・
}
class ShiftButtonModel: ObservableObject {
    @Binding var bindingShiftState: ShiftState
    @Published var shiftState: ShiftState {
        didSet { bindingShiftState = shiftState }
    }

    private var isTouching: Bool = false
    private var previousDate: Date?

    init(shiftState: Binding<ShiftState>) {
        _bindingShiftState = shiftState
        self.shiftState = shiftState.wrappedValue
    }

    func touchDown() {
        if isTouching { return }
        isTouching = true
        if let previousDate, -previousDate.timeIntervalSinceNow < 0.25 {
            shiftState = (shiftState == .capsLock) ? .off : .capsLock
        } else {
            shiftState = (shiftState == .off) ? .on : .off
        }
        previousDate = Date.now
    }

    func touchUp() {
        isTouching = false
    }
}
public struct ShiftButton: View {
    @StateObject var model: ShiftButtonModel

    public init(shiftState: Binding<ShiftState>) {
        _model = StateObject(wrappedValue: ShiftButtonModel(shiftState: shiftState))
    }

    public var body: some View {
        Image(systemName: model.shiftState.systemName)
            .foregroundColor(model.shiftState.foregroundColor)
            .frame(width: 32)
            .frame(minHeight: 32, maxHeight: .infinity)
            .padding(4)
            .background(model.shiftState.backgroundColor)
            .cornerRadius(8)
            .gesture(
                DragGesture(minimumDistance: 0.0, coordinateSpace: .global)
                    .onChanged { _ in
                        model.touchDown()
                    }
                    .onEnded { _ in
                        model.touchUp()
                    }
            )
    }
}

Discussion