NSViewControllerでNSMenuItemから実行可能なdeleteを実装する

2023/03/11に公開

自分のツイートをより詳細化した投稿です。
https://twitter.com/yosshi_4486/status/1634138855111811073?s=20

例えば「NSTableViewで⌫(デリートキー)を押した際に行を削除したい」というのは、割と良くあるケースだと思うので、これを試してみることにします。

Storyboardを利用を選択してMac Appのプロジェクトを作成すると、テンプレートでは以下のようにアプリケーションメニューが生成されており、deleteのNSMenuItemも最初から存在しています。

Main.storyboardのScreenshot

しかし、iOSのUIResponderと違い、cut・copy・paste・deleteなどの標準アクションはNSResponderには実装されていないため、NSViewControllerでoverrideして実装というのが実現できません。どうしたらいいのか困ります。

(より詳細に言うと、UIResponderで実装してあるのはUIResponderStandardEditActionsプロトコルで定義して適合してあるメソッドですが、AppKitにはそのようなプロトコルはありませんし、NSResponderで直接実装されているということもありません。)

こちらのStackoverflowの回答を見ると「Deleteはfirst responderに接続されているから、Deleteアクションに⌫を割り当てて、ViewControllerで@IBAction func delete(_ sender: Any?)を実装すれば実現できるよ」と書いてありますが本当でしょうか?試してみます。

削除キーの割り当て

実装コードは下記のみです。

import Cocoa

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    // 追加実装したメソッド
    @IBAction func delete(_ sender: Any?) {
        print("\(#function) is called")
    }

}

実行してみると、何もないViewControllerが表示されていますが、Deleteが実行できるようになっています。
実行したアプリケーション

Deleteキーを押して実行すると、コンソールでprintが呼び出されたことを確認できます。
上記のdelete:メソッドで、NSTreeControllerやNSArrayControllerを使って行を削除する実装を行えば、冒頭の目的は果たせそうです。

コンソールの表示

まだdeleteしか試していませんが、おそらく他の標準編集アクションについても、同様の処理が実現できるでしょう。

NSMenuItemのdisable/enableをVCの状態によって切り替えたい場合

おそらく、もっとも自然な方法はお使いのViewControllerでNSMenuItemValidation.validateMenuItem(_:)を実装することです。

内部実装を推察すると、Responder Chainを辿りながらvalidateMenuItemをチェックしているのでないだろうか。と思ったので、とりあえずview.window.validateMenuItem(menuItem)を呼び出すようなことはせず、普通にfalseを返しました。また検証してみて分かったことがあれば更新します。

extension ViewController: NSMenuItemValidation {
    
    // 標準編集アクションの利用可能状態の表明
    // Responder Chainの設計にして、自分の処理が終わったら次に適合しているNSWindowに処理を回す.
    func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
        
        // このサンプルでは選択している行が空の場合はfalseを返し(disable), それ以外ではtrueを返す(enable)
        if menuItem.action == #selector(delete) {
            return !treeController.selectionIndexPaths.isEmpty
        }
        
        return false
        
    }
    
}

おわりです。

GitHubで編集を提案

Discussion