Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Widget error upon restore iPhone: The file "Name.sqlite" couldn't be opened

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?

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:

  1. Run the app on my iPhone from Xcode and verify there is no error in the widget, note the sqlite file URL
  2. Perform iCloud backup
  3. Reset iPhone and restore from that backup
  4. 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
  5. 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
  6. Restart iPhone
  7. 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:

  1. The main app is doing "something" to the database that renders the widget copy functional (more on that later).

  2. 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).

Widget error upon restore iPhone: The file "Name.sqlite" couldn't be opened
 
 
Q