How to make [NsApp run] not block? - objective-c

I'm a complete Cocoa newbie.
Right now my simple Hello World app blocks after calling [NsApp run] from main().
All I need is create a window and not block main().
I want my application to behave like glfw:
https://github.com/glfw/glfw/blob/master/src/cocoa_window.m#L1022
For some reason, it doesn't block there. In fact, you can remove this line, and it will still work.
I've been playing with a glfw source to figure out what they do differently. And for example, [NsApp run] blocks if I remove [NSApp setDelegate:_glfw.ns.delegate];
But that's not it.
According to the Apple docs:
The NSApplication class sets up #autorelease block during
initialization and inside the event loop—specifically, within its
initialization (or shared) and run() methods.
Typically, an app creates objects either while the event loop is
running or by loading objects from nib files, so this lack of access
usually isn’t a problem. However, if you do need to use Cocoa classes
within the main() function itself (other than to load nib files or to
instantiate NSApplication), you should create an #autorelease block to
contain the code using the classes.
I guess that's what I need, but I have no idea how to use the #autorelease block.
Thanks for your help.

I figured it out.
GLFW implements its own event loop, so calling [NSApp run] is not needed:
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantFuture]
inMode:NSDefaultRunLoopMode
dequeue:YES];
[NSApp sendEvent:event];

I've been looking at this problem myself, and decided to organize everything everyone said and I have found into one answer.
You should probably note that the reason run blocks is because it has something functionally similar to a while(1) loop inside of it which updates the window and handles events and such. Calling [NSApp stop] stops this loop so it wont block anymore (good), but we also have no more events (bad).
Overall, what GLFW does is the following:
Create an NSApplication, create a delegate, and set the delegate to the application.
Call run and stop on the NSApp. stop is called inside applicationDidFinishLaunching of the delegate.
On each frame get the next event from queue and dispatch it down the application.
More in-depth with code: PS. All code is (altered for simplicity) from the GLFW github
// src/cocoa_init.m
int _glfwPlatformInit(void)
{
...
//setup NSApplication, then init and set delegate
[NSApplication sharedApplication];
GLFWApplicationDelegate* del = [[GLFWApplicationDelegate alloc] init];
[NSApp setDelegate:del];
//This is a guard to make sure run is only called once
if (![[NSRunningApplication currentApplication] isFinishedLaunching])
[NSApp run];
}
Note the isFinishedLaunching guard exists because applicationDidFinishLaunching in the delegate only gets called after the first run call. If for some reason the user called glfwInit() again, without the guard run would be called, causing it to block again.
// src/cocoa_init.m
#interface GLFWApplicationDelegate : NSObject <NSApplicationDelegate>
#end
#implementation GLFWApplicationDelegate
...
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
_glfwPlatformPostEmptyEvent();
[NSApp stop:nil];
}
#end
// src/cocoa_window.m
void _glfwPlatformPollEvents(void)
{
#autoreleasepool {
for (;;)
{
NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (event == nil)
break;
[NSApp sendEvent:event];
}
} // autoreleasepool
}
Note glfwPollEvents retrieves the next event from the queue, then sends the event to the application to be propogated through the responders. It functions as the main loop explained in the documentation about event architecture.
In the main event loop, the application object (NSApp) continuously gets the next (topmost) event in the event queue, converts it to an NSEvent object, and dispatches it toward its final destination

I just experienced the same scenario.
I and found a little different solution.
Just call [[NSApplication sharedApplication] run]; and when your window is created, call [[NSApplication sharedApplication] stop:nil];, the run call will now exit. And then you can still call the sendEvent method.

Related

Why is WKInterfaceLabel text not refreshing

The WatchApp receives data from the iPhone.
I refresh the label text with the data received, nothing happens, the UI is not refreshing.
Other threads suggested pushing it to the main thread and that seems to do nothing either.
Any thoughts most welcome.
-(void)session:(nonnull WCSession *)session didReceiveApplicationContext:(nonnull NSDictionary *)applicationContext
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.lblTitleBorH setText:#"test"];
});
}
Are you using
[*your session* updateApplicationContext:*your dictionary* error:nil];
correctly?
try putting a NSLog inside your above didReceiveApplicationContext code and see if it is printing anything out.
In my case, when I tried to refresh the UI, I found that the outlet references were nil. The problem was caused by two interfaces on the storyboard, belonging to the same WKInterfaceController class.
When I assigned the second screen interface to another WKInterfaceController class it worked fine.
remember to call the UI objects from the main thread by using
dispatch_async(dispatch_get_main_queue(), ^{
...
});
or by using methods like performSelectorOnMainThread: withObject: waitUntilDone:

