0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UIKitのUIMenuを選択しても出しっぱなしにする

0
Last updated at Posted at 2026-06-28

はじめに

UIKitの UIMenu は通常、いずれかを選択するとMenuのUIが自動的に閉じます
設定メニューのように複数の項目を連続して操作したい場合は、選択しても閉じない方が便利です
AIにお願いしたところ、keepsMenuPresented のオプションがマイナーだからか迷走してたので、使い方をまとめました

output-palette.gif

SwiftUI版はこちら

使うAPI

keepsMenuPresented

UIMenuElement.Attributes の一つです

UIAction の attributes に指定すると、アクション実行後もメニューを閉じずに表示し続けます

UIDeferredMenuElement.uncached

メニューが表示されるたびに、ブロックを呼び出してメニュー要素を生成するAPIです

.uncached を使うことで、毎回フレッシュな状態のメニューを返せます

実装

セットアップ

private func setupNavigationBar() {
    navigationItem.rightBarButtonItem = UIBarButtonItem(
        image: UIImage(systemName: "gear"),
        menu: makeSettingsMenu()
    )
}

UIBarButtonItem は menu プロパティに渡すだけで、タップ時にメニューを表示できます

メニューの生成

private func makeSettingsMenu() -> UIMenu {
    let deferred = UIDeferredMenuElement.uncached { [weak self] completion in
        guard let self else {
            completion([])
            return
        }

        completion(self.buildSettingsMenuElements())
    }

    return UIMenu(
        title: "",
        children: [deferred]
    )
}

UIDeferredMenuElement.uncached でラップすることで、メニューが開かれるたびに buildSettingsMenuElements() が呼ばれ、最新状態のメニューが生成されます

メニュー要素の生成

private func buildSettingsMenuElements() -> [UIMenuElement] {
    let listAction = UIAction(
        title: "リスト",
        image: UIImage(systemName: "list.bullet"),
        attributes: .keepsMenuPresented,
        // .on を指定すると選択状態になります
        state: viewMode == .list ? .on : .off
    ) { [weak self] _ in
        self?.viewMode = .list
        self?.refreshSettingsMenu()
    }

    let gridAction = UIAction(
        title: "グリッド",
        image: UIImage(systemName: "square.grid.2x2"),
        attributes: .keepsMenuPresented,
        // .on を指定すると選択状態になります
        state: viewMode == .grid ? .on : .off
    ) { [weak self] _ in
        self?.viewMode = .grid
        self?.refreshSettingsMenu()
    }

    let viewModeMenu = UIMenu(
        title: "表示モード",
        options: [.displayInline, .singleSelection],
        children: [listAction, gridAction]
    )

    viewModeMenu.preferredElementSize = .medium

    return [viewModeMenu]
}

選択状態の更新

private func refreshSettingsMenu() {
    navigationItem.rightBarButtonItem?.menu = makeSettingsMenu()
}

状態だけ更新してもUI上の選択状態の表示は変わりません

keepsMenuPresented によってメニューは開いたまま維持されますが、UIDeferredMenuElement.uncached は「メニューを開いたタイミング」でしか再評価されないためです

メニュー表示
  ↓
uncached が評価される
  ↓
UIAction 実行
  ↓
状態変更
  ↓
(この時点では uncached は再評価されない)
  ↓
menu を再代入
  ↓
UIKit がメニュー変更を検知
  ↓
uncached が再評価される
  ↓
最新状態のメニューが表示される

barButtonItem.menu に新しいメニューを再代入することで、UIDeferredMenuElement.uncached が再評価され、最新状態の選択状態が表示されます

おわりに

keepsMenuPresented + UIDeferredMenuElement.uncached + menu の再代入を組み合わせることで、Menuを閉じずに選択状態を更新できました

今までは設定のために設定画面を作っていましたが、簡単な設定内容であればMenuで充足しそうです

pushすると画面遷移して元画面の作業のコンテキストが一度切れてしまいますし、Navigation Barのボタンを押すと下からハーフモーダルというのもパタパタしてしまいます
個人的には小さい設定であれば、Menuを活用するのが良さそうと思いました

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?