Update NSWindow content while asynchronously loading a file - macos

In my document-based MacOS application, I have some big files that are loaded (especially recent files opened at application launch). I created ProgressController (a NSWindowController subclass) to inform user in a window that file loading is in progress. It is allocated by the makeWindowControllers method of the NSDocument subclass I use to manage documents. These are created when the user opens a file (and notably at startup when documents were displayed as the user did quit the application) and they load their content asynchronously in a background queue which in principle should not affect the main thread performance:
-(instancetype) initForURL:(NSURL *)urlOrNil withContentsOfURL:(NSURL *)contentsURL ofType:(NSString *)typeName error:(NSError *
{
if (self = [super init]){
... assign some variables before loading content...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0), ^{
[[[NSURLSession sharedSession] dataTaskWithURL:urlOrNil completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
self.content = [[NSMutableString alloc] initWithData:data encoding:encoder];
dispatch_async(dispatch_get_main_queue(), ^(){
[self contentIsLoaded];
});
}] resume];
});
}
return self;
}
In contentIsLoaded, the progress window is closed.
- (void) contentIsLoaded
{
... do something ...
[self.progressController close];
}
This behaviour is OK, the window is displayed and closed when necessary.
The problem occurs when I want to update the content of this window on the main queue. I tried setting a NSTimer but is is never fired, even though it is created in the main queue. So, in MyApplicationDelegate I created a GCD timer like this:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if (self->timer !=nil) dispatch_source_cancel(timer);
self.queue = dispatch_queue_create( "my session queue", DISPATCH_QUEUE_CONCURRENT);
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
[self updateProgress];
});
dispatch_resume(timer);
…
}
In MyApplicationDelegate, the updateProgress method is defined as:
- (void) updateProgress
{
dispatch_async(self.queue, ^{
NSLog(#"timer method fired");
dispatch_async(dispatch_get_main_queue(), ^(){
NSLog(#"access to UI fired");
... update window (e.g. NSProgressIndicator)
}
});
if (self.shouldCancelTimer) dispatch_source_cancel(self->timer);
});
}
When running the application, The "Timer method fired" is logged every second. The "timer method fired" message (in the main queue) is logged once or twice only, then the logging seems suspended until the file has been loaded. Then this missing message appears several times in a row, as it was suspended before.
What did I wrong? I supposed that the background queue used for file loading should not affect the main queue and UI updates. Many applications behave like that and I need such behaviour as files in my App (strings, csv, json) can be hundreds of Mbytes!

For starters, have you looked into NSURLSessionTask's progress reporting APIs? There are a bunch of facilities for measuring and getting called back as data loads. It might be possible for you to avoid doing the polling, and just update your UI as these are called back - all on the main thread.
In fact, you might not even need one at all. As I recall, the networking operations carried out by NSURLSession are all done in the background anyways. I bet you can remove all of this, and just use some extra delegate callbacks from NSURLSession APIs to get this done. Way simpler too :)
Good luck!

Related

How to wait for completion of sheet after invoking beginSheet:completionHandler:

My code results in the sheet appearing and functioning properly but the invoker receives control back (as per Apple definition) before the sheet ends itself with endSheet.
How can I get the invoker to wait for the return from the end of the sheet processing, so the resultValue is updated.
Invoker:
[self.window beginSheet: sheetController.window
completionHandler:^(NSModalResponse returnCode) {
resultValue = returnCode;
}
];
...
Sheet:
...
[self.window.sheetParent endSheet:self.window returnCode:false];
I thought the new beginSheet: was meant to do the whole linkage, but that isn't the case. All it does is put up the sheet window. The passage and return of control remains as it was. So the following code works:
Invoker:
[self.window beginSheet: sheetController.window
completionHandler:nil
];
returnCodeValue = [NSApp runModalForWindow:sheetController.window];
// Sheet is active until stopModalWithCode issued.
[NSApp endSheet: sheetController.window];
[sheetController.window orderOut:self];
Sheet:
[NSApp stopModalWithCode:whatever];
Looks like you're most of the way there. The return code should be type NSModalResponse (ObjC) or NSApplication.ModalResponse (Swift). I've also found that you need to uncheck 'Visible at Launch'. Otherwise it won't launch as modal.
There was an NSAlert category from way back that used the older (deprecated) methods. I’m not all that great with Objective-C (and worse with Swift), but I’ve been using an updated equivalent for a while in one of my RubyMotion projects. Hopefully I’ve come close with reversing the code conversions, but basically it fires up a modal event loop with runModalForWindow: after calling beginSheetModalForWindow;, and the completionHandler exits with stopModalWithCode:
// NSAlert+SynchronousSheet.h
#import <Cocoa/Cocoa.h>
/* A category to allow NSAlerts to be run synchronously as sheets. */
#interface NSAlert (SynchronousSheet)
/* Runs the receiver modally as a sheet attached to the specified window.
Returns a value positionally identifying the button clicked */
-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow;
/* Same as above, but runs the receiver modally as a sheet attached to the
main window. */
-(NSInteger) runModalSheet;
#end
// NSAlert+SynchronousSheet.m
#import "NSAlert+SynchronousSheet.h"
#implementation NSAlert (SynchronousSheet)
-(NSInteger) runModalSheetForWindow:(NSWindow *)theWindow {
// Bring up the sheet and wait until it completes.
[self beginSheetModalForWindow:theWindow
completionHandler:^(NSModalResponse returnCode) {
// Get the button pressed - see NSAlert's Button Return Values.
[NSApp stopModalWithCode:returnCode];
}];
[NSApp runModalForWindow:self.window] // fire up the event loop
}
-(NSInteger) runModalSheet {
return [self runModalSheetForWindow:[NSApp mainWindow]];
}
#end
The application will wait for the alert to finish before continuing, with the modal response passed in the result. I don’t know about blocking a couple of thousand lines of code, but I found it useful when chaining two or three sheets, such as an alert before an open/save panel.

