iOSのContext Menus? ほう、面白そうだな。気に入った

本投稿は TECOTEC Advent Calendar 2022 の10日目の記事です。

おはこんばんちは、証券フロンティア事業部の赤池です。
凍えそうな季節で愛をどーこー云いたい気分になったりしますが、皆様いかがお過ごしでしょうか。
私は自宅のPCの排熱がかなりあるせいで、部屋の中が温かい、というか暑いくらいです。

というわけで今回はiOSのContext Menusについて書いていこうかなと思います。
Context Menusってなんぞやって方のために参考画像を。こんなやつです。

それっぽいメニュー

ボタンを押すと、ポップオーバーみたいな感じで出てくるメニューですね。
今回はこのContext Menusについて、簡単な使い方を紹介していこうかなと思います。

動作環境はXcode 14.1(14B47b)、使っているシミュレータはiPhone 14 Pro Max(iOS16.1)です

動機

まず、なんで今さらContext Menusについて書くかというと、実装中のプロジェクトのiOS12サポートが終わって、iOS13以降限定の機能がようやく使えるようになったというのが1点。
プロジェクトの新機能でこんなの使えるんじゃね、ということで試しにやってみようとおもったというのがもう1点。
完全にノリでやってみようと思った次第ですね、はい。

Context Menusの実装方法

では早速、Context Menusの実装について説明しましょう。この方法が一番簡単だと思います、ということでまずはボタンをタップしたらUIContextMenuが表示されるというシンプルなやつです。
とりあえずプロジェクト作って、最初からあるViewControllerの真ん中にUIButtonがあるという設定です。

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupButton()
    }
    
    private func setupButton() {
        let saladMenu = UIAction(title: "シェフの気まぐれサラダ", image: UIImage(systemName: "carrot.fill")) { (action) in
            print("シェフの気まぐれサラダ")
        }
        let pastaMenu = UIAction(title: "イタズラ妖精のきのこパスタ", image: UIImage(systemName: "leaf.fill")) { (action) in
            print("イタズラ妖精のきのこパスタ")
        }
        let menu = UIMenu(title: "本日のメニュー", image: nil, identifier: nil, options: .displayInline, children: [saladMenu, pastaMenu])
        button.menu = menu
        button.showsMenuAsPrimaryAction = true
    }
}

こんな感じで書いてアプリを実行してボタンを押すと、こんな感じになります。

あれれ〜、おっかし〜ぞ。上の画像と違うね〜
画像のキャプションにも書きましたが、最初に見せた画像と微妙に違いますね。
なぜかというと、これはiOS14以降で使える、UIButton用のUIMenu追加ロジックを使っているからですね。

今から新しいアプリを作る分にはこれで問題ないですが、私が関わっているプロジェクトのようにiOS13も対応していると話が違います。
ではその場合、どうするのか? 答えはUIContextMenuInteractionDelegateを使った実装をします。

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!
    
    private var index = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupButton()
    }
    
    private func setupButton() {
        let interaction = UIContextMenuInteraction(delegate: self)
        button.addInteraction(interaction)
    }
    
    @IBAction func buttonAction(_ sender: Any) {
        print("\(index)")
        index += 1
    }
}

extension ViewController: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        
        let actionProvider: ([UIMenuElement]) -> UIMenu? = { _ in
            
            let saladMenu = UIAction(title: "シェフの気まぐれサラダ", image: UIImage(systemName: "carrot.fill")) { (action) in
                    print("シェフの気まぐれサラダ")
                }
            let pastaMenu = UIAction(title: "イタズラ妖精のきのこパスタ", image: UIImage(systemName: "leaf.fill")) { (action) in
                print("イタズラ妖精のきのこパスタ")
            }
            let menu = UIMenu(title: "本日のメニュー", image: nil, identifier: nil, children: [saladMenu, pastaMenu])
            
            return menu
        }
        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider)
    }
}

このコードでアプリを実行して、ボタンを長押しすると最初の画像のような感じになると思います。
短いタップだとIBActionの中が実行されるので、普通のタップはIBAction、ロングタップはContext Menu表示、みたいな場合分けも可能になります。

※ちなみにシステムアイコンの一覧は、↓の公式アプリをインストールすると確認することが出来ます developer.apple.com

ちょっとしたカスタマイズ

Context Menusの中に、更にContext Menusを実装することも出来ます。↑のコードのextension部分をこんな感じに書き換えてみます。

extension ViewController: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        
        llet actionProvider: ([UIMenuElement]) -> UIMenu? = { _ in
            
            let saladMenu = UIAction(title: "シェフの気まぐれサラダ", image: UIImage(systemName: "carrot.fill")) { (action) in
                    print("シェフの気まぐれサラダ")
                }
            
            let mainMenu: UIMenu = {
                let pastaMenu = UIAction(title: "イタズラ妖精のきのこパスタ", image: UIImage(systemName: "leaf.fill")) { (action) in
                    print("イタズラ妖精のきのこパスタ")
                }
                
                let fishMenu = UIAction(title: "サーモンと帆立貝柱のキャベツ包み蒸し", image: UIImage(systemName: "fish.fill")) { (action) in
                    print("サーモンと帆立貝柱のキャベツ包み蒸し")
                }
                
                let menu = UIMenu(title: "メインメニュー", image: UIImage(systemName: "list.bullet.clipboard.fill"), identifier: nil, children: [pastaMenu, fishMenu])
                return menu
            }()
            
            let menu = UIMenu(title: "本日のメニュー", image: nil, identifier: nil, children: [saladMenu, mainMenu])
            
            return menu
        }
        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider)
    }
}

これを実行すると、まずは↓のように表示されます。

更にメインメニューを開いてみると

こんなふうに表示されました。
こういった実装も簡単にできるので、ついつい調子に乗って何個も入れ子にしたくなっちゃいますが、Context MenusのHuman Interface Guideの中に、「サブメニューを使うときは一階層にとどめてね♡(意訳)」とあるので、一階層だけにしておきましょう。

欠点

割と便利なContext Menusですが、欠点ももちろんあります。
それは、CustomViewを使えないこと。
もちろん、普通に使う分にはこれで問題ないわけなので、拡張性を無くすというのも解りますが、もうちょっと手を加えさせてほしいですね。
なので見た目をもっと変えたいという場合はちょっと違いますが、UIPopoverPresentationControllerを使いましょう。

おわりに

ということで今回はiOSのContext Menusについて簡単に紹介してきました。
こういったちょっと前までは作るのが面倒だったものも、最近はApple側が用意してくれるようになったのでだいぶ楽になりましたね。
今後もこんな感じにどんどん、簡単なものを提供してもらいたいと思いつつ、カスタムの余地を残してくれると嬉しいです。
ではまたどこかでお会いしましょうノシ

www.tecotec.co.jp