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)

Default Location

storageURL falls back to <Documents>/Minuta when no folder is selected. What <Documents> means differs per platform:

  • Mac Catalyst (Homebrew build, not sandboxed): the real ~/Documents, so the folder sits next to the user’s other documents and is picked up by iCloud Drive “Desktop & Documents” sync if that is on.

  • Mac App Store build (sandboxed): the container’s Documents, invisible in Finder. Planned, see 917-plan-mac-testflight.

  • iOS: the app container’s Documents. It is exposed to the user through two Info.plist keys (Minuta/Sources/Info.plist):

    • UIFileSharingEnabled publishes the container’s Documents in the Files app under On My iPhone > Minuta (the data lives in the Minuta subfolder there) and in Finder’s file-sharing pane when the device is connected.
    • LSSupportsOpeningDocumentsInPlace lets Files and other apps open those files where they are instead of copying them.

    Without both keys the default folder is invisible on the phone, and users cannot find, back up, or move their data. The folder is still local-only: it does not sync to iCloud Drive. Users who want cloud sync pick an iCloud Drive / Dropbox folder in Settings, or wait for the iCloud default described in 918-plan-icloud-default.

    Files edited from the Files app while Minuta is open are picked up by ExternalChangeMonitor (mtime polling), same as a sync-service drop.

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)