PFUser currentUser nil after app restart on OS X

I've downloaded the latest Parse SDK for OS X, and I'm trying to retain my login after app restarts (obviously). I've used Parse before and I haven't faced this problem, neither on iOS nor OS X.
On my app start:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
[Parse setApplicationId:#"XXX" clientKey:#"XXX"];
}
In my first view controller:
-(void)viewDidAppear{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if(![PFUser currentUser]){
[self performSegueWithIdentifier:#"login" sender:nil];
}else{
...
}
});
}
My login succeeds, and at that point [PFUser currentUser] is valid. Then, I close the app (tried both killing and gracefully quitting). When I open it again, [PFUser currentUser] is nil. I've tried this many times, it yields the same results. Why?
After struggling for a long while, I've found the solution. I need to dispatch_async the user checking block and it starts working. So instead of:
dispatch_once(&onceToken, ^{
if(![PFUser currentUser]){
[self performSegueWithIdentifier:#"login" sender:nil];
}else{
...
}
});
I did:
dispatch_once(&onceToken, ^{
dispatch_async(dispatch_get_main_queue(), ^{
if(![PFUser currentUser]){
[self performSegueWithIdentifier:#"login" sender:nil];
}else{
...
}
});
});
And it started working. Interesting to see that something is still not initialized on viewDidAppear synchronously on main queue (yes, it IS the main queue, double checked that), but is initialized somewhere after posting to the same queue asynchronously. Parse definitely needs more quality control of their SDK.

force NSDocument to save after creation

In its documents, my application uses a lot of assets that are relative to the document path. So the document must be saved before assets can be added. How can I force-call a [NSDocument saveDocumentAs] ?
I managed to do parts of it : by creating my own document controller, and inside openUntitledDocumentAndDisplay: force a call like this :
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError
{
NSDocument * res = [super openUntitledDocumentAndDisplay:displayDocument error:outError];
[res saveDocumentAs:self];
return res;
}
This forces the save dialog to appear, but unfortunately I can not check whether the user pressed cancel : the saveDocumentAs call is asynchronous and continues immediately !
Is there a way to fix this ?
I had a similar problem. By using:
saveDocumentWithDelegate:(id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(void *)contextInfo
you can defer your processing (or not) until after the document save dialogue has completed. This means you can find out whether the user cancelled or not. You split your processing in two, do whatever preparation you need and put the rest (that depends upon a successful save) into another method. If you use something like:
[self saveDocumentWithDelegate:self didSaveSelector:#selector(actuallyDoIt: didSave: contextInfo:) contextInfo:nil];
The document will be saved but, critically, if it has not been saved before, the Save dialogue will appear so the user can input a file name. Once he/she has done that, or cancelled, your method actuallyDoIt: (or whatever) is invoked. The didSave: parameter tells you whether the save actually happened (essentially, did the user cancel) so you can either continue or offer an alert explaining politely to the user that nothing's going to happen until they save.
I have a similar thing in my application, in my case if the user tries to do something, I pull up a prompt to say 'This requires you to save the document first' with buttons to cancel or save.
If you want to absolutely force it, then instead of using saveDocumentAs, just display your own NSSavePanel. Run it modally, check the result, save the document with the result, and if this doesn;t go smoothly, call it again. You can check if the document is saved by looking for a valid value for it's file path.
NSSavePanel can run modally. Here is how it can/should look like.
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError *__autoreleasing *)outError
{
Document *document;
NSSavePanel *panel = [NSSavePanel savePanel];
panel.prompt = #"Create";
NSInteger modalCode = [panel runModal];
if (modalCode == NSModalResponseOK) {
NSURL *URL = [panel URL];
NSError *docError;
document = [[Document alloc] initWithType:nil & docError];
[document saveToURL:URL ofType:nil forSaveOperation:NSSaveOperation completionHandler:^(NSError *error){
if(error) {
return nil;
}
[self addDocument:document];
[document makeWindowControllers];
if (displayDocument) {
[document showWindows];
}
}];
}
return document;
}
To sum up for reference:
Create custom nsdocumentsubclass in XIB (no XIB -> app did finish launching)
override openUntitledDocumentAndDisplay
(NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError;

gcdasyncsocket background file transfer

Having two devices that need to keep transferring data while in background or in LockScreen.
The main resource about backgrounding is available on https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html
So far I'm looking forward to understand how is it expected to implement such above mentioned behaviour: in a scenario where a transfer is in progress and one of the apps (or both) goes into background. Obviously we have resumable transfer management working already.
I've been collecting stubs and answers about and I've ended up with the following:
Ensure every socket is backgroundable.
[socket performBlock:^{
[socket enableBackgroundingOnSocket];
}];
To keep backgrounding even when in Lock Screen, I read an answer saying that we should have something like at the end of didFinishLaunchingWithOptions but what code is in [self backgroundHandler] method?
BOOL backgroundAccepted = [[UIApplication sharedApplication]
setKeepAliveTimeout:600 handler:^{ [self backgroundHandler]; }];
if (backgroundAccepted)
NSLog(#"background handler accepted");
return YES;
The applicationDidEnterBackground delegate method of UIApplication shows
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSLog(#"=== DID ENTER BACKGROUND ===");
if([[UIDevice currentDevice] respondsToSelector:#selector(isMultitaskingSupported)])
NSLog(#"Multitasking Supported");
else
return;
// Shall I remove my KVO observers when in background?? I guess NOT, right? :D
//[[NSNotificationCenter defaultCenter] removeObserver:self];
UIBackgroundTaskIdentifier bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(#"End of tolerate time. Application should be suspended now if we do not ask more 'tolerance'");
// [self askToRunMoreBackgroundTask]; This code seems to be unnecessary. I'll verify it.
}];
if (bgTask == UIBackgroundTaskInvalid)
NSLog(#"This application does not support background mode");
else
NSLog(#"Application will continue to run in background");
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
I got it working following this tutorial but looks like GCDAsyncSocket is no longer maintained so it will work only on iOS7.
http://www.objc.io/issue-5/multitasking.html
To do background file transfer under iOS 8 I am using AFNetworking library (http://afnetworking.com)

How to let DropboxAPI work in runModalForWindow

I'm using the Dropbox API for OSX. All works fine, except when I want to make calls in a modal window that is started with [NSApp runModalForWindow:thisWindow]; it seems the modal loop blocks the
DropboxAPI from processing anything.
The DBRestClient delegate methods are never called in response to for example [client loadMetadata:path]; Which is - if understand correctly - in line with what the NSApplication documentation says for this method. The question is:
Is there a way to let calls to Dropbox work from inside a modal window?
I have seen that timers can be added to the NSModalPanelRunLoopMode. Is there perhaps something similar for the DroboxAPI?
And additionally: will Dropbox calls that were started but not yet completed before this or any other modal window is displayed proceed as normal, or are they also blocked?
Yes; further investigation shows any runModalForWindow and even displaying an NSAlert.showModal will completely block the DropboxAPI. Also, inplace mouse handling loops do the same thing. Imo a major design flaw in the OSX DropboxAPI: it should have been running on a background thread. The only way around this, is to not start any user task that could involve blocking Dropbox while the API something is still running. Which is not really feasible in any non-trivial app that needs dropbox to work in the background.
Implementation of NSApplication runModalForWindow is indeed too harsh.
Here is more humanistic version:
void runWindowModal(NSWindow* pnw) {
[pnw retain];
NSApplication* app = [NSApplication sharedApplication];
NSModalSession session = [app beginModalSessionForWindow:pnw];
for( ;; ) {
if ([app runModalSession:session] != NSModalResponseContinue)
break;
NSEvent* e = [app nextEventMatchingMask: NSAnyEventMask
untilDate: [NSDate distantFuture]
inMode: NSDefaultRunLoopMode
dequeue: YES];
if (e != nil)
[app sendEvent: e];
}
[app endModalSession: session];
[pnw release];
}
This will enable NSURLConnection and other callbacks processing while running modal loops.
Dropbox support just confirmed that the DropboxAPI on OSX works on the main runloop and indeed runModalForWindow etc. will block the API. There's no work around.
The problem is more general. All nsurl connections can run only on the main runloop. This means that webviews have the same issue. And there is a sollution to this. You have to create a modal session and run it for short amounts of time, frequently, and lock the main thread in a loop until the modal window is closed. Every time you switch back to the modal session, the main runloop will finish any scheduled tasks, and that includes the nsurlconnections from the modal session.
Now I will show you a piece of code but note that this is c# using Xamarin.Mac library. You should be able to translate this easily to objc. But if you find it difficult, you can look up solutions for nswebview in modal dialog.
//so this is in my NSWindowController - the modal dialog
IntPtr session;
bool running;
public void showDialog ()//this is the method I use to show the dialog
{
running = true;
session = NSApplication.SharedApplication.BeginModalSession (Window);
int resp = (int)NSRunResponse.Continues;
while (running && resp == (int)NSRunResponse.Continues) {
resp = (int)NSApplication.SharedApplication.RunModalSession (session);
NSRunLoop.Current.RunUntil (NSRunLoopMode.Default, NSDate.DistantFuture);
}
NSApplication.SharedApplication.EndModalSession (session);
}
[Export ("windowWillClose:")]
public void WindowWillClose (NSNotification notification)//this is the override of windowWillClose:
{
running = false;
}

Resources