Send controller object over delegate - cocoa

I am trying to make a cocoa app with an NSplitView that keeps track of "active subviews" (like how the Terminal app knows which pane you are editing).
I have a SessionWindowController that keeps track of the "currentPaneContainerViewController" based on which PaneView the user last clicked on.
some classes and files:
SessionWindowController.h/.m
PaneContainerViewController.h/.m
PaneView.h/.m
PaneContainerView.xib
PaneContainerView.xib has the PaneContainerViewController as its file owner.
My current implementation is as follows:
To access the PaneContainerViewController from the NSView, I use IBOutlets that reference to the file owner, and to access the SessionWindowController I also maintain an IBOutlet of the object conforming to a delegate method I made (namely, the object happens to be the SessionWindowController).
#import <Cocoa/Cocoa.h>
#import "PaneViewDelegate.h"
#class PaneContainerViewController;
#class SessionWindowController;
#interface PaneView : NSView
{
//outlet connection to own controller
IBOutlet PaneContainerViewController *myPaneContainerViewController;
//we'll use delegation to get access to the SessionWindowController
IBOutlet id<PaneViewDelegate> sessionDelegate;
}
#implementation PaneView
-(void)mouseUp:(NSEvent *)event
{
if (sessionDelegate && [sessionDelegate respondsToSelector:#selector(setCurrentPaneContainerViewController:)]) {
[sessionDelegate setCurrentPaneContainerViewController:myPaneContainerViewController];
}
}
Here is the class that sessionDelegate belongs to, which conforms to the PaneViewDelegate protocol:
#interface SessionWindowController : NSWindowController <PaneViewDelegate>
{
PaneContainerViewController *currentPaneContainerController;
}
- (void)setCurrentPaneContainerViewController:(PaneContainerViewController*)controller;
My trouble is accessing the SessionWindowController object using the IBOutlet. In Interface Builder, what do I connect the sessionDelegate outlet to to be able to access the SessionWindowController instance? Furthermore, is it okay to pass the controller to the delegate instead of an NSEvent?
I am new to Cocoa, so if there is a better design pattern do please let me know. This does seem like a lot of boilerplate for a pretty common functionality.

Related

Change IBOutlet text to property of object returned from custom class

I'm using xCode 4.2.1 - I have an app that uses a web service and fetches some data using a custom class and NSURLConnection. The user taps a "refresh" button which starts a series of events that happen in some custom classes in my project, and I can get the object, along with the properties I want to return in a method in the MainViewController, I'm just not able to change the text of an IBOutlet to a property (NSString) of the returned object.
In my "MainViewController.h, I have an IBOutlet (and it's wired to a button in the MainViewController.xib):
IBOutlet UILabel *textLabel;
and:
#property (nonatomic, retain) IBOutlet UILabel *textLabel;
And I've synthesized the label in MainViewController.m
#synthesize textLabel;
The process looks like this: Refresh Button Tapped --> Fires a mthod in MainViewController --> Fires another method in a custom class (retrieves data from web, creates object) --> Sends Object to MainViewController via: (in my custom class implementation)
// parsing, etc.. and define a string for priceString property of mp object
MainViewController *mvc = [[MainViewController alloc] init];
[mvc log:mp];
And in my MainViewController.m I can access a property of that object and print it in NSLog just fine from this method.
- (void) log:(Price *)mp {
self.textLabel.text = mp.priceString;
NSLog(#"%#", mp.priceString);
}
At this point, I can see the data in the log, but the textLabel text won't change.
I've been trying to read examples for a week, and I've heard everything from delegation, to NSNotication answers, but nothing seems to work.
All I need to do is populate an IBOutlet from a -(void) method.
Any help would be greatly appreciated, and I'm down for implementing delegation, but I'm very new to it, and seeking an example.
EDIT - more details.
After further research, I think I should note that my NSURLConnection that is returning my objects is in a separate class, and I've been reading a lot of threads where people are starting the connection in viewDidLoad.
The problem is that you are calling [mvc log:mp]; just after initializing the view controller. At that stage, the view is still not loaded from the xib file.
you can change log to see that:
- (void) log:(Price *)mp {
if (self.textLabel) {
self.textLabel.text = mp.priceString;
} else {
NSLog(#"textLabel is nil.");
}
}
You can call [mvc log:mp]; in the view controller viewDidLoad method, or after viewDidLoad has been executed.
To understand this better, add NSLog(#"viewDidLoad."); in viewDidLoad method, and add NSLog(#"viewController initialized."); just after the code where you initialized the view controller.
You will see the following
viewController initialized.
textLabel is nil. // if you call log as usual after initializing the view controller.
viewDidLoad.

How does an NSView subclass communicate with the controller?

I am brand spanking new to Cocoa programming, and am still kind of confused about how things wire together.
I need a pretty simple application that will fire off a single command (let's call it DoStuff) whenever any point on the window is clicked. After a bit of research it looks like subclassing NSView is the right way to go. My ClickerView.m file has this:
- (void)mouseDown:(NSEvent *)theEvent {
NSLog(#"mouse down");
}
And I have added the View to the Window and have it stretching across the whole thing, and is properly writing to the log every time the window is clicked.
I also have my doStuff method on my controller (this could be refactored to its own class I suppose, but for now it works):
- (IBAction)doStuff:(id)sender {
// do stuff here
}
So, how do I get mouseDown in ClickerView to be able to call DoStuff in the controller? I have a strong .NET background and with that, I'd just have a custom event in the ClickerView that the Controller would consume; I just don't know how to do that in Cocoa.
edit based on Joshua Nozzi's advice
I added an IBOutlet to my View (and changed it to subclass NSControl):
#interface ClickerView : NSControl {
IBOutlet BoothController *controller;
}
#end
I wired my controller to it by clicking and dragging from the controller item in the Outlets panel on the View to the controller. My mouseDown method now looks like:
- (void)mouseDown:(NSEvent *)theEvent {
NSLog(#"mouse down");
[controller start:self];
}
But the controller isn't instantiated, the debugger lists it as 0x0, and the message isn't sent.
You could either add it as an IBOutlet like Joshua said, or you could use the delegate pattern.
You would create a Protocol that describes your delegate's methods like
#protocol MyViewDelegate
- (void)doStuff:(NSEvent *)event;
#end
then you'd make your view controller conform to the MyViewDelegate protocol
#interface MyViewController: NSViewController <MyViewDelegate> {
// your other ivars etc would go here
}
#end
Then you need to provide the implementation of the doStuff: in the implementation of MyViewController:
- (void)doStuff:(NSEvent *)event
{
NSLog(#"Do stuff delegate was called");
}
then in your view you'd add a weak property for the delegate. The delegate should be weak, so that a retain loop doesn't form.
#interface MyView: NSView
#property (readwrite, weak) id<MyViewDelegate> delegate;
#end
and then in your view you'd have something like this
- (void)mouseDown:(NSEvent *)event
{
// Do whatever you need to do
// Check that the delegate has been set, and this it implements the doStuff: message
if (delegate && [delegate respondsToSelector:#selector(doStuff:)]) {
[delegate doStuff:event];
}
}
and finally :) whenever your view controller creates the view, you need to set the delegate
...
MyView *view = [viewController view];
[view setDelegate:viewController];
...
Now whenever your view is clicked, the delegate in your view controller should be called.
First, your view needs a reference to the controller. This can be a simple iVar set at runtime or an outlet (designated by IBOutlet) connected at design time.
Second, NSControl is a subclass of NSView, which provides the target/action mechanism machinery for free. Use that for target/action style controls. This provides a simple way of setting the reference to your controller (the target) and the method to call when fired (the action). Even if you don't use a cell, you can still use target/action easily (NSControl usually just forwards this stuff along to its instance of an NSCell subclass but doesn't have to).
you can also use a selector calling method,
define two properties in custom class:
#property id parent;
#property SEL selector;
set them in view controller:
graph.selector=#selector(onCalcRate:);
graph.parent=self;
and call as:
-(void)mouseDown:(NSEvent *)theEvent {
[super mouseDown:theEvent];
[_parent performSelector:_selector withObject:self];
}

Cocoa - calling a VIEW method from the CONTROLLER

Got a little problem i asked about it before but maybe i didnt ask properly.
I have a cocoa application, which amongst other things, must do the following task:
- load some images from the disk, store them in an array and display them in a custom view.
In the Interface Builder i have a CustomView and an OBJECT that points to TexturesController.h
The custom view is a custom class, TextureBrowser.
Below is the code for the controller and view:
TexturesController
#import <Cocoa/Cocoa.h>
#class TextureBrowser;
#interface TexturesController : NSObject {
IBOutlet NSTextField *logWindow;
IBOutlet TextureBrowser *textureView;
NSMutableArray *textureList;
}
#property textureView;
-(IBAction)loadTextures:(id)sender;
-(IBAction)showTexturesInfo:(id)sender;
#end
TextureBrowser
#interface TextureBrowser : NSView {
NSMutableArray *textures;
}
#property NSMutableArray *textures;
-(void)loadTextureList:(NSMutableArray *)source;
#end
These are just the headers. Now , what i need to do is:
when loadTextures from the TexturesController is called, after i load the images i want to send this data to the view (TextureBrowser), for example, store it in the NSMutableArray *textures.
I tried using the -(void)loadTextureList:(NSMutableArray*)source method from the view, but in the TextureController.m i get a warning : No -loadTextureList method found
This is how i call the method :
[textureView loadTextureList: textureList];
And even if i run it with the warning left there, the array in the view class doesnt get initialised.
Maybe im missing something...maybe someone can give a simple example of what i need to do and how to do it (code).
Thanks in advance.
In TexturesController.m, you have to import TextureBrowser.h so that the controller knows what methods the property textureView has. Right now, you've just got a blank placeholder symbol instead of an actual class.
Since textureView is defined by an outlet, you need to make sure that its class is properly set in Interface Builder. If you provide a generic NSView instead, it won't have the loadTextureList: method.
If you imported TextureBrowser.h in TexturesController.m, I don't see why it wouldn't find your method.
But why don't you simply call self.textureView.textures = textureList; from the TexturesController ?

Getting around IBActions limited scope

I have an NSCollectionView and the view is an NSBox with a label and an NSButton. I want a double click or a click of the NSButton to tell the controller to perform an action with the represented object of the NSCollectionViewItem. The Item View is has been subclassed, the code is as follows:
#import <Cocoa/Cocoa.h>
#import "WizardItem.h"
#interface WizardItemView : NSBox {
id delegate;
IBOutlet NSCollectionViewItem * viewItem;
WizardItem * wizardItem;
}
#property(readwrite,retain) WizardItem * wizardItem;
#property(readwrite,retain) id delegate;
-(IBAction)start:(id)sender;
#end
#import "WizardItemView.h"
#implementation WizardItemView
#synthesize wizardItem, delegate;
-(void)awakeFromNib {
[self bind:#"wizardItem" toObject:viewItem withKeyPath:#"representedObject" options:nil];
}
-(void)mouseDown:(NSEvent *)event {
[super mouseDown:event];
if([event clickCount] > 1) {
[delegate performAction:[wizardItem action]];
}
}
-(IBAction)start:(id)sender {
[delegate performAction:[wizardItem action]];
}
#end
The problem I've run into is that as an IBAction, the only things in the scope of -start are the things that have been bound in IB, so delegate and viewItem. This means that I cannot get at the represented object to send it to the delegate.
Is there a way around this limited scope or a better way or getting hold of the represented object?
Thanks.
Firstly, you almost never need to subclass views.
Bind doesn't do what you think - you want addObserver:forKeyPath:options:context: (You should try to understand what -bind is for tho ).
When you say "the key seems to be it being the "prototype" view for an NSCollectionViewItem" I think you are really confused…
Forget IBOutlet & IBAction - they don't mean anything if you are not Interface Builder. "Prototype" means nothing in Objective-c.
The two methods in the view do not have different scope in any way - there is no difference between them at all. They are both methods, equivalent in every way apart from their names (and of course the code they contain).
If wizardItem is null in -start but has a value in -mouseDown this is wholly to do with the timing that they are called. You either have an object that is going away too soon or isn't yet created at a point you think it is.
Are you familiar with NSZombie? You will find it very useful.

Unable to write to NSTextField from Model Controller object

After always running monolithic blocks of code from within my AppController object, I've just learned how to modularize my code using separate model controller objects (so much neater and less confusing to work with :-))
My simple test app has a main AppController and two model objects (Model1 and Model2). I can successfully pass data from the AppController to the models, and the models themselves can run methods and process the passed data 'internally' as they were intended to do -- but I can't get them to communicate with a darned NSTextField in the UI. Here's the relevant parts of my code:
In AppController.m #import "AppController.h"
#implementation AppController
- (IBAction)passObjectsToModelController:(id)sender
{
NSString *stringToPass = #"Hello from Model2 :-)";
int numToPass=12345;
Model2 *ObjController2 = [[Model2 alloc]initWithStuff:stringToPass:numToPass];
[ObjController2 release];
}
#end
...in Model2.h
#import
#interface Model2 : NSObject
{
IBOutlet NSTextField *passedStringField;
}
- (id)initWithStuff:(NSString*)passedString :(int)passedNum;
#end
...and finally in Model2.m
#import "Model2.h"
#implementation Model2
- (id)initWithStuff:(NSString*)passedString :(int)passedNum
{
if(self = [super init])
{
NSLog(#"now inside 'Model2' controller...");
NSLog(#"the passed string reads: %#",passedString); //••• this works •••
NSLog(#"the passed number is:%d",passedNum); //••• this works •••
[passedStringField setStringValue:passedString]; //••• WTF!!... this DOESN'T work! •••
// do something internally with passedNum here...
}
return self;
}
#end
Both model objects have outlets to the common NSTextField and I've control-dragged from both objects to the field and connected them. My AppController doesn't know about the NSTextField (and I assume, doesn't even want to know). No IB connections have been made between the controller object and model objects.
NSLog tells me that the model objects are being created, and that the passed values are making it that far... but not from there into the text field in the GUI window. I'm not getting any compiler errors or warnings. Am I missing some kind of 'setTarget:' call perhaps?
Any help/ideas would be much appreciated. Thanks :-)
Aside from the lack of MVC that mihirsm mentions, the actual problem is that you're trying to access an outlet in an -init method.
When a object is initialized, outlets are not guaranteed to be connected.
If you want to set the value of an NSTextField declared as an outlet, you should implement -awakeFromNib, which is called when the nib has been loaded and all outlets are guaranteed to be live.
in Model1.h:
#interface Model1 : NSObject
{
IBOutlet NSTextField* passedStringField;
NSString* modelString;
}
- (id)initWithString:(NSString*)passedString number:(int)passedNum;
#end
in Model1.m:
#implementation Model1
- (id)initWithString:(NSString*)passedString number:(int)passedNum
{
if(self = [super init])
{
//copy the string to our ivar
modelString = [passedString copy];
}
return self;
}
//awakeFromNib is called when our outlet is live
- (void)awakeFromNib
{
[passedStringField setStringValue:modelString];
}
//don't forget to release the string, because we created it using -copy
- (void)dealloc
{
[modelString release];
}
#end
The Controller sits between the Model and the View. The Model should not communicate with the View.
It should be the job of the Controller to pass any incoming values from the View to the Model. The Model then processes the data and sends back to the Controller which then updates the View with the new data.
So, in your code you should only have one IBOutlet for the TexField declared in the AppController.
Given all this, I am not exactly sure why the TextField is not being updated. From the given code looks like it should. Maybe multople IBOutlets are causing some issue? Can you try with only one Model having the IBOutlet?

Resources