macOSアプリが使えるフォルダは大きく5種類あります。
- アプリバンドル内(読み取り専用)
- Application Support(永続データ)
- Caches(キャッシュ・一時的)
- tmp(一時ファイル・超短命)
- ユーザーが明示的に選択したフォルダ(Security-Scoped)
キャンバスに送る画像・矢印アイコン・素材フォルダは
👉 ⑤ + 必要なら② がベストです。
① アプリバンドル内(Bundle)
|
1 2 3 |
YourApp.app/ └ Contents/ └ Resources/ |
特徴
- ❌ 書き込み不可
- ⭕ 読み取り専用
- アプリに同梱する初期素材向け
向いている用途
- デフォルト矢印
- 初期スタンプ
- UI用アイコン
② Application Support(最重要)
|
1 |
~/Library/Containers/YourApp/Data/Library/Application Support/YourApp/ |
特徴
- ⭕ 永続保存
- ⭕ サンドボックスでも自由
- ❌ Finderからは見えにくい
向いている用途
- ユーザー素材
- スクショ履歴
- 保存したキャンバス画像
- あなたの「ScreenshotSidebarView」の裏側保存先
取得方法
|
1 2 3 4 |
let url = FileManager.default.urls( for: .applicationSupportDirectory, in: .userDomainMask ).first! |
さらに
|
1 |
let appFolder = url.appendingPathComponent("YourApp", isDirectory: true) |
👉
「アプリが管理するフォルダ」= ここ
③ Caches(キャッシュ)
|
1 |
~/Library/Caches/YourApp/ |
特徴
- ⭕ 書き込み可
- ❌ OSが勝手に消す
- 再生成できるもの専用
向いている用途
- OCRの中間画像
- 一時レンダリング結果
- プレビュー用画像
👉
「消えても困らない」ものだけ
④ tmp(一時ファイル)
|
1 |
/var/folders/xxxx/... |
特徴
- ⏱ アプリ終了やOS都合で消える
- Finder不可視
向いている用途
- テキストエディタ保存時の一時ファイル
- 書き出し直前の中間データ
👉
あなたが見かけた
テキストエディタ保存の際、一時ファイル
= ほぼ確実にここ
|
1 |
FileManager.default.temporaryDirectory |
⑤ ユーザーが選択したフォルダ(重要)
NSOpenPanel / Folder Picker
|
1 2 |
panel.canChooseDirectories = true panel.canChooseFiles = false |
特徴
- ⭕ Finder上の任意フォルダ
- ⭕ 画像素材フォルダに最適
- ⚠️ Security-Scoped Bookmark 必須
向いている用途
- 「素材フォルダA / B 切り替え」
- ユーザー管理の画像集
- 矢印・記号・スクショ置き場
Application Support フォルダへの自動保存
Sidebarにスクリーンショットのデータを並べてる例
フォルダを作る基本コード
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct AppDirectories { static let appSupport: URL = { let base = FileManager.default.urls( for: .applicationSupportDirectory, in: .userDomainMask ).first! let dir = base.appendingPathComponent("YourApp", isDirectory: true) if !FileManager.default.fileExists(atPath: dir.path) { try? FileManager.default.createDirectory( at: dir, withIntermediateDirectories: true ) } return dir }() } |
"YourApp" はアプリ名と合わせるべき?
Bundle Identifier を使う(最強)
理由
- アプリ名変更に強い
- 同名アプリと衝突しない
- App Store 審査でも好印象
|
1 2 |
Application Support/ └ com.yourcompany.yourapp/ |
👉 これが一番「macOSっぽい」
フォルダ名おすすめ度
| 方法 | おすすめ度 |
|---|---|
| Bundle Identifier | ⭐⭐⭐⭐⭐ |
| CFBundleName | ⭐⭐⭐⭐ |
| 固定文字列 | ⭐ |
用途別にファイル分け:
|
1 2 3 4 5 6 7 8 9 |
extension AppDirectories { static var screenshots: URL { let dir = appSupport.appendingPathComponent("Screenshots", isDirectory: true) if !FileManager.default.fileExists(atPath: dir.path) { try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) } return dir } } |
例:スクリーンショットを「撮った瞬間」に自動保存
|
1 2 |
self.SS_images.append(ScreenshotItem(cg_image: image)) self.capture_image = image |
ここに 1行足すだけで自動保存になります。
保存例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func saveScreenshot(_ image: CGImage) { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" let filename = formatter.string(from: Date()) + ".png" let url = AppDirectories.screenshots.appendingPathComponent(filename) let nsImage = NSImage(cgImage: image, size: .zero) let rep = NSBitmapImageRep(data: nsImage.tiffRepresentation!) let data = rep?.representation(using: .png, properties: [:]) try? data?.write(to: url) } |
|
1 |
saveScreenshot(image) |
👉
Sidebarを閉じても、アプリ再起動しても復活可能
アプリ起動時に Sidebar を復元する
起動時ロード
|
1 2 3 4 5 6 7 8 9 10 11 12 |
func loadSavedScreenshots() -> [ScreenshotItem] { let urls = (try? FileManager.default.contentsOfDirectory( at: AppDirectories.screenshots, includingPropertiesForKeys: nil )) ?? [] return urls.compactMap { url in guard let img = NSImage(contentsOf: url), let cg = img.cgImage else { return nil } return ScreenshotItem(cg_image: cg) } } |
|
1 |
@State private var SS_images: [ScreenshotItem] = loadSavedScreenshots() |
👉
これだけで「常に履歴が残るアプリ」になる
「削除バージョン」
ここは 2パターンあります。
パターンA:URLが分かっている場合(最も安全)
保存時にURLも管理する設計
|
1 2 3 4 5 |
struct ScreenshotItem: Identifiable, Hashable { let id = UUID() let cg_image: CGImage let fileURL: URL } |
保存
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func saveScreenshot(_ image: CGImage) -> URL { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" let filename = formatter.string(from: Date()) + ".png" let url = AppDirectories.screenshots.appendingPathComponent(filename) let nsImage = NSImage(cgImage: image, size: .zero) let rep = NSBitmapImageRep(data: nsImage.tiffRepresentation!) let data = rep?.representation(using: .png, properties: [:]) try? data?.write(to: url) return url } |
削除
|
1 2 3 |
func deleteScreenshot(at url: URL) { try? FileManager.default.removeItem(at: url) } |
👉
Sidebarの削除 = ファイル削除
という綺麗な設計になります。
パターンB:画像から逆算する(非推奨)
|
1 2 3 4 |
func deleteScreenshot(by filename: String) { let url = AppDirectories.screenshots.appendingPathComponent(filename) try? FileManager.default.removeItem(at: url) } |
⚠️
- ファイル名の管理が地獄
- 将来拡張しづらい
推奨(永続削除)
|
1 2 3 4 5 6 |
for item in SS_images where selected_item.contains(item.id) { deleteScreenshot(at: item.fileURL) } SS_images.removeAll { selected_item.contains($0.id) } selected_item = [] |
|
1 2 3 4 5 |
struct ScreenshotItem: Identifiable, Hashable { let id = UUID() let cg_image: CGImage let fileURL: URL } |
↑にした時のロード関数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func loadSavedScreenshots() -> [ScreenshotItem] { let dir = AppDirectories.screenshots let urls = (try? FileManager.default.contentsOfDirectory( at: dir, includingPropertiesForKeys: [.creationDateKey], options: [.skipsHiddenFiles] )) ?? [] return urls .filter { $0.pathExtension.lowercased() == "png" } .compactMap { url in guard let nsImage = NSImage(contentsOf: url), let cgImage = nsImage.cgImage else { return nil } return ScreenshotItem( cg_image: cgImage, fileURL: url ) } } |
並び順を「撮影順」にしたい場合
macOS は ディレクトリ列挙順を保証しません。
スクショ履歴アプリなら、時系列順が自然です。
creationDate でソート
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func loadSavedScreenshots() -> [ScreenshotItem] { let dir = AppDirectories.screenshots let urls = (try? FileManager.default.contentsOfDirectory( at: dir, includingPropertiesForKeys: [.creationDateKey], options: [.skipsHiddenFiles] )) ?? [] let sorted = urls.sorted { let a = try? $0.resourceValues(forKeys: [.creationDateKey]).creationDate let b = try? $1.resourceValues(forKeys: [.creationDateKey]).creationDate return (a ?? .distantPast) < (b ?? .distantPast) } return sorted.compactMap { url in guard let nsImage = NSImage(contentsOf: url), let cgImage = nsImage.cgImage else { return nil } return ScreenshotItem(cg_image: cgImage, fileURL: url) } } |
アプリ起動時の使い方
|
1 |
@State private var SS_images: [ScreenshotItem] = [] |
|
1 2 3 |
.onAppear { SS_images = loadSavedScreenshots() } |
保存 → Sidebar → 削除の流れ(完成形)
保存
|
1 2 3 4 |
let url = saveScreenshot(image) SS_images.append( ScreenshotItem(cg_image: image, fileURL: url) ) |
削除
|
1 2 3 |
for item in SS_images where selected_item.contains(item.id) { try? FileManager.default.removeItem(at: item.fileURL) } |
同梱ファイル・フォルダのやり方
❌ リリース時に同梱する素材を
Application Support に「最初から置く」のは NG✅ Bundle(アプリ内)に同梱し、
必要なら初回起動時に Application Support にコピーする
これが macOSアプリの正解ルートです。
なぜ Application Support に直接入れないのか
理由①
Application Support はインストール時には存在しない
- App Store / dmg 配布時
- そのフォルダは 初回起動で初めて作られる
- 同梱不可能
理由②
アプリ更新時に消える可能性がある
- バージョンアップ
- クリーンインストール
- Sandboxing の再構成
👉
初期素材は「配布物に含める」必要がある
正しい構成(これが王道)
1️⃣ アプリ Bundle に素材を入れる
|
1 2 3 4 5 6 7 |
YourApp.app └ Contents └ Resources └ Materials ├ arrow.png ├ highlight.png └ note.png |
Xcode
👉 Copy Bundle Resources に追加
2️⃣ 初回起動時だけ Application Support にコピー
判定フラグ
|
1 2 3 |
func isFirstLaunch() -> Bool { !UserDefaults.standard.bool(forKey: "didCopyDefaultMaterials") } |
コピー処理
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func copyDefaultMaterialsIfNeeded() { guard isFirstLaunch() else { return } guard let bundleURL = Bundle.main.resourceURL? .appendingPathComponent("Materials") else { return } let destURL = AutoSaveManager.materials let files = (try? FileManager.default.contentsOfDirectory( at: bundleURL, includingPropertiesForKeys: nil )) ?? [] for file in files { let target = destURL.appendingPathComponent(file.lastPathComponent) if !FileManager.default.fileExists(atPath: target.path) { try? FileManager.default.copyItem(at: file, to: target) } } UserDefaults.standard.set(true, forKey: "didCopyDefaultMaterials") } |
👉
これが「初期素材のインストール処理」
3️⃣ アプリ起動時に必ず呼ぶ
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@main struct YourApp: App { init() { copyDefaultMaterialsIfNeeded() } var body: some Scene { WindowGroup { ContentView() } } } |
この設計のメリット(大事)
✅ Bundle
- 読み取り専用
- 配布保証
- App Store 審査OK
✅ Application Support
- ユーザー編集可能
- 追加・削除・上書き自由
- 将来の素材アップデートに対応
👉
「初期素材はアプリのもの」
「編集後はユーザーのもの」
コメント