Swift UI list not updating on Cloudkit sync

Viewed 408

I am trying to create a simple application that leverages CloudKit to sync between multiple iOS devices (via iCloud) and possibly macOS devices (still via iCloud).

Problem: I have a core data entity which seems to work great locally in my app. When I switch to using Cloudkit I am unable to see changes on another device without closing/reopening the app.

I am using the Cloudkit template in Xcode with the SwiftUI lifecycle. ie, PersistenceController and managed object context.

I think this is a view not refreshing issue, but am not 100% sure. Once the app on a different device is closed and reopened then it loads the new data successfully. This applies to additions and deletes.

Testing: I have tested this using Testflight as well as running two simulators locally.

Code:

PersistenceController.swift


import CoreData

struct PersistenceController {
    static var shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        for i in 0..<10 {
            let newItem = Card(context: viewContext)
            newItem.cardValue = String(i)
            newItem.cardOrder = Int32(i)
        }
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()
    
    
    //Is this needed?
    lazy var updateContext: NSManagedObjectContext = {
        let _updateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        _updateContext.parent = PersistenceController.shared.updateContext
        return _updateContext
    }()

    let container: NSPersistentCloudKitContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "Cards")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })

        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
    
}

ContentView.swift


import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    
    @FetchRequest(entity: Card.entity(),
                  sortDescriptors: [NSSortDescriptor(key: "cardOrder", ascending: true)])
    var cards: FetchedResults<Card>
    
    
    var body: some View {
        
        NavigationView {
            VStack {
                Text("Scrum +")
                    .font(.largeTitle)
                    .fontWeight(.regular)
                
                Spacer()
                List(cards, id: \.id)  {card in
                        NavigationLink(destination: PointRow(card: card)) {
                                PointRow(card: card)
                        }
                }
                
                Spacer()
                NavigationLink(destination: SettingsView().environment(\.managedObjectContext, self.managedObjectContext)
) {
                    Text("Card Settings")
                }
                .padding(.bottom)
                
            }
        }
    }
}

SettingsView.swift (where saving/deleting/reordering happens)


import SwiftUI

struct SettingsView: View {
    
    
    @State private var editMode = EditMode.inactive
    
    @State private var showModal = false
    
    @Environment(\.managedObjectContext) var managedObjectContext
    
    @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "cardOrder", ascending: true)])
    var cards: FetchedResults<Card>
    
    
    var body: some View {
        VStack {
            Text("Card Values")
                .font(.largeTitle)
            HStack {
                EditButton()
                    .padding(.leading)
                Spacer()
                addButton
                    .padding(.trailing)
            }
            .padding(.vertical)
            List {
                ForEach(cards) {
                    item in
                    Text(item.cardValue!)
                }
                .onDelete(perform: onDelete)
                .onMove(perform: onMove)
                .environment(\.editMode, $editMode)
            }
            
            .sheet(isPresented: $showModal) {
                //Show the view to add a new card.
                SettingsModal(showModal: self.$showModal, numberOfCards: cards.count).environment(\.managedObjectContext, self.managedObjectContext)
            }
        }
    }
    
    
    private var addButton: some View {
        switch editMode {
        case .inactive:
            return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
        default:
            return AnyView(EmptyView())
        }
    }
    
    
    
    func onAdd() {
        showModal = true
    }
    
    
    func onDelete(indexSet: IndexSet) {
        //        print("Deleting card at index -> " + indexSet.first)
        
        let cardToDelete = self.cards[indexSet.first!]
        self.managedObjectContext.delete(cardToDelete)
        
        do {
            try self.managedObjectContext.save()
        } catch {
            print(error)
        }
        
        reorder()
    }
    
    
    private func onMove(source: IndexSet, destination: Int) {
        
        //        pointCards.points.move(fromOffsets: source, toOffset: destination)
        
        //        source.
        
        let firstCard = self.cards[source.first!]
        
        //If there is a card located in the destination.
        if (destination < self.cards.count) {
            let secondCard = self.cards[destination]
            
            
            let tmp = Int(secondCard.cardOrder)
            
            //Increment all place holders from the destination on.
            for i in tmp..<self.cards.count {
                self.cards[i].cardOrder += 1
            }
        }
        
        firstCard.cardOrder = Int32(destination)
        
        do {
            try self.managedObjectContext.save()
        } catch {
            print(error)
        }
        
        self.managedObjectContext.refreshAllObjects()
        
        //Reorder just in case moved to the end.
        reorder()
    }
    
    
    private func reorder() {
        
        for i in 0..<cards.count {
            cards[i].cardOrder = Int32(i)
        }
        
        do {
            try self.managedObjectContext.save()
        } catch {
            print(error)
        }
    }
}

Screenshots of my Core Data model using Cloudkit and of the Entitlement selection. Capabilities

CoreData Properties

UPDATE: I have found the solution. The Persistent Controller should have the following three lines added to the init function:


        //Setup auto merge of Cloudkit data
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        //Set the Query generation to .current. for dynamically updating views from Cloudkit
        try? container.viewContext.setQueryGenerationFrom(.current)

Then this must be ran on real devices. For whatever reason I am unable to get this working in the simulator, but on real devices it syncs in about 30 seconds without closing the app.

Thanks everyone for your help!

0 Answers
Related