macos - Cocoa/OSX: comportamiento extraño en NSSavePanel que no muestra sub-itens
directory (2)
Tengo una instancia NSSavePanel con un comportamiento extraño: cada vez que lo abro y hago clic en la flecha de un directorio (el pequeño botón expandir) muestra un icono de carga indeterminado en la esquina inferior izquierda que nunca termina, y no muestra el directorio / archivo árbol. Una imagen puede ver como sigue:
En ese ejemplo, hice clic en el directorio "espacio de trabajo". Y el panel no muestra los subtitulos. Incluso extraño es que después de hacer clic de nuevo (volver a dibujar el directorio) y luego volver a hacer clic (volver a abrir el directorio), se muestran correctamente todos los archivos.
Mi código es el siguiente:
// 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];
}
¿Alguien puede darme una pista de dónde me estoy perdiendo?
ACTUALIZACIÓN: para mí, parece ser algo relacionado con dispatch_async!
¡Gracias por adelantado!
Acabo de encontrar el camino: estaba haciendo todas las llamadas dentro del dispatch_async en la fila de hilos principales. Por el hecho de que la descarga todavía se está ejecutando en el momento de devolución de llamada, entró en conflicto con el hilo que abre el panel. Solucioné todos los problemas simplemente colocando las líneas correctas, cambiando de:
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];
});
});
a:
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSLog(@"GenerateReport: [success]");
[self.delegate successHandlerCallback:data from: from];
});
y en la devolución de llamada, simplemente actualizando los campos. Al final, descubrí que todo eso era un hilo principal / de fondo que entendí mal.
En realidad, esta es una mala interacción entre NSSavePanel
y la cola principal de Grand Central Dispatch y / o +[NSOperationQueue mainQueue]
. Puedes reproducirlo con solo este código:
dispatch_async(dispatch_get_main_queue(), ^{
[[NSSavePanel savePanel] runModal];
});
En primer lugar, cada vez que realice operaciones de GUI, debe hacerlo en el hilo principal. (Hay raras excepciones, pero debe ignorarlas por el momento). Por lo tanto, tenía razón al enviar el trabajo a la cola principal si iba a hacer algo como abrir un cuadro de diálogo de archivo.
Desafortunadamente, la cola principal es una cola en serie, lo que significa que solo puede ejecutar una tarea a la vez, y NSSavePanel
envía parte de su propio trabajo a la cola principal. Por lo tanto, si envía una tarea a la cola principal y esa tarea ejecuta el panel de guardar de forma modal, monopoliza la cola principal hasta que el panel de salvar finalice. Pero el panel de guardar se basa en la capacidad de enviar sus propias tareas a la cola principal y hacer que se ejecuten.
En lo que a mí respecta, esto es un error en Cocoa. Debe enviar un informe de error a Apple.
La solución correcta es que NSSavePanel
envíe todas las tareas que tenga al hilo principal utilizando un mecanismo de reentrada como una fuente de bucle de -performSelectorOnMainThread:...
, -performSelectorOnMainThread:...
, o CFRunLoopPerformBlock()
. Necesita evitar el uso de la cola principal de GCD o NSOperationQueue
.
Como no puede esperar a que Apple solucione esto, la solución alternativa es que haga lo mismo. Utilice uno de los mecanismos anteriores para enviar su tarea que puede ejecutar el panel de guardar en la cola principal.