Plan: iCloud Drive as the default folder
Status: planned 2026-09-06, blocked on the holder’s iCloud materials. Tracked in the backlog under Integrations.
Context
The default folder on iOS is the app container’s Documents/Minuta. Since 2026-09-06 it is visible in the Files app (On My iPhone > Minuta) through UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace, but it is local to the device: it never appears in iCloud Drive and nothing syncs it. People who install Minuta on a phone expect the default folder to be in iCloud and shared with their other devices.
Minuta’s sync model is “any folder sync service”. iCloud Drive is the folder sync every Apple device already has, so an iCloud ubiquity container as the default folder gives zero-config sync between iPhone, iPad and Mac without any server code and without breaking the “no network requests” rule: the system syncs the files, the app only reads and writes them.
Goal: when iCloud Drive is available, the default folder is iCloud Drive/Minuta (the app’s public ubiquity container), visible in Files and Finder and synced by the system. When iCloud is unavailable (not signed in, iCloud Drive off, or a build without the entitlement such as the Homebrew/personal-team build) the default stays the local Documents/Minuta. Folders the user picked explicitly are untouched.
Needed from the Account Holder (blocks testing on devices)
Add an “iCloud” section to TESTFLIGHT-HOLDER.md:
- Identifiers > App ID
tools.minuta.app> enable the iCloud capability; create the containeriCloud.tools.minuta.appand assign it. CloudKit is not needed (documents only). The widget App ID does not need iCloud. - Profiles: a capability change invalidates
tools.minuta.app AppStore(and latertools.minuta.app MacAppStore). Regenerate and resend the.mobileprovision/.provisionprofile. Check withsecurity cms -D -i <profile> | plutil -extract Entitlements xml1 -o - -thatcom.apple.developer.ubiquity-container-identifiersis present. - Strongly recommended: an Apple Development certificate (
.p12, password through a separate channel) plus aniOS App Developmentprofiletools.minuta.app Developmentwith Ramil’s device UDIDs. iCloud cannot be used on the free personal teamUKMJ95C96B, so without a development profile every iteration goes through TestFlight.
Project changes
Entitlements:
Minuta/TestFlight/Minuta.entitlements(TestFlight/App Store only; the committed Debug entitlements stay empty) and the plannedMinuta.macOS.entitlementsfrom 917-plan-mac-testflight, so the Mac App Store build defaults to the same container:com.apple.developer.icloud-container-identifiers = [iCloud.tools.minuta.app]com.apple.developer.icloud-services = [CloudDocuments]com.apple.developer.ubiquity-container-identifiers = [iCloud.tools.minuta.app]
For local development with the holder’s development profile: an untracked
Minuta/Development.xcconfigmirroringTestFlight.xcconfig(manual signing,Apple Developmentidentity, theDevelopmentprofile, same entitlements file).Minuta/Sources/Info.plist,NSUbiquitousContainers:iCloud.tools.minuta.app NSUbiquitousContainerIsDocumentScopePublic = true NSUbiquitousContainerSupportedFolderLevels = Any NSUbiquitousContainerName = MinutaThe public scope is what makes the container appear as an
iCloud Drive/Minutafolder in Files and Finder. iCloud caches these values perCFBundleVersion; change them only together with a build-number bump.Default location resolution (
Minuta/Sources/Services/StorageLocationManager.swift):- New
UbiquityContainerResolver(app target): callsFileManager.default.url(forUbiquityContainerIdentifier: nil)off the main thread (the first call can take seconds and creates the container), caches the result, returnsnilwhen iCloud is unavailable. - Data root is
<container>/Documentsdirectly, noMinutasubfolder: the container is already shown asMinutain iCloud Drive, andMinuta/Minutawould be redundant. storageURLis synchronous and read during init, so resolution runs once at launch beforeAppStatebuilds the storage service (the app initialization is already async). Until resolved the app does not touch the default folder.StorageSetupViewshows “Default: iCloud Drive/Minuta” or “Default: Documents/Minuta” depending on the result.- Observe
NSUbiquityIdentityDidChange(sign in/out) and re-resolve;AppState.reinitializeStorage()reloads.
- New
Files that are not downloaded yet. In a ubiquity container a file that exists in iCloud but not on this device is a placeholder named
.<name>.icloud.AutomergeStorageServiceenumerates withcontentsOfDirectoryand would skip them. Changes inShared/Sources/MinutaShared/Services/AutomergeStorageService.swift:- Map
.<name>.icloudto<name>when listing. - Call
startDownloadingUbiquitousItem(at:)for placeholders and wait forURLResourceKey.ubiquitousItemDownloadingStatusKey == .currentbefore reading. - New
UbiquitousStorageMonitor(app target) with anNSMetadataQueryonNSMetadataQueryUbiquitousDocumentsScope: keeps everything downloaded (the data is tiny; never evict) and feedsExternalChangeMonitorfrom the query’s update notification instead of 5-second mtime polling when the storage folder is ubiquitous.
- Map
Coordinated I/O. Wrap reads and writes in
NSFileCoordinator(coordinate(readingItemAt:)/coordinate(writingItemAt:)) when the base URL is inside a ubiquity container. This is also correct for user-picked iCloud Drive folders. The existingflockon.minuta.writelock.nosyncstays for the same-machine app + CLI case;.nosyncfiles are never uploaded, so the lock stays local as intended..minuta-versionsyncs, which is fine.Conflicts. Inside a ubiquity container iCloud does not create
name 2.automergecopies (that is iCloud Drive behavior for non-coordinating writers); it keepsNSFileVersion.unresolvedConflictVersionsOfItem(at:). Add a resolver that merges each conflict version into the current document with the same Automerge mergeConflictResolutionServicealready uses, thenremoveOtherVersionsOfItem(at:)and marks the versions resolved. Because every file is a CRDT document, the merge loses nothing regardless of which version iCloud picked as current.ConflictResolutionServicekeeps handling thename 2/(conflicted copy)naming for plain synced folders.Migration of existing users (decision 2026-09-06: always ask, never move silently).
- Detection, after the container resolves: the user is on the local default (
hasSelectedLocation == true, no bookmark) and the localDocuments/Minutacontainstags.automergeorrecords/. Users with an explicitly chosen folder are not asked. - Prompt: “Move to iCloud Drive” / “Keep on this device”. The choice is stored (
storageMigrationDecision), so the prompt shows once; “Keep on this device” leaves the local default and the user can still switch in Settings later. - Order: wait for the
NSMetadataQueryto report every placeholder in the container as.currentfirst, then migrate under the write lock, off the main thread. Merging into a not-yet-downloaded version would only produce an iCloud conflict version to resolve afterwards. - Empty container (first device):
FileManager.setUbiquitous(true, itemAt:destinationURL:)per file. It moves, not copies; the local copy is gone afterwards. - Container already has data (another device synced first):
setUbiquitousfails on an existing destination, andtags.automerge/records/YYYY/MM.automergecollide by name. Load both documents, Automerge-merge, save into the container, delete the local file. Nothing is lost regardless of which side is older. - After a successful move:
useDefaultLocation()semantics stay (the default now resolves to the container),AppState.reinitializeStorage(), and the empty local folder is removed.
- Detection, after the container resolves: the user is on the local default (
CLI discovery (
Shared/Sources/MinutaCLI/Runtime/StorageResolver.swift). On the Mac the container is~/Library/Mobile Documents/iCloud~tools~minuta~app/Documents. Add it as a discovery step afterstorage-path.txtand before~/Documents/Minuta. This also covers the sandboxed Mac App Store build that cannot write the pointer (see 917).doctorwarns when the folder contains.icloudplaceholders, since the CLI cannot trigger downloads on its own.Builds without the entitlement (Debug on the personal team, Homebrew release):
url(forUbiquityContainerIdentifier:)returnsniland behavior is exactly today’s. UI tests keep using--test-storage=.
Verification
cd Shared && swift test;MinutaTestsfor the resolver fallback, placeholder name mapping, and the version-merge function (in-memory Automerge documents, no iCloud needed).- Personal-team Debug build on the simulator and
./scripts/run-mac.sh: no entitlement, storage stays local, all UI tests green on both platforms. - With the holder’s development profile on a device signed in to iCloud: fresh install shows “Default: iCloud Drive/Minuta”; Files shows
iCloud Drive/Minutawithtags.automergeandrecords/; a timer started on the iPhone appears on a Mac App Store build (or a Mac with the same iCloud account and the container synced) within a minute; edits made offline on both devices merge with no lost records; signing out of iCloud falls back to local storage without a crash. - Existing local-default user upgrades: the move prompt appears once; “Keep on this device” leaves everything where it was.
- TestFlight build uploads with the regenerated profile (no ITMS entitlement mismatch).
Docs to update when implemented
- 305-storage-location: default resolution order and the ubiquity resolver.
- 502-multi-cloud-sync: iCloud Documents section becomes “implemented”.
- 506-cross-process-safety: coordinated I/O and
NSFileVersionconflicts. - 311-cli: the container path in discovery order,
doctorplaceholder warning. - 702-testflight and
TESTFLIGHT-ORG.md: iCloud entitlements, regenerated profiles;TESTFLIGHT-HOLDER.md: the “iCloud” section above. - Support page: “Where is my data?” answer for iPhone (Files > iCloud Drive > Minuta, or On My iPhone > Minuta when iCloud is off).
Nice to have
- “Show in Files” button in Settings > Data:
UIApplication.openwith ashareddocuments://<path>URL opens the Files app at the storage folder. Works for the local folder already.