Getting around IBActions limited scope - cocoa

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.

Related

ipad uisplitview: passing data from masterviewcontroller to a uiviewcontroller (redirected via segue)

I'm new to iOS programming so I want to make this thing from scratch to be able understand how the whole thing works. So instead of using the master detail template I did this from the ground up.
I'm having a huge roadblock in terms of passing data between the master view and the detail view. At the moment, whenever I tap on the item of the master view it would look for the first view on the detail view controller, let's call it mainswitchviewcontroller. It's the first ViewController connected via segue to the UINavigationController for the detail controller part (Please refer to image below).
This happens after login where it shows that viewcontroller, I want the user to pick an option before accessing the whole app. which will lead them to the last ViewController of the flow (refer to the image below - red circle).
Now what I'm trying to do is this:
#interface MasterListViewController : UITableViewController{
}
#property (nonatomic) int matNum;
#property (strong, nonatomic) DetailStaffMainContentViewController *detailViewController;
#end
:
:
#implementation MasterListViewController
#synthesize matNum;
:
:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
for(UIViewController *vc in self.splitViewController.viewControllers){
NSLog(#"%#)", vc.title);
}
self.detailViewController.teststring = #"gronk";
}
This is my detailViewController (the one I want to access)
#implementation DetailStaffMainContentViewController
#synthesize tableView;
#synthesize teststring;
#synthesize lblOutput;
Now on select of the row, I assign the teststring which I want to use to populate that label on the controller. That's what I did.
My questions are:
I'm getting an error. Wondering what I was doing wrong?
[MainSwitchBoardViewController setTeststring:]: unrecognized selector
sent to instance 0x7572190
Is this a recommended way of passing data? Or should I just stick with notificationcenters?
Thoughts?
EDIT
Just to give you on the flow of the app:
User logs in (if username does not exist, the floating viewcontroller will pop up and force the user to log in.
Once the user logs in, the pop up goes away and then asks the user what type of role is to be used in the current session (eg. staff or fighter).
If the user picks fighter, it pushes the user to the top VC If the user picks staff, it pushes the user to the bottom VC (collections controller)
FOR Staff: At the moment I'm pushing to the last VC with this [self perfermSegueWithIdentifier:sender:self]
Once the user gets shown the last VC (DetailsStaffView), I want this to be the main Details View where the user does it's transaction. A few more pop ups? (Or maybe another push to another VC for specific transaction needs to be added). But ultimately I don't want the user to access the first Viewcontroller (MasterSwitchBoard)
EDIT 2
This is the main switch board class that controls the first VC after login.
#interface MainSwitchBoardViewController : UIViewController{
}
#property (strong, nonatomic) IBOutlet UILabel *txtLabel;
- (IBAction)btnStaff:(id)sender;
- (IBAction)btnAdmin:(id)sender;
#end
This is the collections VC which atm automatcially pushes to the last VC. In the beginning I was using a notification on this but it seems like it's not necessary so I took it out.
#interface MatListCollectionViewController ()
#end
#implementation MatListCollectionViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[self performSegueWithIdentifier:#"SeguePushToStaffDetailFromMats" sender:self];
}
And then the details staff
#interface DetailStaffMainContentViewController : UIViewController{
}
#property (strong, nonatomic) IBOutlet UITableView *tableView;
#property (nonatomic, retain) IBOutlet UILabel *lblOutput;
#property (nonatomic, retain) NSString *teststring;
#end
#implementation DetailStaffMainContentViewController
#synthesize tableView;
#synthesize teststring;
#synthesize lblOutput;
I'll tackle those in reverse order:
Is this a recommended way of passing data? Or should I just stick with notificationcenters?
You theoretically could use notification center, but I wouldn't in this case. Personally, I only use NSNotificationCenter if:
I'm doing asynchronous process which has no reasonable way of knowing the state of the view controllers (e.g. I have some background task that's doing some time consuming update of data from a server, and the user could have navigated to just about anywhere in the app by the time I'm ready to notify the view controllers that the update is done); and/or
I potentially have multiple objects that I want to notify of some event.
Neither of those conditions apply here. Certainly you could use NSNotificationCenter, but that should be unnecessary. Explicitly setting properties is generally preferable.
I'm getting an error. Wondering what I was doing wrong?
Unfortunately, it's hard to say on the basis of the evidence you've presented thus far. But there are a few things that I'm having trouble reconciling on the basis of your code and comments:
You've declared detailViewController to be a DetailStaffMainContentViewController (and that's the class for which you've synthesized the teststring accessors);
But your error message suggests that detailViewController is clearly a MainSwitchBoardViewController object (which doesn't understand the setTeststring accessor method); and
You haven't shown us how you're setting detailViewController, so it's hard to say where you've gone wrong here.
Personally, in cases like this, I'm always reticent to define properties to maintain pointers to view controllers. I'd generally be inclined to ask the UISplitViewController what's in the detail split. So, rather than having a class property/ivar for that, in my master view controller, I have a method like:
- (UIViewController *)detailViewController
{
return [[self.splitViewController.viewControllers lastObject] topViewController];
}
That's basically saying "get the last object from my split view controller's array of viewControllers (the detail split) and because I (personally) always use a navigation controller in that detail split, let's use the topViewController to get a pointer to my subclassed UIViewController that's in that detail split (embedded within the navigation controller).
Then I have my didSelectRowAtIndexPath check to see if that view controller is the one I expected. If not, I segue to it (and have prepareSegue pass the data I want to that controller).
So, for example, consider this storyboard:
Here, "A" is my default detail controller. "B" might be some random scene that I may have replaced "A" with. And "C" is my real detail view controller where I can see the details for the item I selected from the master view controller's table view. Thus, my didSelectRowAtIndexPath for the master view controller might look like:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UIViewController *controller = [self detailViewController];
if (![controller isKindOfClass:[DetailCViewController class]])
{
// if I'm not already showing "C" in my detail split, then let's segue to it
[self performSegueWithIdentifier:#"GoToC" sender:self];
}
else
{
// if I'm already showing "C" in my detail split, let's just set the item of data I want to pass to it
DetailCViewController *cController = (DetailCViewController *)controller;
cController.detailItem = self.objects[indexPath.row];
}
// I have a method I call to make sure that the popover menu for the master view
// controller disappears (in case it popped up since I was in portrait mode) and
// adds the master view controller button to the navigation bar if I need it.
[self removePopoverAndAddNavigationButton];
}
In case I needed to segue to "C", I also have my `prepareSegue pass the detailItem as needed:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"GoToC"])
{
DetailCViewController *cController = (DetailCViewController *)[segue.destinationViewController topViewController];
cController.detailItem = self.objects[[[self.tableView indexPathForSelectedRow] row]];
}
}
As I said, trying to figure out why your detailViewController has the wrong type of object is hard to say, but this is how I might tackle something like this.
Maybe you should first take a look at some nice movies from the last WWDC where they describe Storyboard Segues.
In the code you provided there is nothing written about segues. Be sure to use segues either via IB or programmatically via [self performSegueWithIdentifier:]. See Apple Docs
Then you can provide additional operations in yout view controller:
[view shouldPerformSegueWithIdentifier:sender:]
[view prepareForSegue:sender:]
The first one decides whether to perform the segue or not. The second one is executed just before segue is performed. Within the secon operation you can provide information to the destinationViewController.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"showIssuesByProject"]){
if (_searchActive) {
((IssueListViewController *)segue.destinationViewController).project = [_filteredProjects projectAtIndex:[self.tableView indexPathForSelectedRow].row];
} else {
((IssueListViewController *)segue.destinationViewController).project = [_projects projectAtIndex:[self.tableView indexPathForSelectedRow].row];
}
}
if ([segue.identifier isEqualToString:#"showIssuesByFilter"]) {
((IssueListViewController *)segue.destinationViewController).filter = [_filters filterAtIndex:[self.tableView indexPathForSelectedRow].row];
}
}
Be careful: UISplitViewControll.viewControllers are only two controllers: the master and detail ones.

Outlets and instance methods

I have a little problem and hope that you can help me.
I want to call a instance method of a subclassed window and set the user interface up there:
//AppDelegate.h
#import <Cocoa/Cocoa.h>
#class MainView;//The main window
#interface DownloadedAppDelegate : NSObject <NSApplicationDelegate> {
IBOutlet MainView*mainview;//the objects are in the same nib, outlet connected with the window
}
#property(nonatomic,retain) IBOutlet MainView*mainview;
#end
.
//AppDelegate.m
#import "MainView.h"
#synthesize mainview;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[mainview launched];//But sometimes this code fails, I don't know why
//launched sets up the interface
}
-(void)dealloc {
mainview=nil;
}
MainView belongs to NSWindow.
Is there something wrong or something to improve? Should I build up the UI somewhere else? Do you know why this code does not work always?
Try putting
[mainview launched];
in
-(void)awakeFromNib {
}
Use the debugger! Is launched even getting called?
Set a break point at the launched call and look at the value of mainview. It is nil? This is because outlets are not guaranteed to be connected until awakeFromNib.

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];
}

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?

Which delegate method should I use to respond to clicks on an NSTextField?

I am trying to respond to a click within a textfield. When the click occurs, I am going to open a panel. My initial thought was to use a delegate method to respond to the click event - but I found that:
This method doesn't work:
(void)textDidBeginEditing:(NSNotification *)aNotification
This method does work, but only when I actually edit the text within the text field, not when I first click it. And - if I edit the text a second time, this method stops working:
(void)controlTextDidBeginEditing:(NSNotification *)aNotification
I could use as much detail as possible - or a code example, ideally. I know that an nstextfield inherits from NSControl, which has a mouseDown event. Is there a similar way to respond to the event with a textfield, also?
Since NSTextField inherits from the NSControl class, it also inherits the -(void)mouseDown:(NSEvent*) theEvent method.
I needed to have an NSTextField call a delegate function upon clicking it today, and thought this basic code might be useful. Note that NSTextField already has a delegate and that in SDK v10.6, the delegate already has a protocol associated with it. Note that if you don't care about protocols, compiler warnings, etc., you don't need the protocol and property declarations or the getter and setter.
MouseDownTextField.h:
#import <Appkit/Appkit.h>
#class MouseDownTextField;
#protocol MouseDownTextFieldDelegate <NSTextFieldDelegate>
-(void) mouseDownTextFieldClicked:(MouseDownTextField *)textField;
#end
#interface MouseDownTextField: NSTextField {
}
#property(assign) id<MouseDownTextFieldDelegate> delegate;
#end
MouseDownTextField.m:
#import "MouseDownTextField.h"
#implementation MouseDownTextField
-(void)mouseDown:(NSEvent *)event {
[self.delegate mouseDownTextFieldClicked:self];
}
-(void)setDelegate:(id<MouseDownTextFieldDelegate>)delegate {
[super setDelegate:delegate];
}
-(id)delegate {
return [super delegate];
}
AppDelegate.h:
#interface AppDelegate <MouseDownTextFieldDelegate>
...
#property IBOutlet MouseDownTextField *textField;
...
AppDelegate.m:
...
self.textField.delegate = self;
...
-(void)mouseDownTextFieldClicked:(MouseDownTextField *)textField {
NSLog(#"Clicked");
...
}
...
If you're building with 10.5 SDK, don't have the protocol inherit from NSTextFieldDelegate.

Resources