iCloud Backup and local-only files
App Group containers are included in iCloud Backup by default. An app that stores something locally precisely so it does not leave the device will, without further work, have it copied to iCloud anyway.
The resource value
Foundation exposes one control.
var url = fileURL
var values = URLResourceValues()
values.isExcludedFromBackup = true
try url.setResourceValues(values)
Note var url. setResourceValues mutates the URL, because the value is cached on the instance as well as written to disk.
Why marking files does not hold
Apple states the constraint directly: set this property each time you save a file, because some common file operations cause it to reset to false.
The mark lives on the inode. An operation that replaces the file rather than rewriting it produces a new inode, and the new one is unmarked.
SQLite does this as a matter of course. A database in WAL mode is three files:
blueguard.sqlite
blueguard.sqlite-wal
blueguard.sqlite-shm
The -wal and -shm files are deleted and recreated across checkpoints and connection lifecycles. A recreated sidecar comes back unmarked, and it holds the rows that have not yet reached the main database. The most recent writes are exactly the ones that end up in the backup.
Mark the directory
Apple's own guidance is to move the files into a directory and mark that.
enum LocalOnlyStore {
static func directory(in container: URL) throws -> URL {
let directory = container.appendingPathComponent(
"io.example.app.localonly",
isDirectory: true
)
try FileManager.default.createDirectory(
at: directory,
withIntermediateDirectories: true
)
var marked = directory
var values = URLResourceValues()
values.isExcludedFromBackup = true
try marked.setResourceValues(values)
return directory
}
}
SQLite recreates files inside the directory, never the directory itself, so the mark survives every checkpoint without being reapplied.
Naming the directory after the bundle identifier follows Apple's advice for directories created under Library, where a generic name risks colliding with something the system creates later.
Fail closed
The interesting decision is what to do when marking fails.
static func url() -> URL? {
guard let container = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)
else { return nil }
return try? directory(in: container)
}
Returning nil rather than an unmarked directory means callers store nothing. A missing local history is a missing feature. A local history copied to iCloud is the thing the product exists not to do.
Returning the unmarked directory would be the friendlier engineering decision and the wrong product decision.
Verifying it, properly
The obvious test sets the value and reads it back. It does not work, and it is worse than no test.
URL caches resource values on the instance, so reading through the same URL that set the value returns the cached answer even after the file has been deleted and recreated with no mark at all. The getter has also been observed disagreeing with the disk in both directions across OS versions.
Read the extended attribute instead, through a path rather than a cached URL.
func isExcludedOnDisk(_ path: String) -> Bool {
let name = "com.apple.metadata:com_apple_backup_excludeItem"
let size = getxattr(path, name, nil, 0, 0, 0)
return size > 0
}
That is what the setter actually writes, so it is the thing to assert on. A test that passes against a file with no mark at all is a test that will report success on the day this breaks.
What this does not give you
Apple is explicit that the mark exists to give the system guidance about what it can exclude. It is not a guarantee that an item never appears in a backup or on a restored device.
Copy that promises otherwise is overstating it. The defence at rest remains the file's data protection class.