6 Replies Latest reply: Apr 11, 2008 1:40 PM by Tod Kuykendall
Harold Holbrook Level 1 Level 1 (35 points)
Does anyone know a way to make an NSTextField notify the controller layer when it has finished editing?

I have one simple text field that I want the user to be able to just hit return or click out and one of my methods gets called.

NSButtons have a place in Bindings where you can put a selector name, and I have used this to keep the Model from having to talk directly to the View. But there is no such "selector name" field for bindings in an NSTextField.

In other words, I am trying to AVOID making an IBOutlet for the text field and addressing that - I want the text field to call my method when it is the "object did finish editing" state. Since every text field is a control, this seems as if it should be available.

I know there are other ways to do this but I am experimenting and looking to do it without having the Model communicate with the View.

Thanks!!!
  • Harold Holbrook Level 1 Level 1 (35 points)
    OK well I have another puzzlement.

    This is a button that passes the current selectionIndex of a TableView to my method.

    I want the selectionIndex for whatever item is selected in a TableView which is filled with a simple string array.

    In the Button's bindings, I bind the argument to the array controller's Controller Key of "selectionIndex", and fill in the "Selector" field with the name of the method.

    Don't worry if that is confusing, because it works. The method gets called when the button is clicked.

    The method has the form

    (void) theMethod: (NSUInteger) theArgument
    {
    NSLog (@"theArgument=%@\n", theArgument);
    }

    And this works, printing "theArgument=0" (or 1, or 2, etc) depending on which row is selected when the button is clicked.

    Here's the problem: see the object formatter ("%@") in the NSLog? If I make that an integer as it is supposed to be, then I get not the index numbers 0, 1 , etc., but huge numbers, as if they were pointers. The huge numbers show up if

    - I use the debugger
    - I use "%d" as the format in NSLog
    - I execute [playerArray removeObjectAtIndex: theArgument]; //gets an array index out of bounds error.

    But if I use %@ in NSLog, I get the correct numbers.

    The result from [NSArrayController selectionIndex] is supposed to be an NSUInteger, and that is what it is labeled as, but it has the wrong value unless I display it with the "%@" format.

    I'm stumped. I have tried taking the intValue of it, tried removing the (NSUInteger) from the method and putting (id) there, etc.

    Anybody see why I am getting this integer that is huge and yet correct when displayed as an object ("%@")?
  • Tod Kuykendall Level 4 Level 4 (2,270 points)
    What happens if you use %ld as a cast in your NSLog statements?

    Also submit your code bracketed by { code } markers (without the spaces) to keep your code from being molested by the board formatting.

    =Tod
  • Harold Holbrook Level 1 Level 1 (35 points)
    Hi Tod,

    Thanks for the reply.

    If I use %ld I still get the huge number. See below.

    It turns out that the value returned by the "argument" binding for an NSButton (where the argument is specified as the "selectionIndex" of an NSArrayController) is an object, namely an (NSNumber *), rather than the NSUInteger that the documentation for NSArrayController's "selectionIndex" method says.

    This explains why the "%@" works. But what had me stumped was that even when I changed the parameter to expect an NSUInteger, it did not work.

    According to a reply I got on the Cocoa-dev list, the returned object is not an NSUInteger (which is a scalar), but rather an NSNumber.

    So what I had to do was 1) change the argument to be an NSNumber *; and 2) when using the passed parameter to index into the array, use

    {code}
    [playerArray removeObjectAtIndex: [theWhackedOne integerValue]];
    {/code}

    And that works (it deletes the item in the array), but now I have the "changing the model doesn't update the View" issue. Apple's documentation on Bindings states that just calling "removeObjectAtIndex" is NOT KVC-compliant. It goes on to try and explain how to remedy this, but frankly I don't understand it.

    The documentation is at http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaBindings/Concepts /Troubleshooting.html

    Under "Changing The Value Of A Model Property...".

    So the documentation for NSArrayController's "selectionIndex" method is wrong, or when invoked by an IB binding is wrapped in an NSNumber wrapper, that they didn't tell me about.

    If you have any insight on the issue of why removing an object from an array does not trigger the KVO for that array model object, I would appreciate hearing it. My "playerArray" is an NSMutableArray, bound to an NSArrayController, and has the @property (readwrite, copy) and @synthesize compiler instructions in the code.

    Message was edited by: Harold Holbrook
  • Tod Kuykendall Level 4 Level 4 (2,270 points)
    +According to a reply I got on the Cocoa-dev list, the returned object is not an NSUInteger (which is a scalar), but rather an NSNumber.+

    This exactly explains the behavior you were seeing.

    +So the documentation for NSArrayController's "selectionIndex" method is wrong, or when invoked by an IB binding is wrapped in an NSNumber wrapper, that they didn't tell me about.+

    This would appear to be the case. Under 10.4 it returned an +unsigned int+ all C-like but the layer of definitions they added to 10.5 to allow side by side 64 and 32 bit versions may have made them re-do this. I don't have a 10.5 machine handy to check to see if the docs have been updated but if it surprised you then probably not.

    +If you have any insight on the issue of why removing an object from an array does not trigger the KVO for that array model object, I would appreciate hearing it. My "playerArray" is an NSMutableArray, bound to an NSArrayController, and has the @property (readwrite, copy) and @synthesize compiler instructions in the code.+

    So your NSArrayController is bound to the correct Object Class Name eg Show and your class has the requisite parts:



    //show.h file
    @interface show : NSObject {
    NSMutableDictionary * properties;
    NSMutableArray * workers;
    }

    -(NSMutableDictionary *) properties;
    -(void) setProperties: (NSDictionary *)newProperties;

    -(NSMutableArray *) workers;
    -(void) setWorkers: (NSArray *) newWorkers;

    //And in the .m file

    #import "show.h"

    @implementation show
    - (id) init
    {
    if (self = [super init])
    //snipped code
    }

    -(NSMutableDictionary *) properties{
    return properties;
    }

    -(void) setProperties: (NSDictionary *)newProperties{
    if (properties != newProperties)
    {
    [properties autorelease];
    properties = [[NSMutableDictionary alloc] initWithDictionary: newProperties];
    }
    }

    -(NSMutableArray *) workers{
    return workers;
    }

    -(void) setWorkers: (NSArray *) newWorkers{
    if (workers != newWorkers)
    {
    [workers autorelease];
    workers = [[NSMutableArray alloc] initWithArray: newWorkers];
    }
    }



    and the compiler isn't throwing any warnings about incomplete implementation or anything? If not that should do it. Something as simple as procedure name typo can throw everything into the trashcan so watch those compiler warnings about incomplete implementations. Also be strict about adding methods to the .h file and not just the .m file as you code. (I do this and then you get used to ignoring the compiler warnings - but they're not always wrong.) Also be careful of the temptation to stub in or copy/paste your getter and setter code from one object to another because it's really easy to miss a name change in the copied code after you've done that and will allow your code to compile but then fail mysteriously.

    I haven't done bindings under 10.5 yet but I'm assuming the requirements are the same. Bindings are powerful but are also very black box - like magic when they work and mysterious when they don't. I know this example doesn't match yours exactly but hopefully it's close enough that it will help. I wouldn't necessarily recommend bindings right off if you're new to Cocoa but if you have to learn them eventually why not start off with them?

    =Tod

    PS There is no { /code } tag it is simply two { code } tags bracketing the code.
  • Harold Holbrook Level 1 Level 1 (35 points)
    Nope. No compiler warnings.

    I took out the @synthesize and @property compiler directives and manually wrote the setter and getter for the array. The Model array is updated, but the View isn't.

    Let me describe the connections

    In IB, I have two TableViews and two NSArrayControllers. They are both bound to the same model object, playerArray. I need two identical TableViews because I need different selections in them to operate on in my code.

    Bindings for the left TableView:

    Bind To: voterController
    Controller Key: arrangedObjects
    Model Key Path: playerName (each element of playerArray is an NSMutableDictionary, and one of the keys in the dictionary is playerName.)

    Bindings for the ArrayController "voterController":

    Bind To: mainController
    Controller Key: {blank}
    Model Key Path: playerArray
    This controller is specified to have a Class of NSArrayController. It will not let me set it to "VoterController." (see below)

    mainController:
    This is a simple NSObject with Class of VoterController
    No Bindings. Not allowed with an NSObject I guess. I don't understand File's Owner, but my guess is that this is taking the place of whatever File's Owner does.

    I coded the setter and getter for playerArray as per your kind example.

    Now for the Xcode files

    VoterController.h


    #import <Cocoa/Cocoa.h>
    #import "VoteeController.h"

    @interface VoterController : NSObject {
    NSMutableArray *playerArray, *roleNumsArray, *roleTemplate;
    NSString *path, *tempString;
    NSScanner *flavorScanner, *roleScanner;
    NSURLRequest *request;
    NSData *theC9FlavorData;
    NSData *theRandomRolesData;
    NSMutableString *postText, *voteString;
    NSInteger theRoleInteger, C9Flavor, indexOfTheWhackedOne;
    BOOL fileFlag;
    }
    @property (readonly) NSMutableString *postText;

    //User actions
    - (void) voteFor:(NSArray *)votee byVoters:(NSArray *)voters;
    - (void) nightKill: (NSNumber *) theWhackedOne;
    - (void) reset;
    - (void) generateRoles;
    - (void) deleteRoles;

    //Internal functions
    - (NSMutableArray *) playerArray;
    - (void) setPlayerArray: (NSArray *) newPlayerArray;
    - (NSString *) makeVoteStringFromVoters:aVoter;
    - (BOOL) checkForTwilight:(NSMutableDictionary *)aPlayer;
    - (void) unVote: (NSNumber *) aVoter;
    - (int) threshold;
    - (void) saveToFile;
    - (NSMutableString *) postText;
    @end


    VoterController.m: (-nightKill: is where the Model deletion of an array element works but does not update the view)



    #import "VoterController.h"
    #import "VoteeController.h"

    @implementation VoterController

    - (id) init
    {
    if (self = [super init])
    {
    NSBundle *mafiaBundle = [NSBundle mainBundle];
    path = [mafiaBundle pathForResource:@"com.jlsoftware.mafia.playerTable" ofType:@"plist"];
    playerArray = [[NSMutableArray arrayWithContentsOfFile:path]mutableCopy];
    roleTemplate = [NSMutableArray arrayWithObjects:
    @"Townie", @"Townie", @"Townie", @"Townie",
    @"Townie", @"Mafia Goon", @"Mafia Goon", nil];
    }
    return self;
    }

    - (NSMutableArray *) playerArray
    {
    return playerArray;
    }

    - (void) setPlayerArray: (NSArray *) newPlayerArray
    {
    [playerArray autorelease];
    playerArray = [[NSMutableArray alloc] initWithArray: playerArray];
    }

    - (BOOL) checkForTwilight:(NSMutableDictionary *)aPlayer
    {return ([[aPlayer valueForKey:@"voteCount"]intValue] >= [self threshold]);}

    - (void) unVote:(NSNumber *) aVoter
    {
    NSLog(@"unVote:aVoter=%@", aVoter);
    [self postText];
    }

    - (int) threshold
    { return ([playerArray count] / 2) + 1;}

    - (NSString *) makeVoteStringFromVoters:aVotee
    {
    if ([aVotee valueForKey:@"voteCount"] == 0)
    {return @"";}

    else
    {
    voteString =
    [[NSString stringWithFormat:
    @"(%@)",[[[aVotee valueForKey:@"votersFor"]allValues]
    componentsJoinedByString:@", "]] mutableCopy];
    return voteString;
    }
    }

    - (void) saveToFile
    {fileFlag = [playerArray writeToFile:path atomically:NO];}

    - (NSMutableString *) postText
    {
    postText = [NSMutableString stringWithFormat:
    @"Official Vote Count: Needed To Lynch:%d ", [self threshold]];
    for (NSMutableDictionary *thePlayer in playerArray)
    {
    int currVote = [[thePlayer valueForKey:@"voteCount"]intValue];
    if (currVote > 0)
    {
    postText = [[postText stringByAppendingFormat:@"%@ - %@ - %@ (L-%d) ",
    [thePlayer valueForKey:@"playerName"],
    [thePlayer valueForKey:@"voteCount"],
    [thePlayer valueForKey:@"votersForString"],
    [self threshold] - currVote] mutableCopy];
    }
    }
    [self saveToFile];
    return postText;
    }

    - (void) reset
    {
    for (NSMutableDictionary *thePlayer in playerArray)
    {
    [thePlayer setValue:0 forKey:@"voteCount"];
    [thePlayer setObject: @"" forKey:@"votersForString"];
    [thePlayer setObject: @"" forKey:@"currentVotedPlayer"];
    [thePlayer setObject: [NSMutableDictionary dictionaryWithCapacity:4] forKey:@"votersFor"];
    }
    [self postText];
    }

    - (void) nightKill:(NSNumber *) theWhackedOne
    {
    NSLog(@"theWhackedOne=%@",theWhackedOne);
    [playerArray removeObjectAtIndex:[theWhackedOne integerValue]];
    [self reset];
    [self postText];
    return;
    }

    - (void) addPlayer:thePlayerNameString
    {
    NSBeep();
    return;
    }

    - (void) deleteRoles
    {
    NSBeep();
    NSBeep();
    NSBeep();
    return;
    }

    @end
  • Tod Kuykendall Level 4 Level 4 (2,270 points)
    +I took out the @synthesize and @property compiler directives and manually wrote the setter and getter for the array. The Model array is updated, but the View isn't.+

    This is alot of code to digest, but I think your issues lie in how your tables are bound. The easiest way to do this is to use the Value binding at the column level if NSTableView.

    +No Bindings. Not allowed with an NSObject I guess.+

    I think the missing piece you may be looking for is the NSObjectContoller to sit between your NSObject and the various controllers. It seems a little more convoluted but it makes a nice bridged set-up. An excellent bindings tutorial is here if you haven't seen it: http://cocoadevcentral.com/articles/000080.php

    It also demostrates the binding of controllers to tables and may help you with your other issues.

    Good luck,

    =Tod