1

My app may create / delete thousands of managed objects while running. I have used secondary NSManagedObjectContexts(MOCs) with NSPrivateQueueConcurrencyType and NSOperations to make the app more responsive and most parts work well. But when I pressed ⌘Q and if the number of unsaved objects are large, the app hangs for quite a while before the window closes (the beach ball keeps on rotating...).

How to make the window disappear immediately, before the save of the MOC? I tried to insert window.close() in applicationShouldTerminate in the AppDelegate, but it has no effect.

My code for deletion is nothing special, except the hierachy is really large. Something like

let items = self.items as! Set<Item>
Group.removeItems(items)
for i in items {
   self.managedObjectContext?.deleteObject(i)
}

Item is a hierarchic entity. Group has a one-to-many relationship to items. The removeItems is generated by CoreData with @NSManaged.

Many thanks.


Updates

I tried the following code, the save still blocks the UI.

@IBAction func quit(sender: AnyObject) {
    NSRunningApplication.currentApplication().hide()
    NSApp.terminate(sender)
}

func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply 
{
    let op = NSBlockOperation { () -> Void in
        do {
            try self.managedObjectContext.save()
        } catch {
            print("error")
        }
        NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
            NSApp.replyToApplicationShouldTerminate(true)
        })
    }

    op.start()
    return .TerminateLater
}

This doesn't make the window close first, when the amount of created / deleted managed objects is large.

Then I changed to the following, as suggested by @bteapot. Still has no effect. The window still won't close immediately.

@IBAction func quit(sender: AnyObject) {
    NSRunningApplication.currentApplication().hide()
    NSApp.terminate(sender)
}

func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply {

    let op = NSBlockOperation { () -> Void in
        self.managedObjectContext.performBlock({ () -> Void in
            do {
                try self.managedObjectContext.save()
            } catch {
                print("errr")
            }
        })

        NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
            NSApp.replyToApplicationShouldTerminate(true)
        })
    }

    dispatch_async ( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
    {() -> Void in
        op.start()
    })

    return .TerminateLater
}

Finally I sort of solved the problem, though the UI is still blocked sometimes, even with the same test data.

The approach used can be found here: https://blog.codecentric.de/en/2014/11/concurrency-coredata/ , Core Data background context best practice , https://www.cocoanetics.com/2012/07/multi-context-coredata/

First I made a backgroundMOC with .PrivateQueueConcurrencyType

lazy var backgroundMOC : NSManagedObjectContext = {
    let coordinator = self.persistentStoreCoordinator
    let moc = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
    moc.persistentStoreCoordinator = coordinator
    moc.undoManager = nil
    return moc
}()

Then made it prent of the original moc.

lazy var managedObjectContext: NSManagedObjectContext = {
    var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
    // managedObjectContext.persistentStoreCoordinator = coordinator

    managedObjectContext.parentContext = self.backgroundMOC
    managedObjectContext.undoManager = nil

    return managedObjectContext
}()

Two methods for the save.

func saveBackgroundMOC() {
    self.backgroundMOC.performBlock { () -> Void in
        do {
            try self.backgroundMOC.save()
            NSApp.replyToApplicationShouldTerminate(true)
        } catch {
            print("save error: bg")
        }
    }
}

func saveMainMOC() {
    self.managedObjectContext.performBlock { () -> Void in
        do {
            try self.managedObjectContext.save()
            self.saveBackgroundMOC()
        } catch {
            print("save error")
        }
    }
}

Change the applicationShouldTerminate() to

func applicationShouldTerminate(sender: NSApplication) -> NSApplicationTerminateReply {
    if !managedObjectContext.commitEditing() {
        NSLog("\(NSStringFromClass(self.dynamicType)) unable to commit editing to terminate")
        return .TerminateCancel
    }

    if !managedObjectContext.hasChanges {
        return .TerminateNow
    }

    saveMainMOC()
    return .TerminateLater

}

The reason it was so slow was I was using NSXMLStoreType instead of NSSQLiteStoreType.

Community
  • 1
  • 1
LShi
  • 1,370
  • 12
  • 28
  • 1
    Call `[[NSRunningApplication currentApplication] hide];`, return `NSTerminateLater` from `applicationShouldTerminate:` and let the app finish what it should. Then call `replyToApplicationShouldTerminate:YES` – bteapot Dec 23 '15 at 12:11
  • So should I do the CoreData MOC save and call `replyToApplicationShouldTerminate:YES` in a `NSOperation` initiated in `applicationShouldTerminate:` and then return `NSTerminateLater`? – LShi Dec 23 '15 at 13:25
  • No, the sequence is: 1). User commands application to quit. 2). App delegate receives `applicationShouldTerminate:` call. 3). In that method you have to a). initiate save process in background thread and b). return `NSTerminateLater`. 4). When the save process finishes you should call `replyToApplicationShouldTerminate:YES` on main thread. – bteapot Dec 23 '15 at 13:32
  • Still no luck. I added the code in the question. – LShi Dec 23 '15 at 14:02
  • Just wrap `op.start()` in `dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ });`, and `self.managedObjectContext.save()` in `self.managedObjectContext.performBlock`. – bteapot Dec 23 '15 at 15:10
  • It's the same. I updated the question. Perhaps I should just keep the number of objects to save small? When the number of objects to save is really large, it seems it takes forever to save. When the number is small, everything is fine, even with the Xcode generated `applicationShouldTerminate:`. – LShi Dec 24 '15 at 01:44
  • I think it's not a very good idea – to process such a large amounts of data in the main thread. Better keep it only for user's changes, and do all the heavy stuff in private context. – bteapot Dec 24 '15 at 05:43

1 Answers1

0

Quitting an application might take a while since it will first empty the processes in queue. Do you want immediate quit discarding everything in the Parent or children MOCs? But this will result in data loss.

If you have multi window application then, then close the window only but not quit the app.

Also thousands of entry should not take longer than 5 seconds to get processed and saved, if you have managed it properly. There could be some loopholes in your code, try to optimize using Instruments, CoreData profiler tool that would help you to understand the amount of time it is eating up.

To hide the window you can use the below, and in background all the coredata processing will happen, and once everything is done the app will terminate.

[self.window orderOut:nil];
Anoop Vaidya
  • 45,475
  • 15
  • 105
  • 134
  • The data changes shouldn't be discarded. Even I add the `windows.close()` before `moc.save()`, the window will still be there until the save finishes. – LShi Dec 23 '15 at 11:54
  • It would be nice if you can show the codes how you are writing and saving. Also try `CoreData Profiler`. – Anoop Vaidya Dec 23 '15 at 12:22
  • I used the Xcode generated `applicationShouldTerminate:` in `AppDelegate` for the save. I think the time it takes is reasonable. I want to hide the window immediately so it feels fast. – LShi Dec 23 '15 at 14:11
  • If you want to hide the window then use `[self.window orderOut:nil];` . Also updated the answer. – Anoop Vaidya Dec 23 '15 at 14:37
  • Thanks. But I tried it. The window won't hide until the save finishes, even I insert the `orderOut()` call **before** the `moc.save()`. – LShi Dec 24 '15 at 01:32