# How to migrate NSPersistentCloudKitContainer to App Groups

Recently I needed to add iOS Widgets to my app [Wins](https://apple.co/3mk0NjK). As I discovered, if you are using Core Data and want to share data between your iOS app and widgets, you need to use App Groups.

At first, I tried to migrate my database following the solution by Donny Wals. In his book [Practical Core Data](https://practicalcoredata.com), there is a separate chapter dedicated to this topic: **“Chapter 6 - Sharing a Core Data store with apps and extensions”**. If you work with Core Data, I highly recommend reading this chapter and the whole book. Donny is a great Core Data expert and his book helped me a lot with understanding this framework.

However, in his example, he is using **NSPersistentContainer**. And in my app, I am using **NSPersistentCloudKitContainer**, which means that I have Core Data with CloudKit support. When trying to migrate my database I ran into 2 problems:

1. My app was freezing during migration if iCloud was turned off (for example, if the user was not signed in to iCloud).
    
2. After migration, all my data was duplicated.
    

I spent many hours until I solved both issues. Using ChatGPT, reading Apple's documentation, and browsing through StackOverflow and Apple Developer Forums. As there wasn't one place with a complete working solution, I decided to write this article as it might help other iOS developers.

At the end of this article, I will show you my entire PersistenceController code. I want you to understand each part of the code, so let's have a look at each part of my PersistenceController. But first, a few words about App Groups.

## App Groups

What are they and why do we need them? By default, when an app uses Core Data, the app's database is created in the Application Support directory of this app. And only this particular app has access to this folder (and database). App Groups are shared folders. When 2 or more apps have access to this folder, they all can use the same database.

You can read how to configure App Groups in this article from Apple:  
[https://developer.apple.com/documentation/xcode/configuring-app-groups](https://developer.apple.com/documentation/xcode/configuring-app-groups)

Some people recommend adding App Groups support to all new projects as doing this at the beginning is much easier than migrating an existing database to the App Group folder.

In this article, I will show you how I migrated my database to the App Groups folder. First, I will show you my whole code for the PersistenceController. After this, I will talk about each part and explain why I did something the way I did.

## Full code

```swift
import CoreData

struct PersistenceController {
	static let shared = PersistenceController()
	
	// Create a database for Preview Canvas.
	static var preview: PersistenceController = {
		let result = PersistenceController(inMemory: true)
		let viewContext = result.container.viewContext
		
		for _ in 0..<4 {
			let newGoal = CDEGoal(context: viewContext)
			newGoal.type = GoalType.number.rawValue
			newGoal.date = Date()
			newGoal.icon = "trophy"
			newGoal.name = "Goal name"
			newGoal.goal = 1000
			newGoal.currently = 500
			newGoal.status = GoalStatus.planned.rawValue
			newGoal.color = "purple"
			newGoal.id = UUID()
		}
		
		do {
			try viewContext.save()
		} catch {
			let nsError = error as NSError
			fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
		}
		return result
	}()
	
	let container: NSPersistentCloudKitContainer
	
	var oldStoreURL: URL {
		let appSupport = FileManager.default.urls(
			for: .applicationSupportDirectory,
			in: .userDomainMask
		).first!
		return appSupport.appendingPathComponent("YourAppName.sqlite")
	}
	
	// We define new App Group containerURL.
	var sharedStoreURL: URL {
		let id = "group.com.yourDomain.YourAppName" // Use App Group's id here.
		let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id)!
		return containerURL.appendingPathComponent("YourAppName.sqlite")
	}

	init(inMemory: Bool = false) {
		container = NSPersistentCloudKitContainer(name: "iCloud.com.yourDomain.YourAppName")
		
		let description = container.persistentStoreDescriptions.first!
		let originalCloudKitOptions = description.cloudKitContainerOptions
		
		// Use the App Group store if migration is not needed (if default store without App Group doesn't exist).
		if !FileManager.default.fileExists(atPath: oldStoreURL.path) {
			description.url = sharedStoreURL
		} else {
			// Disable CloudKit integration if migration is needed.
			description.cloudKitContainerOptions = nil
		}
		
		description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
	
		if inMemory {
			description.url = URL(fileURLWithPath: "/dev/null")
		}
		
		// Load persistent stores.
		container.loadPersistentStores(completionHandler: { (storeDescription, error) in
			if let error = error as NSError? {
				fatalError("Unresolved error \(error), \(error.userInfo)")
			}
		})
		
		// Perform the migration.
		migrateStore(for: container, originalCloudKitOptions: originalCloudKitOptions)
		
		container.viewContext.automaticallyMergesChangesFromParent = true
	}

// Function migrateStore. Migrates data store to App Group if needed.
func migrateStore(for container: NSPersistentCloudKitContainer, originalCloudKitOptions: NSPersistentCloudKitContainerOptions?) {
	let coordinator = container.persistentStoreCoordinator
	let storeDescription = container.persistentStoreDescriptions.first!
	
	// Exit current scope if persistentStore(for:) returns nil (migration is not needed).
	guard coordinator.persistentStore(for: oldStoreURL) != nil else {
		print("Migration not needed")
		return
	}
		
		// Replace one persistent store with another.
		do {
			try coordinator.replacePersistentStore(
				at: sharedStoreURL,
				withPersistentStoreFrom: oldStoreURL,
				type: .sqlite
			)
		} catch {
			fatalError("Something went wrong while migrating the store: \(error)")
		}
		
		// Delete old store.
		do {
			try coordinator.destroyPersistentStore(at: oldStoreURL, type: .sqlite, options: nil)
		} catch {
			fatalError("Something went wrong while deleting the old store: \(error)")
		}
		
		NSFileCoordinator(filePresenter: nil).coordinate(writingItemAt: oldStoreURL.deletingLastPathComponent(), options: .forDeleting, error: nil, byAccessor: { url in
			try? FileManager.default.removeItem(at: oldStoreURL)
			try? FileManager.default.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent("\(container.name).sqlite-shm"))
			try? FileManager.default.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent("\(container.name).sqlite-wal"))
			try? FileManager.default.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent("ckAssetFiles"))
		})
		
		// Unload the store and load it again with new storeDescription to re-enable CloudKit.
		if let persistentStore = container.persistentStoreCoordinator.persistentStores.first {
			do {
				try container.persistentStoreCoordinator.remove(persistentStore)
				print("Persistent store unloaded")
			} catch {
				print("Failed to unload persistent store: \(error)")
			}
		}

		// Set the URL of the storeDescription to the sharedStoreURL.
		storeDescription.url = sharedStoreURL
		// Modify the storeDescription to re-enable CloudKit integration.
		storeDescription.cloudKitContainerOptions = originalCloudKitOptions

		// Load the persistent store with the updated storeDescription.
		container.loadPersistentStores(completionHandler: { (storeDescription, error) in
			if let error = error as NSError? {
				fatalError("Unresolved error \(error), \(error.userInfo)")
			}
		})
		
		print("Migration completed")
	}
}
```

That's the whole code. Now let's look at each part and I will explain to you what's happening and why.

## Create a PersistenceController

You are probably familiar with the first part of the code. Especially if you (like me) start with the default Core Data stack in Xcode. When we start **New Project** and choose "**App**" and later check "**Use Core Data**" and "**Host in iCloud**", we will end up with something like this. **PersistenceController** struct, **static let shared** for our app's data and **static var preview** for our **Preview Canvas**.

In the **for \_ in** block I am creating some objects for my previews. This is not important in the context of database migration, so please don't worry about this part of the code. In your app, this block will be different anyway.

```swift
struct PersistenceController {
	static let shared = PersistenceController()
	
	// Create a database for Preview Canvas.
	static var preview: PersistenceController = {
		let result = PersistenceController(inMemory: true)
		let viewContext = result.container.viewContext
		
		for _ in 0..<4 {
			let newGoal = CDEGoal(context: viewContext)
			newGoal.type = GoalType.number.rawValue
			newGoal.date = Date()
			newGoal.icon = "trophy"
			newGoal.name = "Goal name"
			newGoal.goal = 1000
			newGoal.currently = 500
			newGoal.status = GoalStatus.planned.rawValue
			newGoal.color = "purple"
			newGoal.id = UUID()
		}
		
		do {
			try viewContext.save()
		} catch {
			let nsError = error as NSError
			fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
		}
		return result
	}()
// More code...
}
```

## Declare properties

We need to declare a few properties first. We will use the name **container** for our **NSPersistentCloudKitContainer**. We will assign a particular iCloud container to it later in the **init** method.

We also declare **oldStoreURL** and **sharedStoreURL** properties that we will use later to move our database to the proper **App Group** folder.

```swift
let container: NSPersistentCloudKitContainer

var oldStoreURL: URL {
	let appSupport = FileManager.default.urls(
		for: .applicationSupportDirectory,
		in: .userDomainMask
	).first!
	return appSupport.appendingPathComponent("YourAppName.sqlite")
}

// Define new App Group containerURL.
var sharedStoreURL: URL {
	let id = "group.com.yourDomain.YourAppName" // Use App Group's id here.
	let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id)!
	return containerURL.appendingPathComponent("YourAppName.sqlite")
}
```

## Create the container

We set our container as an **NSPersistentCloudKitContainer** with a name similar to "**iCloud.com.yourDomain.YourAppName**". One note here: while Apple suggests this naming convention, you can as well use a different one, for example, "**iCloud.YourAppName**". The most important thing is that this name must match the name of the iCloud container selected in the **Signing & Capabilities** tab of your app's target.

We also create a constant **description** that we will use later in the code. And a constant **originalCloudKitOptions** which we will pass to the **migrateStore** function later.

```swift
init(inMemory: Bool = false) {
	container = NSPersistentCloudKitContainer(name: "iCloud.com.yourDomain.YourAppName")
	
	let description = container.persistentStoreDescriptions.first!
	let originalCloudKitOptions = description.cloudKitContainerOptions
	
// Other code.
}
```

## Check if migration is needed

In our code, we will check two times if the migration is needed. Here's the first time. We are checking if a database file exists at the **oldStoreURL**. This covers a situation when users run our app for the first time on some device. If so, the database doesn't exist yet on the device and we set the **URL** of our store to **sharedStoreURL** (App Group folder). Database migration won't be needed here at all.

```swift
init(inMemory: Bool = false) {
// Other code.

	// Use the App Group store if migration is not needed (if default store without App Group doesn't exist).
	if !FileManager.default.fileExists(atPath: oldStoreURL.path) {
		description.url = sharedStoreURL
	} else {
		// Disable CloudKit integration if migration is needed.
		description.cloudKitContainerOptions = nil
	}
	
// Other code.
}
```

As you can see, we also have an **else** statement. If a database file does exist on the device at the **oldStoreURL**, we disable iCloud sync by setting **cloudKitContainerOptions** to **nil**.

As I said earlier, my app was freezing during migration if iCloud was turned off on the device. With more detailed logging turned on, I saw that my app tries to connect to iCloud again and again and the migration code is not executed. The only solution I found is to turn off iCloud sync during the migration. So here in the code we are saying: if the database file exists at the **oldStoreURL**, turn off iCloud sync as we will perform the migration.

## Load persistent stores and migrate data

We can migrate the database only if the persistent store was loaded, so we load persistent stores first and after this, we migrate the data.

```swift
init(inMemory: Bool = false) {
// Other code.
	
	// Load the persistent stores.
	container.loadPersistentStores(completionHandler: { (storeDescription, error) in
		if let error = error as NSError? {
			fatalError("Unresolved error \(error), \(error.userInfo)")
		}
	})
	
	// Perform the migration.
	migrateStore(for: container, originalCloudKitOptions: originalCloudKitOptions)
	
// Other code.
}
```

We perform the migration using the **migrateStore** function that we will write in a moment as part of our **PersistenceController**.

## Function to migrate the store

In our **PersistenceController** we create a function named **migrateStore** which we execute at the end of our init method.

We will pass two arguments to this function: **container** and **originalCloudKitOptions**. We also declare the **coordinator** constant equal to **container.persistentStoreCoordinator** and **storeDescription** to make the part where we edit the container's options easier to read.

```swift
// Function migrateStore. Migrates data store to App Group if needed.
func migrateStore(for container: NSPersistentCloudKitContainer, originalCloudKitOptions: NSPersistentCloudKitContainerOptions?) {
	let coordinator = container.persistentStoreCoordinator
	let storeDescription = container.persistentStoreDescriptions.first!

    // More code.
}
```

## Check if migration is needed (again)

We also write a guard statement to check for the second time if the migration is needed. Let's say that a user opened a previous version of our app on his device before. If **persistentStore** for **oldStoreURL** is different than **nil**, it means that the database at the default location exists and we need to migrate the data to the App Group folder. If it doesn't exist (which means that we already migrated our data), print "Migration not needed" and exit from the current scope (**migrateStore** function).

```swift
// Exit current scope if persistentStore(for:) returns nil (migration is not needed).
guard coordinator.persistentStore(for: oldStoreURL) != nil else {
	print("Migration not needed")
	return
}
```

## Migrate database

With this code, we are migrating our database. But a more correct word would be **replacing**. And I will explain why in the next paragraphs.

```swift
// Replace one persistent store with another.
do {
	try coordinator.replacePersistentStore(
		at: sharedStoreURL,
		withPersistentStoreFrom: oldStoreURL,
		type: .sqlite
	)
} catch {
	fatalError("Something went wrong while migrating the store: \(error)")
}
```

We use the word "migrate" when we talk about changing one database to another (or when we change our data model). But this may lead us to one mistake. **NSPersistentStoreCoordinator** has 2 similar methods: [migratePersistentStore](https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/3747534-migratepersistentstore) and [replacePersistentStore](https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/3747536-replacepersistentstore). There is one crucial difference between them:

As one anonymous Apple wrote on [Apple Developer Forums](https://developer.apple.com/forums/thread/653975?answerId=621347022#621347022):

> migratePersistentStore is (...) creating a clean copy of all the data in your store file in a new location on the file system. Use replacePersistentStore instead to move the store to a new location.

I tested **migratePersistentStore** before knowing about **replacePersistentStore** and in the result I had duplicated data. With the **replacePersistentStore** method, no duplicates were created.

## Delete old database

With the [replacePersistentStore](https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/3747536-replacepersistentstore) method, we replaced the store with our new store located in the App Group. Apple's documentation doesn't tell us much about it. It just says this: "Replaces one persistent store with another.". I thought this method will move one store to another place and will literally move the database files. But it doesn't. After running the code I noticed that a new version of the database was in fact created in the App Group folder, but old files were still present in the app support folder.

To get rid of old files, we need to delete them by ourselves. To do this, we will use [destroyPersistentStore](https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/3747532-destroypersistentstore) and [removeItem](https://developer.apple.com/documentation/foundation/filemanager/1408573-removeitem) methods.

```swift
// Delete old store.
do {
	try coordinator.destroyPersistentStore(at: oldStoreURL, type: .sqlite, options: nil)
} catch {
	fatalError("Something went wrong while deleting the old store: \(error)")
}

NSFileCoordinator(filePresenter: nil).coordinate(writingItemAt: oldStoreURL.deletingLastPathComponent(), options: .forDeleting, error: nil, byAccessor: { url in
	try? FileManager.default.removeItem(at: oldStoreURL) // Delete .sqlite file.
	try? FileManager.default.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent("\(container.name).sqlite-shm")) // Delete .sqlite-shm file.
	try? FileManager.default.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent("\(container.name).sqlite-wal")) // Delete .sqlite-wal file.
	try? FileManager.default.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent("ckAssetFiles")) // Delete ckAssetFiles.
    // ckAssetFiles files may be named like this: AppName_ckAssets
})
```

When I checked the Simulator's folder holding my database I saw 3 files there: .sqlite, .sqlite-shm and .sqlite-wal. I didn't see any ckAssetFiles files, but this may be due to iCloud being turned off in the Simulator. I decided to leave the last FileManager.default.removeItem as it was in the example. If there will be **ckAssetFiles** in the database folder, they will be deleted. If they are not there, nothing will happen.

I also need to mention that I tried to comment out **coordinator.destroyPersistentStore** and my files were not deleted. I tried to leave **coordinator.destroyPersistentStore** and comment out **NSFileCoordinator** and again - my old files were not deleted. We need both parts. We need to **destroyPersistentStore** first and later delete each file with **FileManager.default.removeItem**.

I found this solution on [StackOverflow](https://stackoverflow.com/a/72585271/12315994) and I would like to quote a user named **Jordan H** who posted the solution:

> In talking with an engineer in a WWDC lab, they explained it does not actually delete the database files at the provided location as the documentation seems to imply. It actually just truncates rather than deletes. If you want them gone you can manually delete the files (if you can ensure no other process or a different thread is accessing them).

## Re-enable CloudKit

We moved our database. We deleted old files. Now we can re-enable CloudKit. As we can't change **cloudKitContainerOptions** on a running persistence store, we need to unload our current store, change **storeDescription.cloudKitContainerOptions** to the value previously stored in the **originalCloudKitOptions** constant and load the store again with these options.

```swift
// Unload the store and load it again with new storeDescription to re-enable CloudKit.
if let persistentStore = container.persistentStoreCoordinator.persistentStores.first {
	do {
		try container.persistentStoreCoordinator.remove(persistentStore)
		print("Persistent store unloaded")
	} catch {
		print("Failed to unload persistent store: \(error)")
	}
}

// Set the URL of the storeDescription to the sharedStoreURL
storeDescription.url = sharedStoreURL
// Modify the storeDescription to re-enable CloudKit integration
storeDescription.cloudKitContainerOptions = originalCloudKitOptions

// Load the persistent store with the updated storeDescription
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
	if let error = error as NSError? {
		fatalError("Unresolved error \(error), \(error.userInfo)")
	}
})

print("Migration completed")
```

## Final note

That's it. That's how I managed to migrate **NSPersistentCloudKitContainer** to **App Groups**. I tested this solution on Simulator and real devices. I tested with iCloud sync turned on and with iCloud sync turned off on the device. I tested if the data continues to sync after migration. And everything seems to work. However, I assume that it's not the best way of solving this problem. And also I am very far from being a Core Data expert, so please take this into consideration. Please double-check everything before using my code in your projects. Please test the results and make some changes if necessary. And if you know that something could be done better, please leave a comment and let me know about this.

## Thank you for reading!

If you want to support my work, please like, comment, share the article and most importantly...

📱Check out my apps on the App Store:  
[https://apps.apple.com/developer/next-planet/id1495155532](https://apps.apple.com/developer/next-planet/id1495155532)

☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!  
[https://ko-fi.com/kslazinski](https://ko-fi.com/kslazinski)
