import AppKit import Foundation /// フォルダの選択と、サンドボックス下でのアクセス権の保持を担う。 /// /// `NSOpenPanel` で選んだフォルダは配下ごとアクセスが許可される。security-scoped bookmark を /// 保存しておくと、次回起動時に同じフォルダをそのまま開き直せる。 @MainActor final class FolderAccess { private static let bookmarkKey = "lastFolderBookmark" private var accessedURL: URL? /// フォルダ選択パネルを出す。キャンセルされたら nil。 func choose(startingAt current: URL?) -> URL? { let panel = NSOpenPanel() panel.canChooseFiles = false panel.canChooseDirectories = true panel.allowsMultipleSelection = false panel.prompt = "このフォルダを調べる" panel.message = "似た写真を探すフォルダを選んでください" if let current { panel.directoryURL = current } guard panel.runModal() == .OK, let url = panel.url else { return nil } beginAccess(to: url) saveBookmark(for: url) return url } /// 前回のフォルダを復元する。無ければ nil。 func restorePrevious() -> URL? { guard let data = UserDefaults.standard.data(forKey: Self.bookmarkKey) else { return nil } var isStale = false guard let url = try? URL( resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale ) else { UserDefaults.standard.removeObject(forKey: Self.bookmarkKey) return nil } guard FileManager.default.fileExists(atPath: url.path) else { return nil } beginAccess(to: url) if isStale { saveBookmark(for: url) } return url } func releaseAccess() { accessedURL?.stopAccessingSecurityScopedResource() accessedURL = nil } // MARK: - 内部 private func beginAccess(to url: URL) { releaseAccess() if url.startAccessingSecurityScopedResource() { accessedURL = url } } private func saveBookmark(for url: URL) { guard let data = try? url.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil ) else { return } UserDefaults.standard.set(data, forKey: Self.bookmarkKey) } }