Reputation: 11787
I have two .xib files, MainMenu and PreferenceMenu. I set the File's Owner of PreferenceMenu to be a subclass of NSWindowController
, so that I can modify how the window is opened, modified, etc.
Normally I open PreferenceMenu through IB, sending the showWindow:
action to the Preferences Controller object. This has been working fine, and the window has been opening flawlessly. I also implemented NSTabView
and NSToolBar
in my PreferenceMenu as different tabs house different preferences.
Anyways, I now have to prompt the user if they want to open up the PreferenceMenu after first loading the application. My process is check to see if a NSUserDefaults
key is set, then prompts and sets the key if it isn't. If the returned NSAlert
button is a specific button, I then open up PreferenceMenu and switch to the correct tab.
The gist of this process is to:
CCPreferencesController.h
in CCAppDelegate.h
strong
reference to the CCPreferencesController
window (for ARC)Prompt with NSAlert
, and if returned button is correct, run the following:
self.windowController = [[CCPreferencesController alloc] initWithWindowNibName:@"PreferenceMenu"];
[self.windowController showWindow:self];
Import CCAppDelegate.h
in CCPreferencesController.m
Deal with the NSAlert
response in CCPreferencesController.m
:
- (void)showWindow:(id)sender{
if ([CCAppDelegate class] == [sender class]){
[self openLogin];
}
[super showWindow:sender];
}
TabView
in the openLogin
method.This more of less works fine, except showWindow
is called before the window's contents have actually loaded. This means that calling self.toolbar
, self.tabView
, etc. returns NULL
. My solution to this is to simply use a timer to wait for the elements to load, but this is nowhere near elegant.
My question, therefore, is how can I avoid having to use a delay, and instead have my openLogin
method get called on showWindow
but wait for the window's contents to be loaded?
I'm also 99% sure that my code is awful, including how I'm importing .h files and subclassing File's Owner, so any tips to make this better are greatly appreciated.
For those who want to see the code in more detail, here are the relevant parts
CCAppDelegate.h:
#import <Cocoa/Cocoa.h>
#import "CCPreferencesController.h"
@class WebView;
@interface CCAppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate, NSSharingServiceDelegate>
@property (strong) CCPreferencesController *windowController;
...
@end
CCAppDelegate.m:
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame{
bool loginPrompted = [[NSUserDefaults standardUserDefaults] boolForKey:@"loginPrompted"];
if (!loginPrompted){
NSAlert *alert = [NSAlert alertWithMessageText:@"Login"
defaultButton:@"Login"
alternateButton:@"Cancel"
otherButton:nil
informativeTextWithFormat:@"Would you like to login to enable voting?"];
// Display alert
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
// Return focus to window
[[alert window] orderOut:self];
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setBool:YES forKey:@"loginPrompted"];
[prefs synchronize];
if (returnCode == 1){
self.windowController = [[CCPreferencesController alloc] initWithWindowNibName:@"PreferenceMenu"];
[self.windowController showWindow:self];
}
}];
}
}
CCPreferencesController.h:
#import <Cocoa/Cocoa.h>
@interface CCPreferencesController : NSWindowController {}
...
@end
CCPreferencesController.m
#import "CCPreferencesController.h"
#import "CCAppDelegate.h"
@implementation CCPreferencesController
- (id)init{
if(self = [super initWithWindowNibName:@"PreferenceMenu"]) {}
return self;
}
- (void)awakeFromNib{
...
[self.toolbar setSelectedItemIdentifier:@"general"];
}
- (void)showWindow:(id)sender{
if ([CCAppDelegate class] == [sender class]){
[self openLogin];
}
[super showWindow:sender];
}
// This is gross
- (void)openLogin{
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 0.1);
dispatch_after(delay, dispatch_get_main_queue(), ^(void){
[self.toolbar setSelectedItemIdentifier:@"tab"];
[self.tabView selectTabViewItemAtIndex:1];
NSRect frame = [self.window frame];
frame.size.height += 55;
frame.origin.y -= 55;
[self.window setFrame:frame display:YES animate:YES];
});
}
Upvotes: 0
Views: 1174
Reputation: 413
What if you call [super showWindow:sender]
before [self openLogin]
? Like
- (void)showWindow:(id)sender {
[super showWindow:sender];
if ([sender isKindOfClass:[CCAppDelegate class]]) {
[self openLogin];
}
}
Added:
Sorry, my suggestion to move [super showWindow:sender]
before [self openLogin]
doesn't explain your problems. self.toolbar
and self.tabView
are nil
because they are IBOutlet
s and outlets aren't set until the corresponding nib file is loaded. The delay is just a way to wait until nib file is loaded and you admit yourself it's a fishy approach. Another solution is to load nib before calling [self openLogin]
and I've offered to call -showWindow:
because -showWindow:
indirectly loads nib if it isn't loaded already. I strongly encourage you to add breakpoints in -[CCPreferencesController awakeFromNib]
and -[CCPreferencesController windowDidLoad]
. Thus you can see in debugger when nib file is loaded and when IBOutlet
s are ready to be used.
I don't know anything about -[NSWindowController showWindow:]
scheduling something on the runloop. Unfortunately, @Daij-Djan hasn't provided any details about what exactly is scheduled on the runloop. @Daij-Djan has suggested to perform -openLogin
actions in -windowDidLoad
, but I see a sequence before_showWindow:, windowDidLoad, after_showWindow:
. I make a conclusion that nib is loaded immediately, not scheduled on the runloop. That's why it is safe to call -openLogin
without any delays.
Once again, I encourage you to check yourself in debugger how nib file is loaded. It is better to check it yourself, than rely on somebody's words.
The answer to question how to avoid using delay: call -openLogin
after window is loaded and in this case call -openLogin
after -showWindow:
because it causes nib loading.
Upvotes: 0
Reputation: 5665
Looking at the other options, in my opinion, you ought to just do this:
if (returnCode == 1){
self.windowController = [[CCPreferencesController alloc] initWithWindowNibName:@"PreferenceMenu"];
[self.windowController showWindow:self];
__weak CCAppDelegate* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.windowController openLogin];
}
And this:
- (void)openLogin{
[self.toolbar setSelectedItemIdentifier:@"tab"];
[self.tabView selectTabViewItemAtIndex:1];
NSRect frame = [self.window frame];
frame.size.height += 55;
frame.origin.y -= 55;
[self.window setFrame:frame display:YES animate:YES];
}
You are already instructing the WC to do something. It's not out of line to instruct it to show a particular tab, if the WC provides a public method to do such a thing.
It is essentially what I do when a user clicks a notification. I show the window, and have the window controller react tho what's in the userInfo dictionary in the notification on the next runloop iteration.
[edit] -- now schedules openLogin on the runloop with dispatch_async
Upvotes: 0
Reputation: 50099
I'd not go this way but maybe it is right in your situation because you do it bloc kingly but before you call showWindow you could force load the window. just call self.window
before you call super showWindow
NOTE that calling a getter to achieve side-effects is also far from clean!
It'd be best to move the code that needs the window in something like windowDidLoad or so
@implementation MyWindowController {
NSDictionary *_options;
}
- (void)showWindow:(id)sender{
if ([CCAppDelegate class] == [sender class]){
_options = @{@"Login": @YES};
} else {
_options = nil;
}
if(self.isWindowLoaded) {
[self applyOptions];
}
[super showWindow:sender];
}
- (void)windowDidLoad {
[self applyOptions];
}
- (void)applyOptions {
if([_options[@"Login"] boolValue]) {
//if logged in ... blablabla
}
}
Upvotes: 1