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.
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];
Related
When I use UICollectionView with UICollectionViewFlowLayout set. And then try to apply snapshots of datasource via
// load initial data
reloadDataSource()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
self.reloadDataSource(animating: true)
}
I am getting crash on second snapshot applied after 3 seconds delay.
The crash is happening only when animating: true
If I set animating to false then there is no crash but the collection view if left empty.
Here is this mathod applying data source
extension CollectionViewController {
func reloadDataSource(animating: Bool = false) {
print("reloading data source with snapshot -> \(snapshot.numberOfItems)")
self.dataSource.apply(self.snapshot, animatingDifferences: animating) {
print("applying snapshot completed!")
}
}
}
Data source is just
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: cellProvider)
Full project you can play (may changes over time): https://github.com/michzio/SwifUICollectionView
Update
I've tried to simplify example and do something like this and it doesn't work correctly. It seems that moving .apply() to background queue, other queue causes empty data in collectionview
func reloadDataSource(animating: Bool = false) {
print("reloading data source with snapshot -> \(snapshot.numberOfItems)")
diffQueue.async {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.categories])
snapshot.appendItems(Item.categoryItems)
self.dataSource.apply(snapshot, animatingDifferences: animating) {
print("applying snapshot completed!")
}
}
}
Ok it seems that i found the reason of all my errors with updating datasource by applying new snapshots
This lazy var dataSource is causing errors:
private(set) lazy var dataSource: UICollectionViewDiffableDataSource<Section, Item> = {
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: cellProvider)
//dataSource.supplementaryViewProvider = supplementaryViewProvider
return dataSource
}()
I've changed it to
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
and after configureCollectionView in viewDidLoad()
now I am calling configureDataSource() that does what was in lazy var initializer.
Is there a way to wait for the Box API to finish all requests? So for example, if I make a folder item request, I would like to have my program wait for the completion handler to finish before moving on.
As an example:
BOXContentClient *contentClient = [BOXContentClient defaultClient];
BOXFolderItemsRequest *listAllInRoot = [contentClient folderItemsRequestWithID:BOXAPIFolderIDRoot];
[listAllInRoot performRequestWithCompletion:^(NSArray *items, NSError *error) {
//Do something with the results here
}
// Wait here for the completion handler to finish before moving on
I had a go at using an NSCondition, however I am wondering if there's a better way.
(Swift 5.x) You can use this code :
var a: [String:Any]
func myFunction(completion:#escaping (Bool) -> () ) {
DispatchQueue.main.async {
// For example your action on a
}
}
myFunction { (status) in
if status {
print(a!)
}
}
I created a sandboxed user. Signed out of app store on my device, and used my test user to make a test purchase. All good.
Now I signed back into my real account. Every time I open the app, it asks for my test user's credentials immediately.
I tried deleting my test user. I tried deleting the app. I tried signing out of app store. It's STILL asking for this test user's credentials on every app launch on my device. How do I start fresh?
here is my code, although I'm not sure it's really related to my issue
override func viewDidLoad() {
// storekit delegation
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
self.getProductInfo()
}
func getProductInfo(){
if SKPaymentQueue.canMakePayments() {
let request = SKProductsRequest(productIdentifiers: NSSet(object: self.productID))
request.delegate = self
request.start()
}
// else {
// please enable in app purchases
// }
}
Delegate methods
func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
var products = response.products
if products.count != 0 {
self.product = products[0] as? SKProduct
}
}
func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) {
if queue.transactions.count != 0 {
if let purchase = queue.transactions[0] as? SKPaymentTransaction {
if purchase.payment.productIdentifier == self.productID {
println("you bought it already")
}
}
}
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction in transactions as [SKPaymentTransaction] {
switch transaction.transactionState {
case SKPaymentTransactionState.Purchased:
// self.unlockFeature()
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
case SKPaymentTransactionState.Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
default:
break
}
}
}
The activity indicator starts, but does not stop when the hide function is called. I've tried putting the hide function in various places, and it still does not hide.
Hide activity indicator: Q0ViewController().hideActivityIndicator(self.view)
I'm using the swift utility function found here:
https://github.com/erangaeb/dev-notes/blob/master/swift/ViewControllerUtils.swift
Start activity indicator
override func viewDidLoad() {
super.viewDidLoad()
Q0ViewController().showActivityIndicator(self.view)
self.locationManager.delegate = self //location manager start
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
Hide activity indicator after query:
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
CLGeocoder().reverseGeocodeLocation(manager.location, completionHandler: { (placemarks, error) -> Void in
if (error != nil) {
println("Error:" + error.localizedDescription)
//return
}
if placemarks.count > 0 {
let pm = placemarks[0] as CLPlacemark
self.displayLocationInfo(pm)
currentLoc = manager.location
currentLocGeoPoint = PFGeoPoint(location:currentLoc)
var query = PFQuery(className:"test10000")
query.whereKey("RestaurantLoc", nearGeoPoint:currentLocGeoPoint, withinMiles:100) //filter by miles
query.limit = 1000 //limit number of results
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if objects != nil {
unfilteredRestaurantArray = objects
originalUnfilteredArray = objects
println(objects)
} else {
println("error: \(error)")
}
Q0ViewController().hideActivityIndicator(self.view) //HIDE
}
} else {
println("error: \(error)")
}
})
}
It is not an issue with the main queue as dispatch_after(DISPATCH_TIME_NOW, dispatch_get_main_queue(), { ()->() in does not resolve the issue.
Looks like you're creating a new instance of the "Q0ViewController" each time.
Instead I would suggest retaining the initial instance as a property on your class:
// As a variable on the class instance
let myViewController = Q0ViewController()
// Initially show the activity indicator
self.myViewController.showActivityIndicator(self.view)
// Hide the activity indicator
self.myViewController.hideActivityIndicator(self.view)
Hopefully this helps!
Similar to what Joshua suggested, just replaced:
Q0ViewController().showActivityIndicator(self.view)
and
Q0ViewController().hideActivityIndicator(self.view)
To:
self.showActivityIndicator(self.view)
and
self.hideActivityIndicator(self.view)
My scenerio:
I asynchronously call a downloader object function to try to download a file.
Within the downloader's function, it asynchronously initiates a connection for downloading.
Even so, I cannot interact with my UI elements.
For example, I cannot edit my text field while download is in progress.
I want to be able to interact with my UI elements while downloading.
Where did I miss? What should I do? I truly appreciate your help!
ViewController's snippet for async download
//relevant UI elements I am referring to
#IBOutlet var renderButton: UIButton!
#IBOutlet var urlField: UITextField!
func handleOpenURL(url: NSURL){
var downloader: aDownloader = aDownloader(url: url)
//I try to put download to background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {() -> Void in
//This function initiates a connection call in an attempt to download the file
downloader.executeConnection({
(dataURL:NSURL!,error:NSError!) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.imageView.image = UIImage(data:NSData(contentsOfURL: url)!)
})
//rest are nonsense
}, progress: { (percentage:Double) -> Void in
//nonsense
}
)
})
}
aDownloader.swift 's snippet for relevant code
class aDownloader: NSObject, allOtherProtocols {
unowned var url: NSURL
//this block reports the progress as a % number
var progressBlock : ((Double)->Void)?
//this block will return either the destinationFileURL after download finishes,
//or return error of any sort
var dataBlock: ((NSURL!,NSError!)->Void)?
init(url: NSURL) {
self.url = url
}
func executeConnection(received:(NSURL!,NSError!)->Void, progress:(Double)->Void){
var request : NSURLRequest = NSURLRequest(URL: self.url,
cachePolicy: .ReloadIgnoringLocalCacheData,
timeoutInterval: 60.0)
//I attempt to async-ly download the file here
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
var connectionManager : NSURLConnection? = NSURLConnection(request:request,
delegate: self,
startImmediately: false)
connectionManager?.scheduleInRunLoop(NSRunLoop.mainRunLoop(),
forMode: NSRunLoopCommonModes)
connectionManager?.start()
})
self.dataBlock = received
self.progressBlock = progress
}
//nonsense
}
Two things.
Most importantly, you are still configuring your configuration manager to run on the main loop:
connectionManager?.scheduleInRunLoop(NSRunLoop.mainRunLoop(),
forMode: NSRunLoopCommonModes)
You will probably find it easier to just use NSURLConnection's class method
sendAsynchronousRequest:queue:completionHandler: instead of manually starting the request in a background thread.
Also, it is a bad idea to update UI elements from a background thread:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.imageView.image = UIImage(data:NSData(contentsOfURL: url)!)
})
Instead, you should load the image into a temporary variable in the background and then perform the actual assignment back on the main thread.