Search code examples
swiftuicloudkitnsfetchrequest

FetchRequest for 12000 entries extremely slow on macOS app but not iOS app - despite code being the same (SwiftUI on macOS Big Sur / iOS 14)


Edit (original text below could be ignored)

I did some further testing using only a manual fetch (which gets executed only once). It seems it's just the fetch request (for only 12000 entities) itself which is horribly slow. Nevertheless, I wonder why I should get this issue only for macOS and not iOS given it's the same code?

==========================================

Original post

I am developing a simple SwiftUI app for macOS Big Sur/iOS 14 which synchronizes "Card"-entities (nothing fancy; around 20 properties and 7 relationships) via CloudKit. Everything works fine, however, after importing (only) 12000 entries (from a decoded JSON file), the macOS app (but not the iOS app) became unusable, i.e. freezes/has a very high CPU usage. Strangely, this issue also occurs if no data is shown on the UI, e.g. with the following simple view:

MainView & CardList

struct MainView: View {    
    @ObservedObject var model: MainViewModel
    @Environment(\.colorScheme) var colorScheme
  
    var body: some View {
        CardList(predicate: Card.predicate(searchText: self.model.searchText,
                                                        pinned: self.model.filterPinned,
                                                        priority: self.model.getPrioritiesAsArray(),
                                                        subjects: self.model.selectedSubjects,
                                                        bundles: self.model.selectedBundles),
                              sortDescriptor: EntrySort(sortType: self.model.sortType, sortOrder:self.model.sortOrder).sortDescriptor,
                              model: self.model)
    }
    
}

struct CardList: View {    
    @ObservedObject var model: MainViewModel
    @Environment(\.colorScheme) var colorScheme
    
    @Environment(\.managedObjectContext)
    var context: NSManagedObjectContext
    @FetchRequest(
        entity: Card.entity(),
        sortDescriptors: [
            NSSortDescriptor(keyPath: \Card.id, ascending: false)
        ]
    )
    private var result: FetchedResults<Card>
    
    init(predicate: NSPredicate?,
         sortDescriptor: NSSortDescriptor,
         model: MainViewModel) {
        
        let fetchRequest = NSFetchRequest<Card>(entityName: "Card")
        fetchRequest.sortDescriptors = [sortDescriptor]
        
        if let predicate = predicate {
            fetchRequest.predicate = predicate
        }
        _result = FetchRequest(fetchRequest: fetchRequest)
        
        self.model = model
    }
  
    var body: some View {        
        Text("TEST")        
    }

}

The "MainViewModel" look as follows (abbreviated for better legibility):

MainViewModel

class MainViewModel: ObservableObject {    
    @Published var searchText: String = ""
    @Published var selectedSubjects: [Subject]?
    @Published var selectedBundles: [Bundle]?
    @Published var selectedTags: [Tag]?
    
    @Published var filterPinned: Bool? = nil
    @Published var filterPriorityVeryHigh: Bool = false
    @Published var filterPriorityHigh: Bool = false
    @Published var filterPriorityNormal: Bool = false
    @Published var filterPriorityLow: Bool = false
    
    func getPrioritiesAsArray() -> [CardPriority] {
        var res: [CardPriority] = []
        if self.filterPriorityLow { res.append(CardPriority.Low)}
        if self.filterPriorityNormal { res.append(CardPriority.Normal)}
        if self.filterPriorityHigh { res.append(CardPriority.High)}
        if self.filterPriorityVeryHigh { res.append(CardPriority.VeryHigh)}
        return res
    }
    @Published var sortType = SortType.dateCreated
    @Published var sortOrder = SortOrder.ascending
}

AppDelegate

import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    
    var window: NSWindow!
    var coreDataStack = CoreDataStack(modelName: "[Name of Model]")
    var mainViewModel = MainViewModel() 
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {  
        coreDataStack.viewContext.automaticallyMergesChangesFromParent = true
        let mainView = MainView(model: mainViewModel)
            .environment(\.managedObjectContext, coreDataStack.viewContext) 
// ...
}

Question

The very same data was successfully synchronized to the iOS version of the app and can be displayed there without any performance issues (using the same controls, namely a LazyVStack). However, the macOS app became totally unusable because of the constant high CPU usage. Also, I cannot see anything that would constantly change a variable and thereby trigger a constant refresh of the displayed entities (i.e. trigger a new fetch request).

Does anybody have an idea why this issue occurs?

Thanks in advance for any advise!

Sebastian

※ One thing I noticed is that Xcode floods the console with the warning "NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release" because I am still using some [String]-Transformable-Properties on the Card entity. I will address this issue later, but since this warning seems to be displayed constantly, I wonder if @FetchRequest constantly fetches/evaluates all entities? Further, I assume the vast amount of warnings displayed should only have an influence on Xcode's CPU usage?


Solution

  • I solved this issue; it seems that it was indeed the warning "'NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release" which caused this spike in CPU usage. After setting "NSSecureUnarchiveFromDataTransformer" to all [String]-Transformable-Properties of all database entities, the issue was gone.