Search code examples
iosswiftswiftuicloudkit

Why does creating a new CloudKit item with viewContext infinitely refresh views?


I am creating an app with the stored data hosted in CloudKit. When I perform a normal swipe to delete action on any of these list items, the deleteAlert() displays (as it should). However, as long as the alert is displayed, the code continuously loops and creates an infinite number of blank Category values, adding them to the list. At the same time, the alert doesn't allow you to tap on any of the buttons normally, but if you swipe your finger across the button, you can feel lots of short haptic feedback pulses (I suspect it's also looping through creating many overlapping alerts).

import SwiftUI

struct CategoryListView: View {
    
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(entity: Category.entity(), sortDescriptors: [], animation: .default)
    private var categories: FetchedResults<Category>
    
    // Passed value
    var accountSelection: String
    
    @State private var deletingItem = false
    @State private var deleteIndexSet: IndexSet?
    @State private var showingAddView = false
        
    var body: some View {
        
        List {

            ForEach(categories) { category in
                HStack {
                    Button(action: {
                        self.showingAddView.toggle()
                    }) {
                        Text("\(category.name ?? "")")
                    }
                }
                    .alert(isPresented: $deletingItem, content: deleteAlert)
            }
                .onDelete { indexSet in
                    self.deletingItem = true
                    self.deleteIndexSet = indexSet
                }
        }
    }

    func deleteAlert() -> Alert {
        
        var deletedCategory = Category(context: viewContext) // removing this line causes everything to work properly

        try! deletedCategory = categories[deleteIndexSet?.first ?? 0]
        return Alert(
            title:           Text("Delete \(deletedCategory.name ?? "nil")?"),
            message:         Text("Deleting \(deletedCategory.name ?? "nil") will not remove all entries from that category."), // TODO: make it so that if entry does not have a category, add it to a "miscellaneous" or "other" category
            primaryButton:   .cancel(),
            secondaryButton: .destructive(Text("Delete"), action: {print("")})
        )
    }
}


Solution

  • The reason the view is constantly refreshing boils down to the way @FetchRequest works (the array of CloudKit fetched items). Whenever the value of categories changes (as with @State and @ObservedObject etc.), the view it is attached to refreshes. The following loop will repeat endlessly in this case:

    1. Inside of deleteAlert(), var deletedCategory = Category(context: viewContext) creates a new Category and immediately adds it to the current context (the list).

    2. This causes the view to refresh as previously mentioned.

    3. The value of $deletingItem is still true, so another alert will display.

    4. When an alert is displayed, it triggers the code inside of deleteAlert().

    5. Rinse and repeat steps 1-4 infinitely.

    TL;DR Don't create ManagedObjects/ObservableObjects in a View/Body (as @lorem ipsum also pointed out).