Closed3
SwiftUI メニューバーmacOSアプリで特定ディレクトリのファイル一覧を表示したい

fileImporter
👆 こちらにfileImporterを使う様なことが書かれている。fileImporterとは?
ExampleにもDirectoryを選択するやり方が書かれていそう。
シンプルな実装例:
import SwiftUI
struct ContentView: View {
@State private var showFileImporter = false
var body: some View {
Button("Open") {
showFileImporter = true
}
.fileImporter(
isPresented: $showFileImporter,
allowedContentTypes: [.directory]
) { result in
switch result {
case .success(let directory):
print(directory)
case .failure(let error):
print(error)
}
}
}
}
directory
は URL
で返ってくる。
ただ、これだと MenuBarExtra で実行しても選択ダイアログが表示されなかった。

LLMに聞いてみる
質問と回答
- 質問
MenuBarExtra を使って SwiftUIでmacOSアプリを作成しています。
メニューの中にある特定のディレクトリのファイルやディレクトリ一覧を表示したいと思っているんですが、どんな方法がありますか?
- 回答
以下は MenuBarExtra を使って「特定ディレクトリの内容をメニューに動的表示する」ための代表的なアプローチと実装のポイントです。MenuBarExtra は menu スタイルなら純粋なプルダウンメニュー、window スタイルなら自由レイアウトのポップオーバーを使い分けられるので、項目数や UI の複雑さによって設計を切り替えるのがコツです。
1. 基本構成
1-1 Scene 宣言
@main
struct DirListerApp: App {
var body: some Scene {
// パスは後述の FileImporter などで動的に取得してもよい
MenuBarExtra {
DirectoryMenu(path: URL(fileURLWithPath: "/Users/youruser/Documents"))
} label: {
Label("Docs", systemImage: "folder.fill")
}
// ▼必要に応じて .window に切り替える
.menuBarExtraStyle(.menu)
}
}
-
MenuBarExtra
は macOS 13 以降の専用シーン([developer.apple.com][5]) -
menuBarExtraStyle(.menu / .window)
でレイアウトが変わる。List
やScrollView
を使いたい場合は.window
が必須。
1-2 ディレクトリ読み込みコンポーネント
struct DirectoryMenu: View {
let path: URL
@State private var contents: [URL] = []
var body: some View {
// メニュー形式の場合は Button 群、window 形式の場合は List / ScrollView へ差し替え
Group {
if contents.isEmpty { Text("No items") }
else {
ForEach(contents, id: \.self) { url in
Button(url.lastPathComponent) {
NSWorkspace.shared.open(url) // クリックで Finder を開く例
}
}
}
}
.task { // 非同期でも UI が固まらない
contents = load()
}
}
private func load() -> [URL] {
let fm = FileManager.default
guard let enumr = fm.enumerator(
at: path,
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles,
.skipsPackageDescendants,
.skipsSubdirectoryDescendants] // 上位階層だけ shallow に走査
) else { return [] }
return enumr
.compactMap { $0 as? URL }
.sorted { $0.lastPathComponent.localizedCompare($1.lastPathComponent) == .orderedAscending }
}
}
-
FileManager.enumerator(at:includingPropertiesForKeys:options:)
を shallow に使えば巨大フォルダでも数 ms で走査できる。 - 旧式の
enumerator(atPath:)
を深掘りで回すと CPU と I/O が跳ね上がるため.skipsSubdirectoryDescendants
は必須。
2. UI スタイル別の実装戦略
スタイル | 想定件数 | 実装例 | 注意点 |
---|---|---|---|
.menu (既定) |
10〜数十件 |
Button / Divider を直列で |
メニュー幅・高さが OS に制限される。長いタイトルは自動省略。 |
.window |
数百件以上や複雑 UI |
List , ScrollView , 検索バー, アイコン付き Row など自由 |
List を置くとスクロール可能。frame(width:height:) でウィンドウサイズを固定可([nilcoalescing.com][2])。 |
3. パフォーマンスとリアクティブ更新
-
バックグラウンド読み込み
Task.detached {}
で I/O を分離する、または@MainActor
を外して load すると UI ブロッキングを防げる。 -
ライブ更新
DispatchSourceFileSystemObject
やFSEvents
をAsyncStream
でラップしてディレクトリ変更を監視すると、ファイル追加・削除が即反映できる。 -
巨大フォルダ/ネットワークボリューム
Spotlight のNSMetadataQuery
検索や非同期 chunk 読み込みで逐次 append し、先に UI を表示しておく方式も有効。
4. サンドボックスと権限
- Mac App Store 提出や Hardened Runtime 有効化時は App Sandbox がかかる。固定パスを読む場合は com.apple.security.files.read-only などの entitlement が必要。
- ユーザにディレクトリを選ばせる場合は
@Environment(\.openURL)
+FileImporter
でセキュリティスコープ付きブックマークを保存し、次回以降自動マウントすると安全。
5. さらに便利にするアイディア
-
検索フィールド:
.window
スタイルでTextField
を置き、@Published var query
で動的フィルタ。 -
サブメニュー:ファイル種別ごとに
Menu
をネストし、階層を浅く保つ。 -
ドラッグ & ドロップ:
onDrag
/onDrop
を追加すれば Finder へ直接ドラッグできる。 -
Dock・アプリスイッチャ非表示:Info.plist に
LSUIElement
= YES を設定して完全メニューバー常駐型にする。
まとめ
-
小規模リストなら
MenuBarExtra
+.menu
とButton
群が最短実装。 -
大量アイテムやカスタム UI なら
.window
に切り替えてList
/ScrollView
を使う。 -
ディスク I/O は shallow + バックグラウンドで。
FileManager
オプションで深掘りを避けるだけで体感数百倍速い。 - Sandbox 権限とライブ更新 を組み合わせると、ユーザ操作なしでも安全かつリアルタイムに同期できる。
これらの組み合わせで、大量のファイルを扱う高度なメニューバーアプリから、シンプルなユーティリティまで柔軟に構築できます。
-
パスは後述の FileImporter などで動的に取得してもよい
とあるが、何か方法があるのか?- やっぱりできなさそうで、隠しWindowGroupを用意するか、NSOpenPanelを直接呼ぶなどの別のアプローチが必要になるらしい
- 一旦ディレクトリは固定で進める
- ファイル一覧の読み込みは
FileManager.default
を使えばいける?

