StorageLocationManager
Manages user-selected storage location with security-scoped bookmark persistence.
Overview
Located in Minuta/Sources/Services/StorageLocationManager.swift.
This is a @MainActor ObservableObject that handles:
- User folder selection via system picker
- Security-scoped bookmark creation and restoration
- Persistence across app launches
- Fallback to default location
Properties
@Published private(set) var currentURL: URL? // Selected folder URL (nil = default)
@Published private(set) var hasSelectedLocation: Bool // Whether user has made a selection
var storageURL: URL // Resolved URL (selected or default) Key Methods
saveBookmark(for:)
Saves a security-scoped bookmark for the selected URL:
func saveBookmark(for url: URL) throws - Starts accessing security-scoped resource
- Creates bookmark with appropriate options (Mac Catalyst vs iOS)
- Stores bookmark data in UserDefaults
- Updates published properties
restoreBookmark()
Restores URL from saved bookmark:
func restoreBookmark() throws -> URL - Called on init if user has previously selected location
- Handles stale bookmarks by attempting refresh
- Starts accessing security-scoped resource
useDefaultLocation()
Uses the default ~/Documents/Minuta location:
func useDefaultLocation() - Clears stored bookmark
- Marks location as selected (to skip first-launch prompt)
resetToDefault()
Resets to show first-launch folder selection:
func resetToDefault() - Clears stored bookmark
- Marks location as not selected
isCurrentLocationAccessible()
Checks if current storage location is writable:
func isCurrentLocationAccessible() -> Bool Security-Scoped Bookmarks
Different bookmark options for Mac Catalyst vs iOS:
Mac Catalyst
url.bookmarkData(options: .withSecurityScope, ...)
URL(resolvingBookmarkData: data, options: .withSecurityScope, ...) iOS
url.bookmarkData(options: .minimalBookmark, ...)
URL(resolvingBookmarkData: data, options: [], ...) Error Handling
enum StorageLocationError: Error {
case noBookmarkFound
case bookmarkCreationFailed(Error)
case bookmarkResolutionFailed(Error)
case accessDenied
case staleBookmark
} Usage Flow
First Launch
- App checks
hasSelectedLocation - If false, shows
StorageSetupView - User picks folder or uses default
saveBookmark()oruseDefaultLocation()called
Subsequent Launches
- Init calls
restoreBookmark()ifhasSelectedLocation - If restoration fails, resets to show folder picker
storageURLproperty provides current location
Changing Folder
- User opens Settings, taps “Data Folder”
FolderPickerViewshown- On selection,
saveBookmark(for:)called AppState.reinitializeStorage()called to reload data
Integration with AppState
AppState uses StorageLocationManager’s storageURL:
let url = storageLocationManager.storageURL
storage = LocalFileStorageService(baseURL: url) Related
- 301-file-storage - File storage service
- 101-overview - Architecture overview
- 402-ios - UI (SettingsSheet, FolderPickerView, StorageSetupView)