Update UI from another Class Method - Cocoa - objective-c

I would like to update the UI in my application from the AppDelegate, but whenever I call it as so:
Controller *object = [[Controller alloc] init];
[object methodHere];
It doesn't seem to update the UI. What am I doing wrong here? I have put in a NSLog to see if it was being called, and it is. Here is a sample project that shows the error.
Edit: Can someone just show me what to change to the project I provided. I just don't know what to type into my project so that I can change the value of a simple NSTextField from another class.

When you write [[Controller alloc] init], you are not accessing the Controller object that is in your nib. You are creating a new Controller object that is unconnected to anything else in your application.
Remember, every Controller object is not the same any more than every NSArray is the same. Just because you made one Controller in your nib that's connected to an NSTextField does not mean some random Controller that you just created shares that controller's connections.
What you need to do is give the delegate a reference to the Controller that's in the nib.

This is really simple, and Chuck's comments basically explain what you need to do, but I will lay out the code explicitly for you. In testAppDelegate.h:
#interface testAppDelegate : NSObject <NSApplicationDelegate> {
NSWindow *window;
// You can make an IBOutlet to any kind of object you
// want; it's just a way for you to get a reference
// in code to an object that has been alloc'd and
// init'd already by the xib mechanism.
IBOutlet Controller *controller;
}
Then go into your xib in InterfaceBuilder and hook up that outlet from your Test App Delegate object to your Controller object (these objects are already present in the xib).
In testAppDelegate.m:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// This is the key:
// _Don't_ alloc/init a new controller object. The
// objects in your xib are allocated and initialized
// by virtue of being in that file. You just need to
// give your AppDelegate a pointer to it, as above.
[controller setTextValue:#"hello"];
}

It's being called all right, but it's not connected to the interface. There should be a view controller of some sort defined in your appDelegate.h file, call the method on that object instead.
Update for more detail:
One way you could pull this off would be to simply save the Controller when you originally create it (and not release it until later.)
Simply put your own controller object into your .h file
Controller* myController;
And when you create the new view controller you want to flip to, simply set myController to reference that object, and later when you want to update the UI, simply call
[myController methodHere];
A bit clumsy, but it works. Just don't forget to release myController when you're done with that view.
The other idea I'd suggest looking into would be to alter the method you're passing to your delegate. That is, instead of having the method as
-(returnType)callDelegateToDoSomething;
put it in as
-(returnType)callDelegateToDoSomething:(id) sender;
You call the new method the same way, but your controller should automatically pass itself as an argument. Then, inside the method, simply use
[sender methodHere];
and it should hopefully work. (You may need to play around with it a little. I'm not an expert on delegates or the sender argument, but it's worth a shot.)

Related

Correct method to present a different NSViewController in NSWindow

I am developing an app that is a single NSWindow and clicking a button inside the window will present a NSViewController, and a button exists in that controller that will present a different NSViewController. I know how to swap out views in the window, but I ran into an issue trying to do this with the multiple view controllers. I have resolved the issue, but I don't believe I am accomplishing this behavior in an appropriate way.
I originally defined a method in the AppDelegate:
- (void)displayViewcontroller:(NSViewController *)viewController {
BOOL ended = [self.window makeFirstResponder:self.window];
if (!ended) {
NSBeep();
return;
}
[self.box setContentView:viewController.view];
}
I set up a target/action for an NSButton to the AppDelegate, and here's where I call that method to show a new view controller:
- (IBAction)didTapContinue:(NSButton *)sender {
NewViewController *newVC = [[NewViewController alloc] init];
[self displayViewcontroller:newVC];
}
This does work - it presents the new view controller's view. However if I then click any button in that view that has a target/action set up that resides within its view controller class, the app instantly crashes.
To resolve this issue, I have to change didTapContinue: to the following:
- (IBAction)didTapContinue:(NSButton *)sender {
NewViewController *newVC = [[NewViewController alloc] init];
[self.viewControllers addObject:newVC];
[self displayViewcontroller:[self.viewControllers lastObject]];
}
First of all, can you explain why that resolves the issue? Seems to be related to the way the controller is "held onto" in memory but I'm not positive.
My question is, how do I set this up so that I can swap out views from within any view controller? I was planning on getting a reference to the AppDelegate and calling displayViewcontroller: with a new controller I just instantiated in that class, but this causes the crash. I need to first store it in the array then send that reference into the method. Is that a valid approach - make the viewControllers array public then call that method with the lastObject, or how should this be set up?
What is interesting in your code is that you alloc/init a new view controller every time that you call the IBAction. It can be that your view its totally new every time you call the IBAction method, but I would think that you only have a limited number of views you want to show. As far as my knowledge goes this makes your view only to live as long as your IBAction method is long. That the view still exists, is because you haven't refreshed it. However, calling a method inside a view controller that is not in the heap anymore (since you left the IBAction method and all local objects, such as your view controller are taken of the heap thans to ARC) makes the app crash, because you reference a memory space that is not in use or used by something else.
Why does the app work when you ad the view to the viewcontrollers array? I assume this array is an array that has been initiated in the AppDelegate and now you add the view controller with a strong reference count to the viewcontrollers array. When you leave the IBAction method, the view controller still has a strong reference and ARC will not deallocate the view controller.
Is this the proper way? Well, it works. I would not think it is considered very good programming, since you don't alloc/init an object in a method that needs to stay alive after leaving the method. It would be better practice to allocate and initialize your view controller(s) somewhere in an init, awakeFromNIB or a windowDidLoad method of your AppDelegate. The problem with your current solution is that you are creating an endless array of view controllers of which you only use the last. Somewhere your program will feel the burden of this enormously long array of pretty heavy objects (view controllers) and will run out of memory.
Hope this helps.
By the way, this is independent of whether you use Mavericks or Yosemite. I was thinking in a storyboard solution, but that wouldn't answer your question.
Kind regards,
MacUserT

