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

  1. App checks hasSelectedLocation
  2. If false, shows StorageSetupView
  3. User picks folder or uses default
  4. saveBookmark() or useDefaultLocation() called

Subsequent Launches

  1. Init calls restoreBookmark() if hasSelectedLocation
  2. If restoration fails, resets to show folder picker
  3. storageURL property provides current location

Changing Folder

  1. User opens Settings, taps “Data Folder”
  2. FolderPickerView shown
  3. On selection, saveBookmark(for:) called
  4. AppState.reinitializeStorage() called to reload data

Integration with AppState

AppState uses StorageLocationManager’s storageURL:

let url = storageLocationManager.storageURL
storage = LocalFileStorageService(baseURL: url)

Related