I have an app that uses NSPersistentCloudKitContainer
stored in a shared location via App Groups so my widget can fetch data to display. It works. But if you reset your iPhone and restore it from a backup, an error occurs:
The file "Name.sqlite" couldn't be opened
. I suspect this happens because the widget is created before the app's data is restored. Restarting the iPhone is the only way to fix it though, opening the app and reloading timelines does not. Anything I can do to fix that to not require turning it off and on again?
Widget error upon restore iPhone: The file "Name.sqlite" couldn't be opened
Do you check if the store URL you pass to NSPersistentCloudKitContainer
is valid in the failure case, and if the URL is exactly the same as the one in the successful case (after you restart your iPhone)? I am wondering if the system returns you the right root path of the App Group container after you restore from backup...
Best,
——
Ziqiao Chen
Worldwide Developer Relations.
Hi Ziqiao Chen! It seems the URL does change after reset (but is still valid) and the URL does not change upon restarting iPhone to get the successful case.
I added debug text to my widget to show the URL and whether it can be accessed, then performed these steps:
- Run the app on my iPhone from Xcode and verify there is no error in the widget, note the sqlite file URL
- Perform iCloud backup
- Reset iPhone and restore from that backup
- The app is not automatically installed because only apps downloaded from the App Store are reinstalled, so I had to enable Developer Mode then run the app from Xcode
- The error message appears in the widget - the sqlite file couldn't be opened, it is a different URL than it was before the iPhone was reset, and yet FileManager says the file exists, the file is readable and writable, and the contents at that path are non-nil
- Restart iPhone
- The error message is not shown in the widget and everything else is the same (the URL, file exists, readable, writable, non-nil contents)
The URL in step 1 was
file:///private/var/mobile/Containers/Shared/AppGroup/FAF64427-9826-4C86-9C2E-D7E5285BA7EC/MyApp.sqlite
The URL in step 5 and 7 was
file:///private/var/mobile/Containers/Shared/AppGroup/FDE4F3AF-E775-4D5F-842D-1C5AA77BE26F/MyApp.sqlite
The URL is set like this
myPersistentCloudKitContainer.persistentStoreDescriptions[0].url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.mydomain.myapp")!.appendingPathComponent("\(container.name).sqlite")
.
Note if you install the app from the App Store instead of installing it via Xcode in step 4, you also see the error in the widget. But if you first delete the app and then install it, you of course wouldn't see the error since there's no existing data from the app.
While this may not replicate the issue exactly, it seems to be close enough with the same end result - the app can access the database but the widget fails to until you restart iPhone. It seems it can only be fully tested with the production app that's live on the App Store, since that's the only way iOS will reinstall it upon restore from backup. I can't put debug text in my widget for customers to see. 😀
So, the first thing here is that the basic difference between the initial path->
step 1 was file:///private/var/mobile/Containers/Shared/AppGroup/FAF64427-9826-4C86-9C2E-D7E5285BA7EC/MyApp.sqlite
...and the post restore path->
step 5 and 7 was file:///private/var/mobile/Containers/Shared/AppGroup/FDE4F3AF-E775-4D5F-842D-1C5AA77BE26F/MyApp.sqlite
...is the standard behavior of the system, as the UUIDs are generated by the system whenever the app group is created.
The real oddity here is this:
and yet FileManager says the file exists, the file is readable and writable, and the contents at that path are non-nil
The FileManager is low level enough that whatever it says is inherently "true". So, "some" kind of file was absolutely there and was manipulatable by your app. More to the point, the file NOT being there wouldn't have been a bad thing, as CoreData would simply have created the file from scratch using the cloud.
In terms of what's going on here, I suspect that the actual issue is a lower level sqlite issue of some kind. I'm not sure what the specific issue would be, but the console log maybe printing more data and you can also try copying the file "out" to get a more direct look at the file.
Looking at this point:
Anything I can do to fix that to not require turning it off and on again?
One option would be to simply delete the file and let CoreData pull the data from the cloud. As a side note on that point, you might want to consider just excluding the file from the backup entirely. One of the issue that can come up if you're dealing with data that changes frequently is that the user restoring from a much older backup (for reasons unrelated to your app) can introduce unnecessary complications.
In terms of other options, two points I'm curious about:
Restarting the iPhone is the only way to fix it though,
What does your app actually "do" and, most importantly, what if any background modes/work does it use? On it's own I'm not sure how restarting would have changed anything, but having your app run FIRST (before the widget did) might have changed something. Does that fit in with any of what your app is doing?
opening the app and reloading timelines does not.
Does your Widget try to setup myPersistentCloudKitContainer again at any of those points?
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
It's interesting huh!
One option would be to simply delete the file and let CoreData pull the data from the cloud … you might want to consider just excluding the file from the backup entirely
I think this is not an option because the user can turn off iCloud and NSPersistentCloudKitContainer still works to store data without syncing to iCloud. So not all users can get this data back from iCloud, it may only exist in their backup.
What does your app actually "do" and, most importantly, what if any background modes/work does it use?
You can think of it as a very simple "todo" app where you create a todo, it shows up in the widget, and tapping a button marks it complete (via the main app process). Note the widget's access to the database is read-only and cloudKitContainerOptions is not set so the widget extension process does not sync with iCloud (only the main app process does). The only background modes/work used in the app is the "remote notifications" capability that allows CloudKit to silently notify the app when there's new content enabling NSPersistentCloudKitContainer to do its magic - there's no additional background tasks implemented.
On its own I'm not sure how restarting would have changed anything, but having your app run FIRST (before the widget did) might have changed something. Does that fit in with any of what your app is doing?
I did more testing here. If I follow the steps and install the app from the App Store after restore, the widget is run and shows the error, now if you restart the iPhone again the widget is run and shows the same error. So restarting the iPhone only resolves the problem after the main app has been run. If you never open the app, the widget error never resolves. You have to open the app and then restart the device to resolve it.
The only code differences I have for persistence setup in the app vs the widget is to migrate the file at the default store URL to the shared app groups URL if it hasn't been migrated yet, set cloudKitContainerOptions, and it does not set isReadOnly.
Does your Widget try to setup myPersistentCloudKitContainer again at any of those points?
The widget sets up the NSPersistentCloudKitContainer and calls loadPersistentStores only once upon init, which happens at the time WidgetKit calls getSnapshot
, getTimeline
, or relevance
(whichever is called first).
Alright, now this all starts to make sense. Starting with what's going on here:
I did more testing here. If I follow the steps and install the app from the App Store after restore, the widget is run and shows the error, now if you restart the iPhone again the widget is run and shows the same error. So restarting the iPhone only resolves the problem after the main app has been run. If you never open the app, the widget error never resolves. You have to open the app and then restart the device to resolve it.
So, there are two different issues at play here:
-
The main app is doing "something" to the database that renders the widget copy functional (more on that later).
-
Your Widget's internal logic is such that if it doesn't get the database working at initial launch, it can't get it working at all. I don't think the restart itself is actually REQUIRED, that's just the simplest way to force the widget to cold start.
Starting with #1, I think this is what actually causes the problem:
Note the widget's access to the database is read-only and cloudKitContainerOptions is not set so the widget extension process does not sync with iCloud (only the main app process does).
I think what's going on here is that the data store "knows" it's out of date, but it can't fix that (because it's read only). So it's preferring to fail instead of presenting the "wrong" data.
Note that being "out of data" is likely to be a fairly common case because the backup occurred at time "X" but the device was erased at time "X + n minutes" later. However, if you wanted to test my guess, you could try doing the following:
-
Prep the device to be erased (turn of Find My, etc...).
-
Put the device into Airplane mode (so you cut off cloud access).
-
Back the device up to a mac.
-
Immediately erase the device as soon as the backup finishes.
-
Restore the device using the backup you just made.
...and there's a decent chance everything works fine. I suspect an iCloud backup immediately followed by and erase/restore would also work, but that's trickier to predict.
In terms of what you do about this, what I would NOT do is make the widget read/write. While that probably would resolve the immediate issue (and might be worth testing as another way of confirming the general "theory" is correct), I also think using a read only architecture is VASTLY preferable to multiple read/write, particularly if you haven't designed with read/write in mind from the start.
That leads to #2....
I'm not that familiar with WidgetKit, but I'd assume you could set things up so that could detect that the database was dead and retry "later". Building on that, what I'd probably do is something like this:
-
Widget detects problem, present a UI that basically says "please launch app".
-
The app launch clears the issue.
-
At the next opportunity, the Widget sorts out the database and returns to normal operation.
Note that the initial UI doesn't even really need to be an error/warning dialog, just a "hey, I need you to open the app so I can start working".
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Thanks Kevin! I captured a sysdiagnose and found this:
error 2025-06-17 08:33:06.224277 -0600 MyAppWidget os_unix.c:46922: (2) open(/private/var/mobile/Containers/Shared/AppGroup/FC8E38EF-1FEF-4C9A-9712-D10421E30FE5/Name.sqlite-wal) - No such file or directory
error 2025-06-17 08:33:06.224284 -0600 MyAppWidget unable to open database file in "SELECT TBL_NAME FROM SQLITE_MASTER WHERE TBL_NAME = 'Z_METADATA'"
error 2025-06-17 08:33:06.224863 -0600 MyAppWidget error: (14) I/O error for database at /private/var/mobile/Containers/Shared/AppGroup/FC8E38EF-1FEF-4C9A-9712-D10421E30FE5/Name.sqlite. SQLite error code:14, 'unable to open database file'
I confirmed upon restore the sqlite file exists but sqlite-wal does not - it won’t open without that file. But that file does get created once you open the app, and then you can reinitialize the widget by restarting the iPhone (that’s the only way I know to make the system cold start the widget process). You’re exactly right, setting isReadOnly
is what causes this behavior. If I do not put it in read-only mode, the error does not occur because it is apparently able to open the database file without the sqlite-wal file, the same way the app is able to.
So I can work around the issue by setting isReadOnly
only when the sqlite-wal file exists. That means it would be in read/write mode until the user restarts the iPhone then it’ll be read-only. I’m wondering if that's a bad idea. I don’t attempt to write to the database from the widget, but the widget does not sync (cloudKitContainerOptions
is not set), so are there any implications in making the widget open in read/write mode, creating the wal file in the process? If the data has been changed on iCloud and thus is out of date, would that cause the app to fail to sync or have some other effect when it's opened? 🤔
Is it expected for the sqlite-wal file to be missing after restore? It seems if that were preserved the issue would not occur (maybe the sqlite-shm file is also required, that too is missing).
I confirmed upon restore the sqlite file exists but sqlite-wal does not - it won’t open without that file.
Correct. For context, "WAL" stands for "Write-Ahead Logging" and is part of the architecture sqlite uses to maintain data integrity. I guess/suspect that it's not be backed up because, assuming the database is being properly managed, it should generally be "empty" anytime it would have been backed up (because the contents were merged into the file at close/suspend). You can read more about it here:
https://www.sqlite.org/wal.html
But the critical point is:
...Beginning with version 3.22.0 (2018-01-22), a read-only WAL-mode database file can be opened if the -shm and -wal files already exist or those files can be created or the database is immutable.
SO, basically, yes, what you're seeing is expected behavior.
Shifting to the workaround side, I would to three things:
(1) Implement a fallback "there's a problem, open the app" UI. It's likely that you've identified the primary (possibly "only") issue here, but I'm a big believer in having "fallback" plans in place. The critical benefit here is that if/when your users start seeing this UI, you "know" that the solution below didn't cover what ever happened
So I can work around the issue by setting isReadOnly only when the sqlite-wal file exists.
(2) I would actually just try opening the file read only and going to #3 if it fails. You might think about checking for the WAL file as a secondary diagnostic, but I'd avoid relying on the EXACT details* of the configuration.
*For example, I can't guarantee that their aren't cases where the WAL file does exist, r/o opens will fail, and which a r/w open will resolve. If you specifically check for the WAL file, then you'll end up "skipping" cases you'd other wise fix.
That means it would be in read/write mode until the user restarts the iPhone then it’ll be read-only. I’m wondering if that's a bad idea.
Yes, that's a bad idea, as it creates a situation where your widget is sometimes read only or sometimes read write and you'd potentially need to be auditing both of those code paths "fully". Honestly, I think you'd be better of being read write "all the time" in that case, as you'd at least be testing that path much more throughly. However, what I'd actually recommend is:
(3) Do a read/write open, then destroy those CoreData object and do your read only open again. I think that will let you open the file and it means the read/write sequence is trivial to debug/verify (since the file is barely open at all).
__
Kevin Elliott
DTS Engineering, CoreOS/Hardware
Thank you, this is very helpful. To confirm we're aligned:
- Open the database in read-only mode
- If loadPersistentStores fails, try it again in read-write mode
- If that succeeds destroy those CoreData object and do a read-only open again
- If that fails show an error message to the user
destroy those CoreData object
Is that accomplished via container.persistentStoreCoordinator.destroyPersistentStore
, so all together like so?
let container = NSPersistentCloudKitContainer(name: "AppName")
let sharedStoreURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.domain.appname")!.appendingPathComponent("\(container.name).sqlite")
guard FileManager.default.fileExists(atPath: sharedStoreURL.path) else {
// Show error to user
return
}
let description = container.persistentStoreDescriptions.first!
description.url = sharedStoreURL
description.isReadOnly = true
container.loadPersistentStores { description, error in
if let error {
container.persistentStoreDescriptions.first?.isReadOnly = false
container.loadPersistentStores { description, error in
if let error {
// Show error to user
} else {
try? container.persistentStoreCoordinator.destroyPersistentStore(at: sharedStoreURL, type: .sqlite)
container.persistentStoreDescriptions.first?.isReadOnly = true
container.loadPersistentStores { description, error in
if let error {
// Show error to user
}
}
}
}
}
}