[SwiftUI] 操作中でも shift キーで動作の変わる マウス操作の実装方法

SwiftUI

macOS アプリで キーボードとの組み合わて動くマウス操作の作り方を説明します

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.2.2
  • Xcode 12.4

Gesture 途中でも、キーボードの状態で動作を切り替えたい

前回の記事で、Gesture 開始時の条件で、動作する Gesture を切り替える方法を説明しました。

SwiftUI[SwiftUI] shift キーで動作の変わる マウス操作

しかし、Gesture 開始後に、動作を切り替えられないのは少しストレスです。

今回は、動作中でも振る舞いを切り替えられるような実装を説明します。

例題は、前回と同じです。ドラッグすると自由にテキストを移動させられますが、Shift キーを押されると 100px 単位のグリッドに沿っての移動に切り替わります。

AppKit での shift キーをチェックする方法

AppKit では NSEvent を使うことで、Shift キーの状態変化をチェックできました。

example

// (1)
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { (event) -> NSEvent? in
    // (2)
    if event.modifierFlags.contains(.shift) {
        // shift key is pressed 
    } else {
        // shift key is released 
    }
    // (3) 
    return nil
}
コード解説
  1. 自分のアプリケーション内での検知であれば、addLocalMonitorForEvents を使って、モニターすることができます。.flagsChanged を指定することで、Shift キーの状態変化のみをモニターできます
  2. 渡されてくる event が shift キーを含んでいるかどうかの判断ができるので、必要な処理を行わせることができます
  3. イベントをフォワードする必要があれば、event を返します。必要に応じて修正した event を返すこともできます。

SwiftUI で 動作中にキーボードの状態を検知して振る舞いを変える仕組みを探してみたのですが、見つかりませんでした。

AppKit の方法と組み合わせて実装していくことにします。

Gesture の中で振る舞いを切り替える

SwiftUI 的に、動作中の Gesture を切り替えることはできませんので、1つの Gesture の中で、shift キーの状態に応じて動作を変える必要があります。

内部に、withShift というフラグを用意し、そのフラグの状態に応じて計算式を切り替えます。

Gesture を抜粋

.gesture(DragGesture()
    .onChanged { gesture in
        if self.isDragging == false {
            self.dragStartPosition = CGPoint(x: self.textOffset.width, y: self.textOffset.height)
            self.isDragging = true
        }
        let newOffset =  CGSize(width: gesture.translation.width + self.dragStartPosition.x,
                                 height: gesture.translation.height + self.dragStartPosition.y)
        // (1)
        self.textOffset = withShift ? CGSize(width: CGFloat(Int(newOffset.width/100) * 100), height: CGFloat(Int(newOffset.height/100)*100)) : newOffset
    }
    .onEnded { gesture in
        self.isDragging = false
    })
コード解説
  1. withShift というフラグの状態で、計算式を切り替えています。Shift 押下:withShift = true を想定してます。

キーの押下を Gesture に伝える

キーの押下自体は、NSEvent で検知できますが、検知したものを SwiftUI/Gesture へ知らせる必要があります。

今回は、NSEvent を Notification に変換し、NotificationCenter経由で Publisher に変換したものを onReceive で受け取るという方向で実装しました。

以下のステップが必要となります。

  1. アプリ起動時に、NSEvent をモニタして、変更発生時に NotificationCenter へ post するようにします。
  2. SwiftUI のビューが NotificationCenter からの通知を publisher 経由で受け取り、Gesture の振る舞いを変更します。

順番に見ていきます。

NSEvent のモニタリング

アプリの起動タイミング等で、以下のように NSEvent を監視し、NotificationCenter 経由で notification を行うようにします。

NSEvent 監視

public let shiftIsChanged = NSNotification.Name("ShiftIsChanged")

@main
struct DragExampleApp: App {
    init() {
        NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { (event) -> NSEvent? in
            if event.modifierFlags.contains(.shift) {
                NotificationCenter.default.post(name: shiftIsChanged, object: true)
            } else {
                NotificationCenter.default.post(name: shiftIsChanged, object: false)
            }
            return nil
        }
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

上記では、App の init で行なっていますが、適切な箇所で行う必要があります。

NotificationCenter への post についても、userInfo を使って複雑な情報を送ることもできますが、シンプルに shift の ON/OFF のみを通知するようにしています。

注意
NSEvent の monitor 次第では、全てのイベントを受け取って消費することもできてしまうので、注意が必要です。

NotificationCenter を subscribe する

onReceive を使うことで NotificationCenter を Publisher として、subscribe することは簡単ですので、以下のように、適切な View の .onReceive で受け取ることで、ロジック切り替えのフラグを操作することができます。

example

struct ContentView: View {
    @State private var textOffset: CGSize = CGSize.zero
    @State private var dragStartPosition: CGPoint = CGPoint.zero
    @State private var isDragging:Bool = false
    @State private var withShift:Bool = false

    var body: some View {
        ZStack {
            Color.white.frame(width: 800, height: 800)
            Text("Hello, World!")
                .background(Color.white)
                .offset(textOffset)
                .gesture(DragGesture()
                    .onChanged { gesture in
                        if self.isDragging == false {
                            self.dragStartPosition = CGPoint(x: self.textOffset.width, y: self.textOffset.height)
                            self.isDragging = true
                        }
                        let newOffset =  CGSize(width: gesture.translation.width + self.dragStartPosition.x,
                                                 height: gesture.translation.height + self.dragStartPosition.y)
                        self.textOffset = withShift ? CGSize(width: CGFloat(Int(newOffset.width/100) * 100), height: CGFloat(Int(newOffset.height/100)*100)) : newOffset
                    }
                    .onEnded { gesture in
                        self.isDragging = false
                    })
                .onReceive(NotificationCenter.default.publisher(for: shiftIsChanged)) { notif in
                    guard let shiftIsOn = notif.object as? Bool else { return }
                    self.withShift = shiftIsOn
                }
        }
    }
}

完成した 「押下キーが変わると 途中で振る舞いの変わる Gesture」

以下のような振る舞いの Gesture となります。

まとめ:操作中でも修飾キーに応じて動作の変わる Gesture の実装

操作中でも修飾キーに応じて動作の変わる Gesture の実装
  • NSEvent を monitor して、変更を検知する
  • NotificationCenter 経由で subscribe して、Gesture で変更を検知できるようにする
  • NSEvent の monitor 方法によっては、全てのイベントを消費してしまうことも可能なため、注意が必要です。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です