FileManager使って一覧取得
まず一覧表示するために、権限が必要そう。XCodeで「Target」>「Signing & Capabilities」>「App Sandbox」の「File Access Type」を見てもユーザー選択したものか、特定の「XXX Folder」の権限しか編集できなさそう。
一度ユーザーに特定のディレクトリを選択してもらい、権限を保存しておく必要がありそう。
👇雑なサンプル
if let data = UserDefaults.standard.data(forKey: "UserSelectedBookmark") {
// 既に保存済みの場合
var stale = false
if let scopedURL = try? URL(resolvingBookmarkData: data,
options: [.withSecurityScope],
bookmarkDataIsStale: &stale),
scopedURL.startAccessingSecurityScopedResource() {
// ここで権限を使ってやりたいことをやる
scopedURL.stopAccessingSecurityScopedResource()
}
} else {
// ユーザーにディレクトリを選択してもらい、権限を保存する
let panel = NSOpenPanel()
panel.directoryURL = URL(fileURLWithPath: "....") // 初期表示
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.begin { result in
guard result == .OK, let url = panel.url else { return }
let bookmark = try? url.bookmarkData(options: .withSecurityScope,
includingResourceValuesForKeys: nil,
relativeTo: nil)
UserDefaults.standard.set(bookmark, forKey: "UserSelectedBookmark")
}
}
ファイル or ディレクトリ一覧取得
private func load(path: URL) -> [URL] {
return try! FileManager.default.contentsOfDirectory(
at: path,
includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey],
options: [.skipsHiddenFiles])
}
このスクラップは2ヶ月前にクローズされました