I am trying to create a custom NSViewController and just log something out in viewDidLoad. In iOS, this is very trivial and works fine. However, when I setup a contentViewController on NSWindow (which i assume is similar to RootViewController in iOS?) it attempt to load it from a nib.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.ABViewController = [[ABViewController alloc] init];
self.window.contentViewController = self.ABViewController;
}
2016-06-28 09:15:42.186 TestApp[32103:33742217] -[NSNib _initWithNibNamed:bundle:options:] could not load the nibName: ABViewController in bundle (null).
What assumptions am I missing about how Cocoa is different from iOS that prevent me from simply setting up a viewController?
NSViewController doesn't have the same behavior as UIViewController, in that it won't automatically know to look for a nib file with the same name as itself. In other words, it won't automatically know to look for the ABViewController.nib file.
The simplest way to fix this is just override the nibName method in ABViewController:
#implementation ABViewController
- (NSString *)nibName {
return NSStringFromClass([self class]);
}
#end
Note that using NSStringFromClass() is usually better than trying to hard code the string, as this way will survive refactoring.
You can then call [[ABViewController alloc] init]; like before and NSViewController's default init method will get the nib name from your overridden nibName method.
It looks like your program wan't find the file where the view's are defined. You need to do something like this for storyboards:
UIStoryboard *sboard = [UIStoryboard storyboardWithName:#"StoryboardFileName"
bundle:NSBundle.mainBundle()];
SecondViewController *vc1 = [sboard instantiateInitialViewController];
Related
I am having a weird error with NSViewController where if I allocate a view using the viewcontroller's regular init message, the view created is not my view, but when using the default NIB name, it does work.
Specifically, this code works all the time. It creates the view defined in the nib file, and displays it in the parentView.
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [[TransparentAccessoryViewController alloc] initWithNibName:#"TransparentAccessoryViewController" bundle:nil];
NSLog(#"%#", [controller.view class]); // Returns "TransparentAccessoryView" -- CORRECT
[parentView addSubview:controller.view];
}
However, the following code works SOME of the time (which is weird in that it doesn't always fail). With some parentViews, it works perfectly fine, and with others, it doesn't. The parent views are just random custom NSViews.
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [TransparentAccessoryViewController new];
NSLog(#"%#", [controller.view class]); // Returns "NSSplitView" -- INCORRECT
[parentView addSubview:controller.view];
}
The errors that comes up are as follows (I have no idea why it is bringing up an NSTableView, as I don't have an NSTableView here at all. Also, it is weird that it complains about an NSTableView when the type it prints is an NSSplitView):
2013-04-07 21:33:12.384 Could not connect the action refresh: to
target of class TransparentAccessoryViewController
2013-04-07 21:33:12.384 Could not connect the action remove: to target
of class TransparentAccessoryViewController
2013-04-07 21:33:12.385 * Illegal NSTableView data source
(). Must implement
numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:
The NIB file defines a custom subclassed NSView, called TransparentAccessoryView, and hooks this up to the File Owner's view property, standard stuff (all I did was change the custom class name to TransparentAccessoryView). I added an NSLog's to see what was going on, and for some reason, in the second case, the view class type is incorrect and thinks it is an NSSplitView for some reason. The ViewController class is as follows:
#implementation TransparentAccessoryViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Initialization code here.
}
return self;
}
- (void)awakeFromNib {
self.textField.stringValue = #"";
}
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [[TransparentAccessoryViewController alloc] initWithNibName:#"TransparentAccessoryViewController" bundle:nil];
NSLog(#"%#", [controller.view class]);
[parentView addSubview:controller.view];
}
#end
I thought that the default init message triggers the viewcontroller to load the NIB named after the viewcontroller, which seems to be the case some of the time as the second version of my code works in certain conditions.
Does anyone know why this behavior is occurring at all?
From the docs:
If you pass in a nil for nibNameOrNil then nibName will return nil and
loadView will throw an exception; in this case you must invoke
setView: before view is invoked, or override loadView.
Therefore, if you're initializing a NSViewController with -init, you should call -setView: to set the view controller's view, or override -loadView. In the latter case, you could certainly implement the UIViewController-like behavior that you're probably expecting -- if nibNameOrNil is nil, try to load a nib that has the same name as the class.
I think that when you call init on a NSViewController, you're assuming that the implementation of init for NSViewController searches for a nib with the same name as the view controller and uses it. However, this is undocumented API or at least I can't seem to find any documentation supporting that assumption. The link you posted on your comments doesn't cite any documentation either and even reiterates that this is undocumented and that Apple could change this implementation at any point.
I think to assure that your code works in future versions of the SDK (and since it is already creating undesired behavior), you should not rely on this assumption. To achieve the same outcome simply override init and initWithNibName:bundle: in such a way as explained by this post:
#implementation MyCustomViewController
// This is now the designated initializer
- (id)init
{
NSString *nibName = #"MyCustomViewController";
NSBundle *bundle = nil;
self = [super initWithNibName:nibName bundle:bundle];
if (self) {
...
}
return self;
}
- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}
I created a new blank standard application using Xcode template. Removed the window in MainMenu.xib and I created a new customized NSWindowController subclass with a xib.
They were named "WYSunFlowerWindowController.h" and "WYSunFlowerWindowController.m".
And I append then init function like below:
- (id)init
{
NSLog(#"init()");
return [super initWithWindowNibName:#"WYSunFlowerWindowController" owner:self];
}
And my WYAppDelegate.m file is like below:
static WYSunFlowerMainWindowController* windowController = nil;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
if (windowController == nil) {
windowController = [[WYSunFlowerMainWindowController alloc] init];
}
[[windowController window] makeKeyAndOrderFront:windowController];
}
And I have the problem, that the window can't show it self after I launch the app. Anyone can tell me why? Is anything wrong with my code?
I am a newbie in Objective-C and cocoa. So I think I maybe make a silly mistake that I can't figure it out by myself.
UPDATE:
Here is my project source. Pleas have a look and help me to figure out what is my mistake。
https://dl.dropbox.com/u/3193707/SunFlower.zip
In your init method, I think you have to set self to the super init first before you return self.
-(id)init
{
NSLog (#"init()");
self = [super initWithWindowNibName:#"WYSunFlowerWindowController" owners:self];
return self;
}
Edit:
Try replace makeKeyAndOrderFront: with [windowController showWindow:self]
Then if that still doesn't work, check your window controller xib, make sure the file owner is set to WYSunFlowerWindowController and that the IBOutlet Window (declared in NSWindowController) is connected to the window.
Edit 2:
Commenting out your #property and #synthesize window in your controller was the trick. Don't redeclare get and setters that were already predefined in a superclass.
I'm looking for a "best practice" / "low test friction" way to do state based testing on view controllers inside my base AppDelegate class. Currently the below provides an easy way to stub in my own UIViewController (using ocmock) when something happens to it inside a method on the class.
-(FirstViewController *)getFirstViewController
{
if (self.viewController1)
{ return self.viewController1; }
self.viewController1 = [[FirstViewController alloc] initWithNibName:#"FirstViewController" bundle:nil];
return self.viewController1;
}
The first question I have -Is this a valid way to stub out / inject my own mock view controller for testing? (seems to work great but I'm not sure if this is how the pros are doing state based testing today)
The next question I have -Is it valid to keep 1 copy of the view controller in memory like this (only creating it from scratch once for the life of the app) ?
**note- I would dependency inject this but my init is already large enough just injecting the nav controller and tab bar controller so that's not an option for this large class sadly
If it's the root view controller, you should just make it a property of your app delegate:
#interface MyAppDelegate : NSObject <UIApplicationDelegate>
#property(retain)FirstViewController *firstViewController;
#end
#implementation MyAppDelegate
#synthesize firstViewController;
...
#end
Unless the method you're testing is the method where you initialize firstViewController, you don't need any kind of lazy loading approach. You just get the app delegate in your test, create an instance of FirstViewController and assign it to the property on your delegate, and define the test:
-(void)testSomething {
MyAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
FirstViewController *firstViewController = [[FirstViewController alloc] init];
appDelegate.firstViewController = firstViewController;
// test some app delegate method
...
}
If you want to mock out the controller for whatever you're testing, you can do that as well:
-(void)testSomething {
MyAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
id mockController = [OCMockObject mockForClass:[FirstViewController class]];
appDelegate.firstViewController = mockController;
[[mockController expect] someControllerMethod];
// test some app delegate method
...
[mockController verify];
}
Dependency injection doesn't require you to inject all dependencies through the init method. There are reasons why that's preferred but that's another discussion.
You could simply add a -setFirstViewController: method to your class. You would use that method in your test to inject your mock. If you don't like that method being around in your app you can add the method using a category in your test code.
For this kind of test I will make it like you, well, slighly differently.
1st- lazy loading of First View controller is encapsulated insite a property.
In .h file
#interface AppDelegate {
FirstViewController *viewController1_;
}
Then
#property (nonatomic, readonly) FirstViewController viewController1;
In .m file
- (FirstViewController *)viewController1 {
if (!viewController1_) {
viewController1_ = [[FirstViewController alloc] initWithNibName:#"FirstViewController" bundle:nil];
}
return viewController1_;
}
2nd- If I want to inject a mock object, I use KVC in my test code
[appDelegateUnderTest setValue:mockViewController forKey:#"viewController1_"];
Regards,
recently I started using storyboard and I've the following situation: I want to set the text of an UILabel from the AppDelegate. So I created an instance of my ViewController
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:#"MainStoryboard"
bundle: nil];
ViewController *controller = (ViewController*)[mainStoryboard
instantiateViewControllerWithIdentifier: #"mainViewController"];
myViewController = controller;
[window addSubview:myViewController.view];
[window makeKeyAndVisible];
and called the following method from the delegate
- (void) updateParameterLabel:(NSString *)parameter {
NSLog(#"URL-2: %#", parameter);
parameterLabel.text = parameter;
}
But the parameter is not shown in the UI.
Another think, which is kind of strage:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(#"View did Appear");
}
The "View did appear" is logged twice ...
Any hints?
Regards,
Sascha
Setting the text of a UILabel from your application delegate isn't great design. Your view controllers should be managing the content of your views, hence their name. Typically your storyboard is instantiated automatically, and you don't need any of the storyboardWithName et code you've got, assuming you're working with Apple's default templates.
Maybe think about re-architecting your application to follow the 'model-view-controller' pattern more strictly, and also look at how Apple instantiate storyboards automatically (just create a new storyboard project in XCode to see this).
If you still want to make it work, make the UILabel a property of your viewcontroller and set the label by using
In delegate :
- (void) updateParameterLabel:(NSString *)parameter {
NSLog(#"URL-2: %#", parameter);
[myViewController updateParemeter:parameter];
}
In myViewController:
- (void) updateParameterLabel:(NSString *)parameter {
NSLog(#"URL-2: %#", parameter);
parameterLabel.text = parameter;
[self.view setNeedsDisplay];//edit
}
So use the viewController to update your label. Of course you need the label as a property in your viewController
For what I see you are trying to update the label before it appears, so why don't you try calling your updateLabel method in the viewWillAppear, it would be something like this
-(void)viewWillAppear:(BOOL)animated{
[self updateParameterLabel:#"Some Text"];
[super viewWillAppear:YES];
}
And updateParameterLabel has to be implemented in the viewController.
How can I overwrite the initWithRootViewController method in a UINavigationController?
The only methods generated by xcode for me where methods suchs as loadFromNibName and loadView. These methods don't get called and I need to add an NSNotification to the navigationcontroller at startup.
I know it looks a little like the following but I don't know what put in the body of the method
- (id)initWithRootViewController:(UIViewController *)rootViewController
{
// what goes here?
}
EDIT
I guess the question really is "how do you customize a UIViewCOntroller during initialization"
Edit 2
My Navigation Controller header
#interface AccountViewNavigationController : UINavigationController {
}
#end
Instantiating my UINavigationController Like so will result in no startup methods hitting break point
accountViewNavController = [[UINavigationController alloc] initWithRootViewController:accountView];
Where as if I instantiate like so loadView does get called.... but it gets called numerous times
accountViewNavController = [[UINavigationController alloc] init];
[accountViewNavController initWithRootViewController:accountView NO];
I'm highly confused by this stage.
Use the same basic structure you use for overriding any other init method:
- (id)initWithRootViewController:(UIViewController *)rootViewController
{
if ((self = [super initWithRootViewController:rootViewController])) {
// Your modifications go here
}
return self;
}
Do note that Apple claims UINavigationController is "not intended for subclassing", but they don't absolutely forbid it. I guess that means "don't try to change how the class works by messing with the internal message flow".