Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
496
.claude/skills/axiom-icloud-drive-ref/SKILL.md
Normal file
496
.claude/skills/axiom-icloud-drive-ref/SKILL.md
Normal file
@@ -0,0 +1,496 @@
|
||||
---
|
||||
name: axiom-icloud-drive-ref
|
||||
description: Use when implementing 'iCloud Drive', 'ubiquitous container', 'file sync', 'NSFileCoordinator', 'NSFilePresenter', 'isUbiquitousItem', 'NSUbiquitousKeyValueStore', 'ubiquitous file sync' - comprehensive file-based iCloud sync reference
|
||||
license: MIT
|
||||
compatibility: iOS 5.0+, iPadOS 13.0+, macOS 10.7+
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
last-updated: "2025-12-12"
|
||||
---
|
||||
|
||||
# iCloud Drive Reference
|
||||
|
||||
**Purpose**: Comprehensive reference for file-based iCloud sync using ubiquitous containers
|
||||
**Availability**: iOS 5.0+ (basic), iOS 8.0+ (iCloud Drive), iOS 11.0+ (modern APIs)
|
||||
**Context**: File-based cloud storage, not database (use CloudKit for structured data)
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing document-based iCloud sync
|
||||
- Syncing user files across devices
|
||||
- Building document-based apps (like Pages, Numbers)
|
||||
- Coordinating file access across processes
|
||||
- Handling iCloud file conflicts
|
||||
- Using NSUbiquitousKeyValueStore for preferences
|
||||
|
||||
**NOT for**: Structured data with relationships (use `axiom-cloudkit-ref` instead)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**iCloud Drive is for FILE-BASED sync**, not structured data.
|
||||
|
||||
**Use when**:
|
||||
- User creates/edits documents
|
||||
- Files need to sync like Dropbox
|
||||
- Document picker integration
|
||||
|
||||
**Don't use when**:
|
||||
- Need queryable structured data (use CloudKit)
|
||||
- Need relationships between records (use CloudKit)
|
||||
- Small key-value preferences (use NSUbiquitousKeyValueStore)
|
||||
|
||||
---
|
||||
|
||||
## Ubiquitous Containers
|
||||
|
||||
### Getting Ubiquitous Container URL
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Get iCloud container
|
||||
func getICloudContainerURL() -> URL? {
|
||||
// nil = use first container in entitlements
|
||||
return FileManager.default.url(
|
||||
forUbiquityContainerIdentifier: nil
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ Check if iCloud is available
|
||||
if let iCloudURL = getICloudContainerURL() {
|
||||
print("iCloud available: \(iCloudURL)")
|
||||
} else {
|
||||
print("iCloud not available (not signed in or no entitlement)")
|
||||
}
|
||||
```
|
||||
|
||||
### Container Structure
|
||||
|
||||
```
|
||||
iCloud Container/
|
||||
├── Documents/ # User-visible files (Files app)
|
||||
│ └── MyApp/ # Your app's documents
|
||||
├── Library/ # Hidden from user
|
||||
│ ├── Application Support/
|
||||
│ └── Caches/
|
||||
```
|
||||
|
||||
### Saving to iCloud Drive
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Save document to iCloud
|
||||
func saveToICloud(data: Data, filename: String) throws {
|
||||
guard let iCloudURL = FileManager.default.url(
|
||||
forUbiquityContainerIdentifier: nil
|
||||
) else {
|
||||
throw iCloudError.notAvailable
|
||||
}
|
||||
|
||||
let documentsURL = iCloudURL.appendingPathComponent("Documents")
|
||||
|
||||
// Create directory if needed
|
||||
try FileManager.default.createDirectory(
|
||||
at: documentsURL,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
let fileURL = documentsURL.appendingPathComponent(filename)
|
||||
|
||||
// Use file coordination for safe access
|
||||
let coordinator = NSFileCoordinator()
|
||||
var error: NSError?
|
||||
|
||||
coordinator.coordinate(
|
||||
writingItemAt: fileURL,
|
||||
options: .forReplacing,
|
||||
error: &error
|
||||
) { newURL in
|
||||
try? data.write(to: newURL)
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Coordination (Critical for Safety)
|
||||
|
||||
**Always use NSFileCoordinator** when accessing iCloud files. This prevents:
|
||||
- Race conditions with sync
|
||||
- Data corruption
|
||||
- Lost updates
|
||||
|
||||
### Reading Files
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Coordinated read
|
||||
func readICloudFile(url: URL) throws -> Data {
|
||||
let coordinator = NSFileCoordinator()
|
||||
var data: Data?
|
||||
var coordinationError: NSError?
|
||||
|
||||
coordinator.coordinate(
|
||||
readingItemAt: url,
|
||||
options: [],
|
||||
error: &coordinationError
|
||||
) { newURL in
|
||||
data = try? Data(contentsOf: newURL)
|
||||
}
|
||||
|
||||
if let error = coordinationError {
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
throw fileError.readFailed
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
### Writing Files
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Coordinated write
|
||||
func writeICloudFile(data: Data, to url: URL) throws {
|
||||
let coordinator = NSFileCoordinator()
|
||||
var coordinationError: NSError?
|
||||
|
||||
coordinator.coordinate(
|
||||
writingItemAt: url,
|
||||
options: .forReplacing,
|
||||
error: &coordinationError
|
||||
) { newURL in
|
||||
try? data.write(to: newURL)
|
||||
}
|
||||
|
||||
if let error = coordinationError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Moving Files
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Coordinated move
|
||||
func moveFile(from sourceURL: URL, to destURL: URL) throws {
|
||||
let coordinator = NSFileCoordinator()
|
||||
var coordinationError: NSError?
|
||||
|
||||
coordinator.coordinate(
|
||||
writingItemAt: sourceURL,
|
||||
options: .forMoving,
|
||||
writingItemAt: destURL,
|
||||
options: .forReplacing,
|
||||
error: &coordinationError
|
||||
) { newSource, newDest in
|
||||
try? FileManager.default.moveItem(at: newSource, to: newDest)
|
||||
}
|
||||
|
||||
if let error = coordinationError {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## URL Resource Values for iCloud
|
||||
|
||||
### Checking iCloud Status
|
||||
|
||||
```swift
|
||||
// ✅ Check if file is in iCloud
|
||||
func isInICloud(url: URL) -> Bool {
|
||||
let values = try? url.resourceValues(forKeys: [.isUbiquitousItemKey])
|
||||
return values?.isUbiquitousItem ?? false
|
||||
}
|
||||
|
||||
// ✅ Check download status
|
||||
func getDownloadStatus(url: URL) -> String {
|
||||
let values = try? url.resourceValues(forKeys: [
|
||||
.ubiquitousItemDownloadingStatusKey,
|
||||
.ubiquitousItemIsDownloadingKey,
|
||||
.ubiquitousItemDownloadingErrorKey
|
||||
])
|
||||
|
||||
if let downloading = values?.ubiquitousItemIsDownloading, downloading {
|
||||
return "Downloading..."
|
||||
}
|
||||
|
||||
if let status = values?.ubiquitousItemDownloadingStatus {
|
||||
switch status {
|
||||
case .current:
|
||||
return "Downloaded"
|
||||
case .notDownloaded:
|
||||
return "Not downloaded (iCloud only)"
|
||||
case .downloaded:
|
||||
return "Downloaded"
|
||||
@unknown default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// ✅ Check upload status
|
||||
func isUploading(url: URL) -> Bool {
|
||||
let values = try? url.resourceValues(forKeys: [.ubiquitousItemIsUploadingKey])
|
||||
return values?.ubiquitousItemIsUploading ?? false
|
||||
}
|
||||
|
||||
// ✅ Check for conflicts
|
||||
func hasConflicts(url: URL) -> Bool {
|
||||
let values = try? url.resourceValues(forKeys: [
|
||||
.ubiquitousItemHasUnresolvedConflictsKey
|
||||
])
|
||||
return values?.ubiquitousItemHasUnresolvedConflicts ?? false
|
||||
}
|
||||
```
|
||||
|
||||
### Downloading Files
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Request download
|
||||
func downloadFromICloud(url: URL) throws {
|
||||
try FileManager.default.startDownloadingUbiquitousItem(at: url)
|
||||
}
|
||||
|
||||
// ✅ Monitor download progress
|
||||
let query = NSMetadataQuery()
|
||||
query.predicate = NSPredicate(format: "%K == %@",
|
||||
NSMetadataItemURLKey, url as NSURL)
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .NSMetadataQueryDidUpdate,
|
||||
object: query,
|
||||
queue: .main
|
||||
) { notification in
|
||||
// Check progress
|
||||
if let item = query.results.first as? NSMetadataItem {
|
||||
if let percent = item.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as? Double {
|
||||
print("Downloaded: \(percent)%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query.start()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
### Detecting Conflicts
|
||||
|
||||
```swift
|
||||
// ✅ Get conflict versions
|
||||
func getConflictVersions(for url: URL) -> [NSFileVersion]? {
|
||||
return NSFileVersion.unresolvedConflictVersionsOfItem(at: url)
|
||||
}
|
||||
```
|
||||
|
||||
### Resolving Conflicts
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Resolve conflicts
|
||||
func resolveConflicts(at url: URL, keepingVersion: ConflictResolution) throws {
|
||||
guard let conflicts = NSFileVersion.unresolvedConflictVersionsOfItem(at: url),
|
||||
!conflicts.isEmpty else {
|
||||
return // No conflicts
|
||||
}
|
||||
|
||||
let current = try NSFileVersion.currentVersionOfItem(at: url)
|
||||
|
||||
switch keepingVersion {
|
||||
case .current:
|
||||
// Keep current version, discard others
|
||||
for conflict in conflicts {
|
||||
conflict.isResolved = true
|
||||
}
|
||||
|
||||
case .other(let chosenVersion):
|
||||
// Replace current with chosen conflict version
|
||||
try chosenVersion.replaceItem(at: url, options: [])
|
||||
chosenVersion.isResolved = true
|
||||
|
||||
// Mark other conflicts as resolved
|
||||
for conflict in conflicts where conflict != chosenVersion {
|
||||
conflict.isResolved = true
|
||||
}
|
||||
|
||||
case .manual:
|
||||
// App merges manually, then marks resolved
|
||||
let mergedData = mergeConflicts(current: current, conflicts: conflicts)
|
||||
try mergedData.write(to: url)
|
||||
|
||||
for conflict in conflicts {
|
||||
conflict.isResolved = true
|
||||
}
|
||||
}
|
||||
|
||||
// Remove resolved versions
|
||||
try NSFileVersion.removeOtherVersionsOfItem(at: url)
|
||||
}
|
||||
|
||||
enum ConflictResolution {
|
||||
case current
|
||||
case other(NSFileVersion)
|
||||
case manual
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NSUbiquitousKeyValueStore (Preferences Sync)
|
||||
|
||||
**For small preferences only** (<1 MB total, <1024 keys)
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: Sync small preferences
|
||||
let store = NSUbiquitousKeyValueStore.default
|
||||
|
||||
// Set values
|
||||
store.set(true, forKey: "darkModeEnabled")
|
||||
store.set(2.0, forKey: "textSizeMultiplier")
|
||||
store.set(["en", "es"], forKey: "selectedLanguages")
|
||||
|
||||
// Synchronize
|
||||
store.synchronize()
|
||||
|
||||
// Read values
|
||||
let darkMode = store.bool(forKey: "darkModeEnabled")
|
||||
let textSize = store.double(forKey: "textSizeMultiplier")
|
||||
|
||||
// Listen for changes from other devices
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
|
||||
object: store,
|
||||
queue: .main
|
||||
) { notification in
|
||||
// Update UI with new values
|
||||
updatePreferences()
|
||||
}
|
||||
```
|
||||
|
||||
**Limitations**:
|
||||
- Total storage: 1 MB
|
||||
- Max keys: 1024
|
||||
- Max value size: 1 MB
|
||||
- Use only for preferences, not data
|
||||
|
||||
---
|
||||
|
||||
## Entitlements
|
||||
|
||||
```xml
|
||||
<!-- iCloud capability -->
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
</array>
|
||||
|
||||
<!-- Ubiquitous containers -->
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.example.app</string>
|
||||
</array>
|
||||
|
||||
<!-- Key-value store (if using) -->
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)com.example.app</string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Document Picker Integration
|
||||
|
||||
```swift
|
||||
// ✅ Present iCloud document picker
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
let picker = UIDocumentPickerViewController(
|
||||
forOpeningContentTypes: [.pdf, .plainText]
|
||||
)
|
||||
picker.delegate = self
|
||||
picker.allowsMultipleSelection = false
|
||||
|
||||
// Enable iCloud
|
||||
picker.directoryURL = getICloudContainerURL()
|
||||
|
||||
present(picker, animated: true)
|
||||
```
|
||||
|
||||
### Pattern 2: Monitor Directory for Changes
|
||||
|
||||
```swift
|
||||
// ✅ Monitor iCloud directory
|
||||
class ICloudMonitor {
|
||||
let query = NSMetadataQuery()
|
||||
|
||||
func startMonitoring(directory: URL) {
|
||||
query.predicate = NSPredicate(format: "%K BEGINSWITH %@",
|
||||
NSMetadataItemPathKey, directory.path)
|
||||
|
||||
query.searchScopes = [NSMetadataQueryUbiquitousDataScope]
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: .NSMetadataQueryDidUpdate,
|
||||
object: query,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.processResults()
|
||||
}
|
||||
|
||||
query.start()
|
||||
}
|
||||
|
||||
func processResults() {
|
||||
for item in query.results {
|
||||
if let metadataItem = item as? NSMetadataItem,
|
||||
let url = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL {
|
||||
print("File: \(url.lastPathComponent)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | API | Notes |
|
||||
|------|-----|-------|
|
||||
| Get iCloud URL | `FileManager.default.url(forUbiquityContainerIdentifier:)` | Returns nil if unavailable |
|
||||
| Check if in iCloud | `.isUbiquitousItemKey` resource value | Bool |
|
||||
| Download file | `startDownloadingUbiquitousItem(at:)` | Async, monitor with NSMetadataQuery |
|
||||
| Check download status | `.ubiquitousItemDownloadingStatusKey` | current/notDownloaded/downloaded |
|
||||
| Check for conflicts | `.ubiquitousItemHasUnresolvedConflictsKey` | Bool |
|
||||
| Resolve conflicts | `NSFileVersion.unresolvedConflictVersionsOfItem(at:)` | Manual merge or choose version |
|
||||
| Sync preferences | `NSUbiquitousKeyValueStore.default` | <1 MB total |
|
||||
| File coordination | `NSFileCoordinator` | **Always** use for iCloud files |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `axiom-storage` — Choose iCloud Drive vs CloudKit
|
||||
- `axiom-cloudkit-ref` — For structured data sync
|
||||
- `axiom-cloud-sync-diag` — Debug iCloud sync issues
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Reference
|
||||
**Minimum iOS**: 5.0 (basic), 8.0 (iCloud Drive), 11.0 (modern APIs)
|
||||
Reference in New Issue
Block a user