Cocoa - awakeFromNib is not called

I have an MainMenu.xib, and AppController is its file owner. I added -(void)awakeFromNib method which worked fine. Now, rounds of fixings down the road awakeFromNib stopped being called, and I can't figure out why. It owns the xib, so it should be called when it is unarchived. What's going on?
EDITED:
Well, I renamed awakeFromNib to something, and called that from init... that worked. Still confused as to why awakeFromNib is not. I also have a +(void) initialize method in there, could that be messing something up?
- (id)init {
self = [super init];
if (self) {
[self something];
}
return self;
}
-(void)something {
NSLog(#"yup");
}
Setting the class name of the File's Owner in the nib is only so you can tell Xcode what object's outlets and actions to show you so you can hook things up. It doesn't affect what object is actually the File's Owner when the app runs and the nib gets loaded.
The MainMenu nib's File's Owner is always the application object, no matter what class name you set for the FO in Xcode's inspector. Setting it to any class name but NSApplication[1] is wrong.
When you run your app, you should find error messages in the Console about any outlets or actions of the AppController that you tried to connect. They couldn't be connected because the application object doesn't have them.
Change the class name back in the nib editor, and create your AppController as a custom object in the MainMenu nib.
Well, I renamed awakeFromNib to something, and called that from init... that worked.
That means that init is getting called, which means you're calling it. That's a valid alternative to creating it in a nib, though you shouldn't override awakeFromNib if it's not in a nib or owning one.
Your choice: Continue creating the AppController using alloc and init, or remove that code and create it in the MainMenu nib instead.
[1]: Or, if you've subclassed NSApplication and changed the principal class of your app bundle to be that subclass, the name of that subclass.

Change the custom class in a storyboard using code when instantiating

I have a tab bar controller and a bunch of the same tabs. Each tab only differs in functionality, but the UI's are all the same. In the storyboard I designed the flow and UI of one tab and set it base class. Then when I create the tabs I tried typecasting them before adding them to the tab bar but it didn't work.
In the storyboard the View Controller indentified "TabView" has the custom class "TabColor"
TabRed *red = (TabRed *)[storyboard instantiateViewControllerWithIdentifier:#"TabView"];
TabBlue *blue = (TabBlue *)[storyboard instantiateViewControllerWithIdentifier:#"TabView"];
However the loadView method in TabColor gets called, not the TabRed/TabBlue.
Also if I nslog it the result is a TabColor object:
NSLog(#"%#", red)
Expected: TabRed
Actual: TabColor
tl;dr:
Storyboards and xibs contain collections of serialized objects. Specifying a class in a storyboard means you will get an instance of that class when you load the storyboard. A way to get the behavior you're looking for would be to use the delegation pattern common in cocoa/cocoa-touch.
Long Version
Storyboards, and similarly xib/nib files, are actually sets of encoded objects when you get down to it. When you specify a certain view is a UICustomColorViewController in the storyboard, that object is represented as a serialized copy of that an instance of that class. When the storyboard is then loaded and instantiateViewControllerWithIdentifier: gets called, an instance of the class specified in the storyboard will be created and returned to you. At this point you're stuck with the object you were given, but you're not out of luck.
Since it looks like you're wanting to do different things you could architect your view controller such that that functionality is handled by a different class using delegation.
Create a protocol to specify the functionality you'd like to be different between the two view controllers.
#protocol ThingDoerProtocol <NSObject>
-(void) doThing;
#end
Add a delegate property to your viewcontroller:
#interface TabColor
...
#property (strong, nonatomic) thingDoerDelegate;
And then have your new objects implement the protocol and do the thing you want them to.
#implementation RedTabDoer
-(void) doThing {
NSLog(#"RedTab");
}
#end
#implementation BlueTabDoer
-(void) doThing {
NSLog(#"BlueTab");
}
#end
Then create and hook up those objects when you load the storyboard.
TabColor *red = [storyboard instantiateViewControllerWithIdentifier:#"TabView"];
red.thingDoerDelegate = [[RedTabDoer new] autorelease];
TabColor *blue = [storyboard instantiateViewControllerWithIdentifier:#"TabView"];
blue.thingDoerDelegate = [[BlueTabDoer new] autorelease];
This should then allow you to customize the functionality of the view controller by changing the type of object that is assigned to the controllers delegate slot.
TabRed *red = (TabRed *)[storyboard instantiateViewControllerWithIdentifier:#"TabView"];
TabBlue *blue = (TabBlue *)[storyboard instantiateViewControllerWithIdentifier:#"TabView"];
Casting doesn't change values, it only changes the way the compiler interprets those values (and stops it from complaining when you use type in place of another). So casting a TabColor* to a TabRed* tells the compiler to pretend that your first pointer points to a TabRed instance, but it doesn't transmogrify the object that the pointer refers to into an instance of TabRed.
As waltflanagan explains, storyboards and .xib files contain actual objects, and the type of each object is determined when you create the file; you can't change it at run time. What you can do, though, is to have each of your several view controllers load the same view hierarchy. You don't even have to write any code to do this. Just create a .xib file containing your tab controller and the view controllers for each tab:
Be sure to set the type for each view controller appropriately in the .xib so that the right kind of view controller will be created for each tab:
Set the "NIB Name" field for each view controller to specify a .xib file that contains the view hierarchy that these controllers will use. If you specify the same .xib file for each controller, each controller will instantiate its own copies of those views:
Specify any IBOutlets in the common superclass of your view controllers so that all your view controllers have the same outlets. You can specify that superclass as the type of "File's Owner" in the common .xib file so that IB knows what outlets are available. File's owner is really a proxy for the object that's loading the .xib, so when one of your view controllers (TabRed for example) loads the common view .xib, that controller will be the one that the views in the .xib are connected to. When TabBlue loads the .xib, that object will be the one that those views are connected to.
This might seem confusing at first, but play with it. Understanding this will really help you understand .xib files (and therefore storyboards). They're a lot less magical than they seem when you're a beginner, but once you get it they'll seem even cooler.

Changes not reflected across view when using binding in cocoa

I am creating some sample applications to understand the concepts of view navigation, binding etc in cocoa.
Here is the scenario:
I have a window that has a tab view(2 tabs) in MainMenu.Xib.
I have a text field in the first tab and label in the second tab. I want both of them to reflect the same value and I want to do this using binding. Also, I don't want to use the views provided to me along with the tab view.
These are the steps I have done.
The view of each tab view item is set separately in the applicationDidFinishLaunching: method using the following code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
//initialize view controllers
view1=[[ViewTab1 alloc] initWithNibName:#"ViewTab1" bundle:nil];
view2=[[ViewTab2 alloc] initWithNibName:#"ViewTab2" bundle:nil];
//set views
[[[myTabView tabViewItems] objectAtIndex:0]setView:view1.view];
[[[myTabView tabViewItems] objectAtIndex:1]setView:view2.view];
}
myTabView is the outlet reference of the tab view from MainMenu.xib in AppDelegate.
ViewTab1 is the name of the first view controller (and the xib).
ViewTab2 is the name of the second view controller (and the xib).
ViewTab1 has one single text field (and an associated label). I have bound this to a variable(name) declared in AppDelegate.
ViewTab2 has a label. I have bound this also to the same variable in AppDelegate.
The variable, 'name' is initialized in the init method of AppDelegate.
AppDelegate.h
....
NSString *name;
....
#property(strong) ViewTab1 *view1;
#property(strong) ViewTab2 *view2;
#property (assign) IBOutlet NSTabView *myTabView;
#property (strong) NSString *name;
....
AppDelegate.m
....
#synthesize myTabView;
#synthesize view1,view2;
#synthesize name;
....
- (id)init {
self = [super init];
if (self) {
name=#"dummy";
}
return self;
....
Apart from this I haven't done any coding in my program.
In the ViewTab1.xib I got an object and made it an instance of AppDelegate and then connected the delegate reference of the Application object(NSApplication) to the same object. (I hope this is the right way of getting the AppDelegate object.)
I did the same in ViewTab2.xib
Then I bound the text field in ViewTab1 and label in ViewTab2 to this variable in AppDelegate.
When I run the program both the text field and label shows "dummy". But when I change the value in the text field, its not reflected in the label in the second tab( i.e. ViewTab2).
Please tell me what I'm doing wrong.
How to establish binding to the same App delegate object from any loaded Nib?
Yes, I know this frustrated situation as described in question... after many weeks and hundreds pages of documentation for KVO - Notifications - Bindings I think there is one very simple solution for that.
As we can find in some information sources the nib-loading process produce new instances of members... and we need to use binding connection to the old one.
Note that bindings made in InterfaceBuilder are redirect to these new instances automatically after loading nib
Why not redirect the pointer of App delegate to the old instance?
In method where you loads your nib you can test which object is app delegate before and just after nib load.
If the new one isn’t the same as the previous one you can redirect it as you want.
This simple example works for me in Xcode3 under 10.5.8 with target to OSX10.5 / i386:
// ***** SOMEWHERE IN DEFAULT APP-DELEGATE.m IMPLEMENTATION
- (IBAction) createOtherWindowFromNib: (id)sender
{
// ensure that app delegate is set as you want...
[NSApp setDelegate:self];
NSLog(#"APP-DELEGAT **** CREATE-TEST-WINDOW ***** WHO IS APP-DELEGATE BEFORE NIB LOAD: %# ", [[NSApp delegate] description]);
// we can bind members of the nib to this controller over proxy object named "File’s Owner"
NSWindowController *otherWinCapo = [[NSWindowController alloc] initWithWindowNibName: #"OtherTestWindow"];
NSLog(#"APP-DELEGAT **** CREATE-TEST-WINDOW ***** WHO IS APP-DELEGATE AFTER NIB LOAD: %# ", [[NSApp delegate] description]);
// make some test for delegates before/after here if you need ...
// usually your bindings made inside "OtherTestWindow.xib" by IB doesn’t works in this moment
// ... and some redirection if needed
[NSApp setDelegate:self];
// afer that the bind made in IB inside "OtherTestWindow.xib"
// referred to (proxy object) "Application.delegate.myBOOL" (Bind to:Application, Model Key Path:delegate.myBOOL)
// react to changes of myBOOL placed in default app delegate object as expected
// simultaneously in every open instance of "OtherTestWindow.xib"
[otherWinCapo showWindow: otherWinCapo.window]; // we need populate the window instance on screen to see it
}
I think the problem is that the objects in your xibs that you set to the app delegate class create 2 different instances of the app delegate, so changing the value of the text field changes the value of name in one instance but not in the other. That's what you're doing wrong, unfortunately, I can't think of a solution at this time.
Have you turned on 'Continuously Updates Value' in the NSTextField controls?
See this example.

objective-c: Calling a void function from another controller

i have a void, like -(void) doSomething in a specific controller.
i can call it in this controller via [self doSomething], but i don't know how to call this void from another .m file.
I want to call it in a -(IBAction) action:(id)sender
I've tried using performSelector, but i got a 'unreconiezd selector send' in the log.
I've no idea if i have to use the notification center, or delegate...
Thanks,
ronan.
You have to have an instance of the first controller (the one where you declare the function in) in your second controller.
FirstViewController *firstController = [[FirstViewController alloc] init];
[firstController doSomething];
If your first controller is declared somewhere else, and you want your second controller to know about it, have a property of FirstViewController type in your second controller, and initialize it when you need it with your FirstViewController instance.
Sometimes, this can be quite complicated to do if you have multiple controller instances and you want every one of them to know about all the others, so I would rather suggest rewriting your method to a class method:
+ (void) doSomething;
so you could call it from anywhere with
[FirstViewController doSomething];
if you have the object you want to call it on and the method is public, then you just:
[object doSomething];
The problem is that you have two controllers who don't know about each other's existence. The notification center certainly is the way to deal with this situation: the advantage is that you need not establish a formal connection between the two.
The delegate method is another possibility, but then you have to either establish the connection via an IBOutlet in Interface Builder, or you must have one controller create the other controller and pass itself as the delegate. This ties them together more closely, which may or may not be appropriate. If the only connection between the two controllers is the calling of one -(void) method, then I'd go with NSNotification.