Cocoa/OSX - Strange behavior in NSSavePanel that not shows sub-itens - macos

I've a NSSavePanel instance with a strange behavior: whenever I open it and click on a directory's arrow (the little expand button) it shows an indeterminate loading icon on the left-bottom corner that never ends, and not shows the directory/file tree. An image can see as follow:
In that example, I've clicked in "workspace" directory. And the panel not shows the sub-itens. Even strange is that after I click it again (redrawing the directory) and then click again (re-open the directory), it properly shows all files.
My code is as follows:
// here, I'm creating a web service client, and then calling a method to download a report, and passing the same class as delegate
- (IBAction) generateReport:(id)sender {
// SOME STUFF HERE...
WSClient *client = [[[WSClient alloc] init] initWithDelegate:self];
[client GenerateReport:#"REPORT" withParams:params];
}
- (void) GenerateReport:(NSString *)from withParams:(NSDictionary *)parameters {
// SOME STUFF HERE...
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
dispatch_async(dispatch_get_main_queue(), ^(void) {
NSLog(#"GenerateReport: [success]");
[self.delegate successHandlerCallback:data from: from];
});
});
}
}];
// this is the callback
- (void) successHandlerCallback:(NSData *) data from: (NSString *) from {
NSString savePath = [savePath stringByReplacingOccurrencesOfString:#"file://" withString:#""];
NSString *filePath = [NSString stringWithFormat:#"%#", savePath];
[data writeToFile:filePath atomically:YES];
}
// and this is to build a panel to let user chose the directory to save the file
- (NSURL *) getDirectoryPath {
NSSavePanel *panel = [NSSavePanel savePanel];
[panel setNameFieldStringValue:[self getDefaultFileName]];
[panel setDirectoryURL:[NSURL fileURLWithPath:[[NSString alloc] initWithFormat:#"%#%#%#", #"/Users/", NSUserName(), #"/Downloads"]]];
if ([panel runModal] != NSFileHandlingPanelOKButton) return nil;
return [panel URL];
}
Can someone give a hint on where I'm missing?
UPDATE: To me, it seens to be something related with dispatch_async!
Thanks in advance!

Actually, this is a bad interaction between NSSavePanel and the Grand Central Dispatch main queue and/or +[NSOperationQueue mainQueue]. You can reproduce it with just this code:
dispatch_async(dispatch_get_main_queue(), ^{
[[NSSavePanel savePanel] runModal];
});
First, any time you perform GUI operations, you should do so on the main thread. (There are rare exceptions, but you should ignore them for now.) So, you were right to submit the work to the main queue if it was going to do something like open a file dialog.
Unfortunately, the main queue is a serial queue, meaning that it can only run one task at a time, and NSSavePanel submits some of its own work to the main queue. So, if you submit a task to the main queue and that task runs the save panel in a modal fashion, then it monopolizes the main queue until the save panel completes. But the save panel is relying on the ability to submit its own tasks to the main queue and have them run.
As far as I'm concerned, this is a bug in Cocoa. You should submit a bug report to Apple.
The correct solution is for NSSavePanel to submit any tasks it has to the main thread using a re-entrant mechanism like a run-loop source, -performSelectorOnMainThread:..., or CFRunLoopPerformBlock(). It needs to avoid using the main queue of GCD or NSOperationQueue.
Since you can't wait for Apple to fix this, the workaround is for you to do the same. Use one of the above mechanisms to submit your task that may run the save panel to the main queue.

I just found the way: I was making all the calls within the dispatch_async on the main thread queue. By the fact that the download is still running at the callback moment, it conflicted with the thread that opens the panel. I fixed all issues by just placing the correct lines, changing from:
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
dispatch_async(dispatch_get_main_queue(), ^(void) {
NSLog(#"GenerateReport: [success]");
[self.delegate successHandlerCallback:data from: from];
});
});
to:
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSLog(#"GenerateReport: [success]");
[self.delegate successHandlerCallback:data from: from];
});
and in the callback, just updating the fields. In the end, I found that all of that were a main/background thread misunderstand by me.

Related

Why is FSEvent stream not giving me any events?

I'm trying to monitor a directory and get notified when files are added/removed/renamed. I'm using the CDEvents Objective-C wrapper. Here's the code I'm using:
self.events = [[CDEvents alloc] initWithURLs:#[self.programsFolder]
delegate:self
onRunLoop:[NSRunLoop currentRunLoop]
sinceEventIdentifier:kFSEventStreamEventIdSinceNow
notificationLantency:2
ignoreEventsFromSubDirs:NO
excludeURLs:nil
streamCreationFlags:kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagWatchRoot];
and the delegate method:
- (void)URLWatcher:(CDEvents *)URLWatcher eventOccurred:(CDEvent *)event {
NSLog(#"event apparantly happened");
// just redo everything for now
NSFileManager *manager = [NSFileManager defaultManager];
NSArray *startMenuFiles = [manager contentsOfDirectoryAtURL:_programsFolder
includingPropertiesForKeys:#[NSURLNameKey]
options:0
error:NULL];
NSMutableArray *newItems = [NSMutableArray new];
for (NSURL *startMenuFile in startMenuFiles) {
if (![[startMenuFile path] hasSuffix:#".plist"])
continue;
[newItems addObject:[[StartMenuItem alloc] initFromFile:startMenuFile]];
}
self.mutableItems = newItems;
}
But when I create files or folders in the folder it's monitoring, nothing happens. You can see I added an NSLog so I'll know when an event happens. Nothing is ever logged.
What could be the problem?
If you need more context, all of the code for this project is at http://github.com/vindo-app/vindo. Look in Code > Start > StartMenu.m in Xcode.
The problem was in initializing the FSEvents source. You have to provide a run loop to monitor for events. This code was being called from a thread other than the main thread, so [NSRunLoop currentRunLoop] was not the main thread's run loop. I changed it to [NSRunLoop mainRunLoop], and that fixed the problem.

How can an app send a (basic) Apple Event to itself in Cocoa?

I thought I was so slick in rewriting my handler for the "Open" menu command in the application delegate:
- (IBAction)openDocument:(id)sender {
NSOpenPanel * const panel = [NSOpenPanel openPanel];
panel.allowsMultipleSelection = YES;
panel.delegate = self;
[panel beginWithCompletionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) {
NSMutableArray * const paths = [NSMutableArray arrayWithCapacity:panel.URLs.count];
for (NSURL *file in panel.URLs) {
[paths addObject:file.path];
}
[self application:NSApp openFiles:paths];
}
}];
}
In the old code, looping through each file URL, I used to call my window creation code directly. Then I swapped out implementing the single-file -application:openFile: for its multi-file variant, and decided to reuse that code within -openDocument:.
I thought it was fine.
The single-file version return a BOOL indicating its success. For the multi-file version, you're supposed to call a special function of the application object.
I guess when you start up your app with some files, Cocoa's handler looks at them with the open-file AppleEvent, calls -application:openFiles:, waits for the response global to be set, then sends back and AppleEvent reply. When I reuse -application:openFiles: in -openDocument, I post to that global when the application object isn't expecting it.... Oh, crap.
Oh, I could submit the files for processing by putting them in an open-file AppleEvent and sending to self. I look around the docs, and I see everything except what I need: how to send an AppleEvent and the list of possible AppleEvents and their parameters.
Could someone here demostrate how to create and send an open-file AppleEvent? Or at least tell us where the table of event IDs and parameters are? (The various Cocoa guides on scripting refer to the event ID reference, but it's a dead link, leading to Apple's developer portal general/search page.)
I just guessed:
- (IBAction)openDocument:(id)sender {
NSOpenPanel * const panel = [NSOpenPanel openPanel];
panel.allowsMultipleSelection = YES;
panel.delegate = self.openPanelDelegate;
[panel beginWithCompletionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) {
NSAppleEventDescriptor * const fileList = [NSAppleEventDescriptor listDescriptor];
NSAppleEventDescriptor * const openEvent = [NSAppleEventDescriptor appleEventWithEventClass:kCoreEventClass eventID:kAEOpenDocuments targetDescriptor:nil returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
for (NSURL *file in panel.URLs) {
[fileList insertDescriptor:[NSAppleEventDescriptor descriptorWithDescriptorType:typeFileURL data:[[file absoluteString] dataUsingEncoding:NSUTF8StringEncoding]] atIndex:0];
}
[openEvent setParamDescriptor:fileList forKeyword:keyDirectObject];
[[NSAppleEventManager sharedAppleEventManager] dispatchRawAppleEvent:[openEvent aeDesc] withRawReply:(AppleEvent *)[[NSAppleEventDescriptor nullDescriptor] aeDesc] handlerRefCon:(SRefCon)0];
}
}];
}
looking at the docs for "NSAppleEventManager.h" and "NSAppleEventDescriptor.h" and half-remembering Mac programming materials I read in the 1990s.
I also poked around the Legacy/Retired Documents section of Apple's Developer Portal / Xcode.

Doing something after NSOpenPanel closes

I have an NSOpenPanel and I want to do some validation of the selection after the user has clicked OK. My code is simple:
void (^openPanelHandler)(NSInteger) = ^(NSInteger returnCode) {
if (returnCode == NSFileHandlingPanelOKButton) {
// do my validation
[self presentError:error]; // uh oh, something bad happened
}
}
[openPanel beginSheetModalForWindow:[self window]
completionHandler:openPanelHandler];
[self window] is an application-modal window. The panel opens as a sheet. So far so good.
Apple's docs say that the completion handler is supposed to be called "after the user has closed the panel." But in my case, it's called immediately upon the "OK/Cancel" button press, not upon the panel having closed. The effect of this is that the error alert opens above the open panel, not after the panel has closed. It still works, but it's not Mac-like.
What I would prefer is for the user to click OK, the open panel sheet to fold up, then the alert sheet to appear.
I guess I could present the alert using a delayed selector, but that seems like a hack.
Since the panel completion handler is invoked before the panel has effectively been closed,1 one solution is to observe NSWindowDidEndSheetNotification on your modal window:
Declare an instance variable/property in your class to hold the validation error;
Declare a method that will be executed when the panel is effectively closed. Define it so that if presents the error on the current window;
Have your class listen to NSWindowDidEndSheetNotification on [self window], executing the method declared above when the notification is sent;
In the panel completion handler, if the validation fails then assign the error to the instance variable/property declared above.
By doing this, the completion handler will only set the validation error. Soon after the handler is invoked, the open panel is closed and the notification will be sent to your object, which in turn presents the validation error that has been set by the completion handler.
For example:
In your class declaration, add:
#property (retain) NSError *validationError;
- (void)openPanelDidClose:(NSNotification *)notification;
In your class implementation, add:
#synthesize validationError;
- (void)dealloc {
[validationError release];
[super dealloc];
}
- (void)openPanelDidClose:(NSNotification *)notification {
if (self.validationError) [self presentError:error];
// or [[self window] presentError:error];
// Clear validationError so that further notifications
// don't show the error unless a new error has been set
self.validationError = nil;
// If [self window] presents other sheets, you don't
// want this method to be fired for them
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSWindowDidEndSheetNotification
object:[self window]];
}
// Assuming an action fires the open panel
- (IBAction)showOpenPanel:(id)sender {
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(openPanelDidClose:)
name:NSWindowDidEndSheetNotification
object:[self window]];
void (^openPanelHandler)(NSInteger) = ^(NSInteger returnCode) {
if (returnCode == NSFileHandlingPanelOKButton) {
// do my validation
// uh oh, something bad happened
self.validationError = error;
}
};
[openPanel beginSheetModalForWindow:[self window]
completionHandler:openPanelHandler];
}
1If you think this behaviour is wrong, consider filing a bug report with Apple. I don’t really remember whether an error should be presented over an open/save panel.

NSOutlineView not refreshing when objects added to managed object context from NSOperations

Background
Cocoa app using core data Two
processes - daemon and a main UI
Daemon constantly writing to a data store
UI process reads from same data
store
Columns in NSOutlineView in UI bound to
an NSTreeController
NSTreeControllers managedObjectContext is bound to
Application with key path of
delegate.interpretedMOC
NSTreeControllers entity is set to TrainingGroup (NSManagedObject subclass is called JGTrainingGroup)
What I want
When the UI is activated, the outline view should update with the latest data inserted by the daemon.
The Problem
Main Thread Approach
I fetch all the entities I'm interested in, then iterate over them, doing refreshObject:mergeChanges:YES. This works OK - the items get refreshed correctly. However, this is all running on the main thread, so the UI locks up for 10-20 seconds whilst it refreshes. Fine, so let's move these refreshes to NSOperations that run in the background instead.
NSOperation Multithreaded Approach
As soon as I move the refreshObject:mergeChanges: call into an NSOperation, the refresh no longer works. When I add logging messages, it's clear that the new objects are loaded in by the NSOperation subclass and refreshed. It seems that no matter what I do, the NSOutlineView won't refresh.
What I've tried
I've messed around with this for 2 days solid and tried everything I can think of.
Passing objectIDs to the NSOperation to refresh instead of an entity name.
Resetting the interpretedMOC at various points - after the data refresh and before the outline view reload.
I'd subclassed NSOutlineView. I discarded my subclass and set the view back to being an instance of NSOutlineView, just in case there was any funny goings on here.
Added a rearrangeObjects call to the NSTreeController before reloading the NSOutlineView data.
Made sure I had set the staleness interval to 0 on all managed object contexts I was using.
I've got a feeling this problem is somehow related to caching core data objects in memory. But I've totally exhausted all my ideas on how I get this to work.
I'd be eternally grateful to anyone who can shed any light as to why this might not be working.
Code
Main Thread Approach
// In App Delegate
-(void)applicationDidBecomeActive:(NSNotification *)notification {
// Delay to allow time for the daemon to save
[self performSelector:#selector(refreshTrainingEntriesAndGroups) withObject:nil afterDelay:3];
}
-(void)refreshTrainingEntriesAndGroups {
NSSet *allTrainingGroups = [[[NSApp delegate] interpretedMOC] fetchAllObjectsForEntityName:kTrainingGroup];
for(JGTrainingGroup *thisTrainingGroup in allTrainingGroups)
[interpretedMOC refreshObject:thisTrainingGroup mergeChanges:YES];
NSError *saveError = nil;
[interpretedMOC save:&saveError];
[windowController performSelectorOnMainThread:#selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// In window controller class
-(void)refreshTrainingView {
[trainingViewTreeController rearrangeObjects]; // Didn't really expect this to have any effect. And it didn't.
[trainingView reloadData];
}
NSOperation Multithreaded Approach
// In App Delegate (just the changed method)
-(void)refreshTrainingEntriesAndGroups {
JGRefreshEntityOperation *trainingGroupRefresh = [[JGRefreshEntityOperation alloc] initWithEntityName:kTrainingGroup];
NSOperationQueue *refreshQueue = [[NSOperationQueue alloc] init];
[refreshQueue setMaxConcurrentOperationCount:1];
[refreshQueue addOperation:trainingGroupRefresh];
while ([[refreshQueue operations] count] > 0) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
// At this point if we do a fetch of all training groups, it's got the new objects included. But they don't show up in the outline view.
[windowController performSelectorOnMainThread:#selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// JGRefreshEntityOperation.m
#implementation JGRefreshEntityOperation
#synthesize started;
#synthesize executing;
#synthesize paused;
#synthesize finished;
-(void)main {
[self startOperation];
NSSet *allEntities = [imoc fetchAllObjectsForEntityName:entityName];
for(id thisEntity in allEntities)
[imoc refreshObject:thisEntity mergeChanges:YES];
[self finishOperation];
}
-(void)startOperation {
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isStarted"];
[self setStarted:YES];
[self setExecuting:YES];
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isStarted"];
imoc = [[NSManagedObjectContext alloc] init];
[imoc setStalenessInterval:0];
[imoc setUndoManager:nil];
[imoc setPersistentStoreCoordinator:[[NSApp delegate] interpretedPSC]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:imoc];
}
-(void)finishOperation {
saveError = nil;
[imoc save:&saveError];
if (saveError) {
NSLog(#"Error saving. %#", saveError);
}
imoc = nil;
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
[self setExecuting:NO];
[self setFinished:YES];
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
-(void)mergeChanges:(NSNotification *)notification {
NSManagedObjectContext *mainContext = [[NSApp delegate] interpretedMOC];
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
-(id)initWithEntityName:(NSString *)entityName_ {
[super init];
[self setStarted:false];
[self setExecuting:false];
[self setPaused:false];
[self setFinished:false];
[NSThread setThreadPriority:0.0];
entityName = entityName_;
return self;
}
#end
// JGRefreshEntityOperation.h
#interface JGRefreshEntityOperation : NSOperation {
NSString *entityName;
NSManagedObjectContext *imoc;
NSError *saveError;
BOOL started;
BOOL executing;
BOOL paused;
BOOL finished;
}
#property(readwrite, getter=isStarted) BOOL started;
#property(readwrite, getter=isPaused) BOOL paused;
#property(readwrite, getter=isExecuting) BOOL executing;
#property(readwrite, getter=isFinished) BOOL finished;
-(void)startOperation;
-(void)finishOperation;
-(id)initWithEntityName:(NSString *)entityName_;
-(void)mergeChanges:(NSNotification *)notification;
#end
UPDATE 1
I just found this question. I can't understand how I missed it before I posted mine, but the summary is: Core Data wasn't designed to do what I'm doing. Only one process should be using a data store.
NSManagedObjectContext and NSArrayController reset/refresh problem
However, in a different area of my application I have two processes sharing a data store with one having read only access and this seemed to work fine. Plus none of the answers to my last question on this topic mentioned that this wasn't supported in Core Data.
I'm going to re-architect my app so that only one process writes to the data store at any one time. I'm still skeptical that this will solve my problem though. It looks to me more like an NSOutlineView refreshing problem - the objects are created in the context, it's just the outline view doesn't pick them up.
I ended up re-architecting my app. I'm only importing items from one process or the other at once. And it works perfectly. Hurrah!

NSThread with _NSAutoreleaseNoPool error

I have an method which save files to the internet, it works but just slow. Then I'd like to make the user interface more smooth, so I create an NSThread to handle the slow task.
I am seeing a list of errors like:
_NSAutoreleaseNoPool(): Object 0x18a140 of class NSCFString autoreleased with no pool in place - just leaking
Without NSThread, I call the method like:
[self save:self.savedImg];
And I used the following to use NSThread to call the method:
NSThread* thread1 = [[NSThread alloc] initWithTarget:self
selector:#selector(save:)
object:self.savedImg];
[thread1 start];
Thanks.
Well first of all, you are both creating a new thread for your saving code and then using NSUrlConnection asynchronously. NSUrlConnection in its own implementation would also spin-off another thread and call you back on your newly created thread, which mostly is not something you are trying to do. I assume you are just trying to make sure that your UI does not block while you are saving...
NSUrlConnection also has synchronous version which will block on your thread and it would be better to use that if you want to launch your own thread for doing things. The signature is
+ sendSynchronousRequest:returningResponse:error:
Then when you get the response back, you can call back into your UI thread. Something like below should work:
- (void) beginSaving {
// This is your UI thread. Call this API from your UI.
// Below spins of another thread for the selector "save"
[NSThread detachNewThreadSelector:#selector(save:) toTarget:self withObject:nil];
}
- (void) save {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// ... calculate your post request...
// Initialize your NSUrlResponse and NSError
NSUrlConnection *conn = [NSUrlConnection sendSyncronousRequest:postRequest:&response error:&error];
// Above statement blocks until you get the response, but you are in another thread so you
// are not blocking UI.
// I am assuming you have a delegate with selector saveCommitted to be called back on the
// UI thread.
if ( [delegate_ respondsToSelector:#selector(saveCommitted)] ) {
// Make sure you are calling back your UI on the UI thread as below:
[delegate_ performSelectorOnMainThread:#selector(saveCommitted) withObject:nil waitUntilDone:NO];
}
[pool release];
}
You need to mainly create an autorelease pool for the thread. Try changing your save method to be like this:
- (void) save:(id)arg {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//Existing code
[pool drain];
}
You will not you that the above does not call release on the NSAutoreleasePool. This is a special case. For NSAutoreleasePool drain is equivalent to release when running without GC, and converts to a hint to collector that it might be good point to run a collection.
You may need to create a run loop. I will add to Louis's solution:
BOOL done = NO;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[NSRunLoop currentRunLoop];
// Start the HTTP connection here. When it's completed,
// you could stop the run loop and then the thread will end.
do {
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) {
done = YES;
}
} while (!done);
[pool release];
Within the thread, you need to create a new autorelease pool before you do anything else, otherwise the network operations will have issues as you saw.
I don't see any reason for you to use threads for this. Simply doing it asynchronously on the run loop should work without blocking the UI.
Trust in the run loop. It's always easier than threading, and is designed to provide the same result (a never-blocked UI).

Resources