pushViewController Taking too much to show the view

I have a really light ViewController, it does nothing in viewDidLoad. I'm pushing this view on top of a navigationController. The method who does this action is called from inside a block. After the call to showView I added an NSLog, and that log prints in the console really fast, but the view takes a lot to load... I really don't understand what maybe happening... any idea???
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
[self showView];
NSLog(#"EXECUTED");
});
- (void) showView{
TestViewController *test = [[TestViewController alloc]init];
[self.navigationController pushViewController:test animated:NO];
}
From the docs for ABAddressBookRequestAccessWithCompletion:
The completion handler is called on an arbitrary queue. If your app uses an address book throughout the app, you are responsible for ensuring that all usage of that address book is dispatched to a single queue to ensure correct thread-safe operation.
You should make sure your UI code is called on the main thread.
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error {
dispatch_async(dispatch_get_main_queue(), ^{
[self showView];
NSLog(#"EXECUTED");
});
});
This might not be the only problem, but according to the docs, the completion handler passed to ABAddressBookRequestAccessWithCompletion is called on an arbitrary queue. -showView should only be called on the main queue since it is dealing with UIViewController objects.
The other thing to ask is what else is happening on the main queue? Are there other long-running tasks that could be blocking UI updates?

addSubview waits for rest of method

I would like show my view immediately when I call it. I don't know how to make the view show.
-(IBAction) showProgress: (id) sender {
progressViewController *progress = [[progressViewController alloc] initWithNibName:#"progressViewController" bundle:NULL];
[self.view addSubview:progress.view];
[self someFunctionWhichTakesAgesToBeDone];
}
It's called from current UIViewController. And the view appears after the long function. How can I show it before the long funcion? Thanks for answer.
Use GCD (Grand Central Dispatch) which is the simplest way (and recommended by Apple), the code will then be:
-(IBAction) showProgress: (id) sender {
progressViewController *progress = [[progressViewController alloc] initWithNibName:#"progressViewController" bundle:NULL];
[self.view addSubview:progress.view];
// Heavy work dispatched to a separate thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(#"dispatched");
// Do heavy or time consuming work
[self someFunctionWhichTakesAgesToBeDone];
// When finished call back on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
// Return data and update on the main thread
});
});
}
It´s two blocks. The first one does the heavy work on a separate thread and then a second block is called when the heavy work is finished so that changes and UI updates are done on the main thread, if needed.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/nsobject_Class/Reference/Reference.html
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait`
use
[self.view performSelectorOnMainThread:#selector(addSubview:) withObject:progress.view waitUntilDone:YES]
or put your Sleep() function (i hope it's anything else, Sleep() func is really bad, as its been told) into another function MySleepFunc and call
[self performSelector:#selector(MySleepFunc) withObject:nil afterDelay:0.003]
instead of Sleep(3).
Read about multi-threading. In short, there's single UI thread that does drawing, accepting user events and so on. If you pause it with sleep() or any other blocking method, nothing will be shown/redrawn and no events will be processed. You have to make your HTTP request from background thread.

Release a NSWindowController when the window is closed

I'm building a Cocoa application and have a question about using window controllers. The idea is that if the user selects New from the menu bar, an instance of MyWindowController which is a subclass of NSWindowController is created and a new window from MyWindow.xib is displayed.
I'm handling the action in the application delegate. From what I have seen after searching around something like the following could be done. Once the window is displayed I don't have any reason to store a pointer to the window controller anymore and since I allocated it I also autorelease it before displaying the window.
[[[[MyWindowController alloc] init] autorelease] showWindow:self];
Since the window is released soon afterwards the window will briefly display on the screen and then go away. I have found a solution where I retain the window controller in the -showWindow: method and let it release itself once it gets a windowWillClose notification.
- (IBAction)showWindow:(id)sender
{
[self retain];
[[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification
object:self.window
queue:nil
usingBlock:^(NSNotification *note) {
[self release];
}];
[super showWindow:sender];
}
Is there a better way to do this? I have searched the Apple documentation and have not found anything on which practices to use. It sounds like something very basic which it should cover so maybe I'm just searching with the wrong terms.
Normally you would hold on to the window controller, and only release it when you are done with it. I'd say that your app delegate would be responsible for that. Just store them in an array if there can be multiple. Whilst your solution may work, it's not very elegant.
If you are working on a document based Cocoa app, you create the window controller in your document subclass method makeWindowControllers and let that class hold a pointer to your window controller.
func windowShouldClose(_ sender: NSWindow) -> Bool {
#if DEBUG
let closingCtl = sender.contentViewController!
let closingCtlClass = closingCtl.className
print("\(closingCtlClass) is closing")
#endif
sender.contentViewController = nil // will force deinit.
return true // allow to close.
}

App modal NSPanel / sheet / dialog + NSThread == window hangs?

I'm in the midst of debugging an extremely unusual problem, and I was wondering if anybody might have any insight into what might be going wrong:
In a controller class from a NIB, I take an NSPanel from that same NIB, and then show it app modally on a NSWindow (that was created by hand in code):
[[NSApplication sharedApplication] beginSheet: myPanel
modalForWindow: window
modalDelegate: self
didEndSelector: #selector(sheetDidEnd:returnCode:contextInfo:)
contextInfo: nil];
[[NSApplication sharedApplication] runModalForWindow: myPanel];
Now, when the "finish" button on that sheet is clicked, I run some code to disable some buttons and fire off a thread to make sure the user input is valid (I have to validate with a remote service). This thread is fired from a separate validator object I create:
// controller calls:
[validator validateCreds: creds
notify: #selector(validationComplete:)
onObject: self];
// validator object
validateInfo: (NSDictionary *)parms
notify: (SEL)notifySelector
onObject: (id)notifyObject
{
// build up data with parms and notify info
[[NSThread detachNewThreadSelector: #selector(remotevalidate:)
toTarget: self withObject: data];
}
Next, when the validation is finished, the validator notifies my controller object:
[notifyObject performSelectorOnMainThread: notifySelector
withObject: results waitUntilDone: NO];
And then my controller object, in the method that the validator object calls, kills the dialog:
- (void)validationComplete: (id)data
{
[[NSApplication sharedApplication] stopModal];
[createTwitterPanel orderOut: nil];
[[NSApplication sharedApplication] endSheet: createTwitterPanel
returnCode: NSOKButton];
}
- (void)sheetDidEnd:(NSWindow *)sheet
returnCode:(int)returnCode
contextInfo:(void *)contextInfo
{
m_returnCode = returnCode;
}
My problem: Although the panel is closed / disappears, the top NSApp runModalForWindow: does not exit until some system event is sent to the window that was showing the dialog. Trying to move, resize, or do anything to the window, or otherwise switching away from the application suddenly causes the method to exit and execution to continue. No amount of waiting seems to help, otherwise, however.
I have verified that all methods being invoked on the controller class are all being invoked on the main app thread.
An even more interesting clue is that the dialog has two controls, a WebView, and an NSTextField: Even if I force the exit of runModalForWindow: by clicking on the window, TABbing between the two controls remains screwed up — it simply never works again. It's like my event loop is horked.
I've tried changing validationComplete: to instead post a notification to the main thread, and I've also played with the waitUntilDone on the performSelectorOnMainThread method, all to no effect.
Any ideas? Things I should try looking at?
From the NSApplication documentation:
abortModal must be used instead of
stopModal or stopModalWithCode: when
you need to stop a modal event loop
from anywhere other than a callout
from that event loop. In other words,
if you want to stop the loop in
response to a user’s actions within
the modal window, use stopModal;
otherwise, use abortModal. For
example, use abortModal when running
in a different thread from the
Application Kit’s main thread or when
responding to an NSTimer that you have
added to the NSModalPanelRunLoopMode
mode of the default NSRunLoop.
So, I learned something today.