I’m attempting to add local persistence to items fetched from CloudKit using Core Data in my app, but I’m not achieving the desired behavior. Here’s what I aim to accomplish:
- User opens the app.
- Fetch items from CloudKit.
- Display the fetched items.
- Save the fetched items to Core Data.
- User closes and reopens the app.
- Display the saved items from Core Data, as before.
- Sync with CloudKit and smoothly update the display if there are any changes.
My approach below is wrong because, as a SO user mentioned: “In SwiftUI the View structs are the view model already you shouldn’t try to use objects or will run into major issues. StateObject is for something else.” I am seeking guidance on the correct way to handle this.
class CloudKitViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var selectedItem: Item?
private var database = CKContainer.default().publicCloudDatabase
private let persistentContainer: NSPersistentContainer
init() {
let container = CKContainer(identifier: "iCloud.com.user.my.project")
self.database = container.publicCloudDatabase
// Set up Core Data
persistentContainer = NSPersistentContainer(name: "Model")
persistentContainer.loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to load persistent stores: (error)")
}
}
loadSavedItems()
fetchItems()
}
func fetchItems() {
let query = CKQuery(recordType: "Item", predicate: NSPredicate(value: true))
database.perform(query, inZoneWith: nil) { records, error in
if let records = records {
DispatchQueue.main.async {
self.items = records.map { record in
Item(
id: record.recordID,
city: record["City"] as? String ?? ""
)
}
self.saveItemsToCoreData()
}
} else if let error = error {
print("Error fetching records: (error.localizedDescription)")
}
}
}
private func loadSavedItems() {
let context = persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Model> = Model.fetchRequest()
do {
let itemEntities = try context.fetch(fetchRequest)
self.items = itemEntities.map { entity in
Item(
id: CKRecord.ID(recordName: entity.id ?? ""),
city: entity.city ?? ""
)
}
} catch {
print("Failed to fetch items from Core Data: (error)")
}
}
private func saveItemsToCoreData() {
let context = persistentContainer.viewContext
// Remove old data
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Model.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try context.execute(deleteRequest)
} catch {
print("Failed to delete old items from Core Data: (error)")
}
// Save new data
items.forEach { item in
let entity = Model(context: context)
entity.id = item.id.recordName
entity.city = item.city
}
do {
try context.save()
} catch {
print("Failed to save items to Core Data: (error)")
}
}
}
Here’s how items are displayed to the user on startup:
struct MyView: View {
@StateObject private var viewModel = CloudKitViewModel()
var body: some View {
NavigationView {
VStack {
List {
Section {
ForEach(viewModel.items) { item in
ItemRow(
item: item
)
}
}
}
}
}
}
}
struct Item: Identifiable {
var id: CKRecord.ID
var city: String
}
struct ItemRow: View {
var item: Item
var body: some View {
HStack {
Text(item.city)
}
}
}
3