After days of trying different things, I accidentally found out that the order of descriptions in the container array plays a crucial role in how Core Data prioritizes loading configurations and saving entities. In my original code, I was loading the default configuration first as shown below...
guard let description = container.persistentStoreDescriptions.first else{
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
Then I was appending the public configuration as follows...
container.persistentStoreDescriptions.append(publicDescription)
Leaving the public configuration at the end of the persistentStoreDescriptions array, and apparently, the way Core Data works is that it searches the first configuration, and if the entity you're saving exists in the first configuration, it saves it in that configuration otherwise it keeps looping through all configs until it finds the entity in one of the configurations but since (by default) the Default configuration contains all entities, it was always saving to the default configuration so, the solution is to always leave the default configuration as the last item in the array.
Solution:
Here is what I did that solved my issue:
Replaced this line...
container.persistentStoreDescriptions.append(publicDescription)
with...
container.persistentStoreDescriptions = [publicDescription, description]
Again, I basically added the Public configuration to the first configuration in the array. The key here is to always leave the default configuration at last, regardless of how many configurations you have.
FYI - The sample project from the Linking Data Between two Core Data Stores led me to try to reorder the configurations in the array.
https://developer.apple.com/documentation/coredata/linking_data_between_two_core_data_stores
Post
Replies
Boosts
Views
Activity
Just to clarify, the error shown in my original post is not an error that makes the app crash, it's an error I get in the debug console, I assume it's an error logged by CloudKit.
I don't think you can delete data created by other users, in what scenario would you want to delete the data created by another user? If what you really want, is for you as the main admin to be able to delete data other users created, I think you can do it directly in the ClouldKit console.
Just to make sure, when you say you deployed to production, are you referring to deploying the Schema changes in CloudKit? When using CoreData+CloudKit, you must deploy your schema once you're ready for production. Go to the CloudKit console, look for Deploy Schema Changes, and click on it.
Here is the code that works only if the user is logged in. Again, I'm connecting to the Public database.
class CoreDataManager: ObservableObject{
// Singleton
static let instance = CoreDataManager()
private let queue = DispatchQueue(label: "CoreDataManagerQueue")
private var iCloudSync = true
lazy var context: NSManagedObjectContext = {
return container.viewContext
}()
lazy var container: NSPersistentContainer = {
return setupContainer()
}()
func updateCloudKitContainer() {
queue.sync {
container = setupContainer()
}
}
func setupContainer()->NSPersistentContainer{
let container = NSPersistentCloudKitContainer(name: "CoreDataContainer")
guard let description = container.persistentStoreDescriptions.first else{
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
let cloudKitContainerIdentifier = "iCloud.com.example.PublicDatabaseTest"
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
description.cloudKitContainerOptions = options
description.cloudKitContainerOptions?.databaseScope = .public // Specify Public Database
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}
func save(){
do{
try context.save()
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
With the following code I'm successfully turning On or Off sync between CoreData and CloudKit. I have iCloudSync saved in UserDefault / @AppStorage and controlled with a toggle switch in my app settings. This line is what turn it off description.cloudKitContainerOptions = nil, I hope it helps.
class CoreDataManager: ObservableObject{
// Singleton
static let instance = CoreDataManager()
private let queue = DispatchQueue(label: "CoreDataManagerQueue")
@AppStorage(UserDefaults.Keys.iCloudSyncKey) private var iCloudSync = false
lazy var context: NSManagedObjectContext = {
return container.viewContext
}()
lazy var container: NSPersistentContainer = {
return setupContainer()
}()
func updateCloudKitContainer() {
queue.sync {
container = setupContainer()
}
}
func setupContainer()->NSPersistentContainer{
let container = NSPersistentCloudKitContainer(name: "YourCoreDataContainerName")
guard let description = container.persistentStoreDescriptions.first else{
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if iCloudSync{
let cloudKitContainerIdentifier = "iCloud.com.example.DatabaseName"
if description.cloudKitContainerOptions == nil {
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
description.cloudKitContainerOptions = options
}
}else{
description.cloudKitContainerOptions = nil
}
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}
func save(){
do{
try context.save()
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
@Claude31 - Thank you very much for your reply. For some reason resizing the UIImage directly solves the issue.
if let inputImage = UIImage(data: category.image ?? Data()) {
let resizeImage = inputImage.scalePreservingAspectRatio(targetSize: CGSize(width: 25, height: 25))
Image(uiImage: resizeImage)
.resizable()
.scaledToFit()
}
Code for resizing the UIImage
extension UIImage {
func scalePreservingAspectRatio(targetSize: CGSize) -> UIImage {
// Determine the scale factor that preserves aspect ratio
let widthRatio = targetSize.width / size.width
let heightRatio = targetSize.height / size.height
let scaleFactor = min(widthRatio, heightRatio)
// Compute the new image size that preserves aspect ratio
let scaledImageSize = CGSize(
width: size.width * scaleFactor,
height: size.height * scaleFactor
)
// Draw and return the resized UIImage
let renderer = UIGraphicsImageRenderer(
size: scaledImageSize
)
let scaledImage = renderer.image { _ in
self.draw(in: CGRect(
origin: .zero,
size: scaledImageSize
))
}
return scaledImage
}
}
Here what worked for me. It basically returns and displays all categories and their respective items from the specified list.
CategoryServiceModel
func loadCategories(for list: List) -> ([Category], [Item]) {
let request = NSFetchRequest<Category>(entityName: "Category")
let predicate = NSPredicate(format: "ANY items IN %@", list.items ?? [])
request.predicate = predicate
let sort = NSSortDescriptor(keyPath: \Category.name, ascending: true)
request.sortDescriptors = [sort]
do {
let categories = try manager.context.fetch(request)
var categories: [Category] = []
var items: [Item] = []
for category in categories {
categories.append(category)
if let items = category.items?.allObjects as? [Item] {
let filteredItems = items.filter { $0.list == list }
items.append(contentsOf: filteredItems)
}
}
return (categories, items)
} catch let error {
print("Error fetching categories. \(error.localizedDescription)")
return ([], [])
}
}
SwiftUI
List {
ForEach(categorySM.categories) { category in
Section(header:Text(category.name ?? "")){
ForEach(category.items?.allObjects as? [Item] ?? []) { item in
Text("\(item.name ?? "")")
}
}
}
}
.onAppear{
categorySM.loadCategories(for: selectedList)
}
It's hard to tell if Apple uses Core Data and CloudKit, but my guess is that they do. I'm not really sure what you mean by "reordering items" but I can tell you that using Core Data and CloudKit is very straightforward, just search for NSPersistentCloudKitContainer for using CloudKit and for Core Data I highly recommend you to watch the following tutorials; they're great.
Swiftful Thinking:
https://www.youtube.com/watch?v=BPQkpxtgalY
Not sure why I keep posting in this forum. It's useless.
Your issue sounds more like a schema issue. Did you recently add or removed Entities or attributes to your Core Data model? If you did, you must deploy schema changes in the CloudKit console, by going to CloudKit Console -> Development Database -> Deploy Schema Changes.
If you haven't made any changes to your Core Data model, make sure that all of your Entities and attributes are present in the Development database under Schema -> Record Types. If you're missing Entities or attributes, go to your app and enter some data for the missing entities or attributes, this will create them in the development database; make sure to have CloudKit on in your app when you enter the data. I hope it makes sense.
I guess my issue is that I'm not seeing the whole picture and it feels like I'm writing a lot more code and making things more complicated for little benefit; at least I don't see the benefit since I don't unitest and I'm a single developer writing small applications. For instance, if we use Example 1 and Example 2 in a SwiftUI view and compare the code, with and without Dependency Injection, Example 2 uses a lot more code and looks more complicated.
Again, I guess I still don't see the benefit of the extra complexity.
Example 1: Without Dependency Injection
class Stereo{
func volume(){
print("Adjusting volume...")
}
}
class Car{
var stereo = Stereo()
func adjustVolume(){
stereo.volume()
}
}
struct ContentView: View {
var car = Car()
var body: some View {
Button("Adjust Volume"){
car.adjustVolume()
}
}
}
Calling the CarView in other views
CarView()
Example 2: With Dependency Injection
class Stereo{
func volume(){
print("Adjusting volume...")
}
}
class Car{
var stereo: Stereo
init(stereo: Stereo){
self.stereo = stereo
}
func adjustVolume(){
stereo.volume()
}
}
struct CarView: View {
var car: Car
init(car: Car){
self.car = car
}
var body: some View {
Button("Adjust Volume"){
car.adjustVolume()
}
}
}
####Calling the CarView in other views.
CarView(car: Car(stereo: Stereo()))
A little late but the issue was that I didn't push the schema from development to production as noted by @deeje.
Yes, everything occurs in the app. The user doesn't have to do anything except update the app to the new version, otherwise, he can continue using the app with the old file. I really like the idea of having a flag in the JSON file.
Something like, if flagInJSONFile != lastSavedInUserDefaultsVariable{ updateFile()}
In the JSON file I could name the flag, update2023 then overwrite it next year or the next update to something like update2024.
if update2023 != update2024{updateFile()}
Thanks a lot for the good ideas.
Yes, the file is included in the app update, it's not in the server.
The user doesn't need to know as long as they get the latest file released in the latest app version.
I have a JSON that is included in the app. At the first app launch, it will be read and load all of its content to Core Data. Now, if later on the JSON file gets new info and needs to update Core Data I would like to reload the entire JSON file. And again, this will be done in a new app release.
I can do it the way I showed it in my original post or use new variables to check against every time there is an update, but I don't really like it. I was wondering if there is a way to overwrite the shouldLoad variable to true to make it look as if it was the first app launch even for existing users. I hope it starts making sense. Thanks a lot for following along.