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