How to add an object to a programmatically bound NSMutableArray? - objective-c

I have an NSDocument which has the following structure:
#interface MyDocument : NSDocument
{
NSMutableArray *myArray;
IBOutlet NSArrayController *myArrayController;
IBOutlet MyView *myView;
}
#end
I instantiate the NSArrayController and the MyView in MyDocument.xib, and have made the connections to the File's Owner (MyDocument), so I am pretty sure that from the point of view of Interface Builder, I have done everything correctly.
The interface for MyView is simple:
#interface MyView : NSView {
NSMutableArray *myViewArray;
}
#end
Now, in MyDocument windowControllerDidLoadNib, I have the following code:
- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
[super windowControllerDidLoadNib:aController];
[myArrayController setContent:myArray];
// (This is another way to do it) [myArrayController bind:#"contentArray" toObject:self withKeyPath:#"myArray" options:nil];
[myView bind:#"myViewArray" toObject:myArrayController withKeyPath:#"arrangedObjects" options:nil];
}
In the debugger, I have verified that myViewArray is an NSControllerArrayProxy, so it would appear that my programmatic binding is correct. However, when I try to add objects in MyView's methods to the MyView myViewArray, they do not appear to update the MyDocument's myArray. I have tried both of the following approaches:
[myViewArray addObject:value];
[self addMyViewArraysObject:value];
(The second approach causes a compiler error, as expected, but I thought that the Objective-C runtime would "implement" this method per my limited understanding of KVO.)
Is there something wrong with how I'm trying to update myViewArray? Is there something wrong with my programmatic binding? (I am trying to do this programmatically, because MyView is a custom view and I don't want to create an IB palette for it.)

The problem is that you're mutating your array directly. Implement indexed accessor methods and call those.
KVO overrides your accessor methods (as long as you conform to certain formats) and posts the necessary notifications. You don't get this when you talk directly to your array; anything bound to the property won't know that you've changed the property unless you explicitly tell it. When you use your accessor methods, KVO tells the other objects for you.
The only time to not use your accessor methods (synthesized or otherwise) is in init and dealloc, since you would be talking to a half-inited or -deallocked object.
Once you're using your own accessor methods to mutate the array, and thereby getting the free KVO notifications, things should just work:
The view, when mutating its property, will automatically notify the array controller, which mutates its content property, which notifies your controller.
Your controller, when mutating its property, will automatically notify the array controller, which mutates its arrangedObjects property, which notifies the view.

I can see two possibilities here:
First, do you instantiate the NSMutableArray object (and release it) in your MyDocument class? It should look something like this:
- (id)init
{
if ((self = [super init]) == nil) { return nil; }
myArray = [[NSMutableArray alloc] initWithCapacity:0];
return self;
}
- (void)dealloc
{
[myArray release];
[super dealloc];
}
Second, did you declare myViewArray as a property in MyView? It should look something like this:
// MyView.h:
#interface MyView : NSView
{
NSMutableArray * myViewArray;
}
#property (assign) NSMutableArray * myViewArray;
#end
// MyView.m:
#implementation MyView
#synthesize myViewArray;
#end
Other than that, it looks to me like you have done all of the binding properly.
update: How about using the NSArrayController to add items to the array:
// MyView.h:
#interface MyView : NSView
{
NSMutableArray * myViewArray;
IBOutlet NSArrayController * arrayController;
}
#property (assign) NSMutableArray * myViewArray;
- (void)someMethod;
#end
// MyView.m:
#implementation MyView
#synthesize myViewArray;
- (void)someMethod
{
id someObject = [[SomeClass alloc] init];
[arrayController addObject:[someObject autorelease]];
}
#end

The problem appears to be that I had been binding MyView's myViewArray to the NSArrayController's arrangedObjects property instead of its content property.
When binding to arrangedObjects, I found that the actual object pointed to by myViewArray was an instance of NSControllerArrayProxy. I didn't find a definitive answer as to what this object actually does when I searched online for more information on it. However, the code examples I found suggest that NSControllerArrayProxy is intended to expose conveniences for accessing the properties of objects in the array, rather than the objects (in the array) themselves. This is why I believe that I was mistaken in binding to arrangedObjects.
The solution was to instead bind MyView's myViewArray to the NSArrayController's content property:
- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
[super windowControllerDidLoadNib:aController];
[myArrayController setContent:myArray];
[myView bind:#"myViewArray" toObject:myArrayController withKeyPath:#"content" options:nil];
}
Although this appears to work, I am not 100% sure that it is correct to bind to content in this case. If anyone can shed some light on programmatically binding to the various properties of an NSArrayController, I would welcome comments to this answer. Thanks.

First of all, there's nothing wrong with binding to arrangedObjects: an NSTableColumn, for instance, should have its content bound to arrangedObjects only, and its contentValues to arrangedObjects.someProperty.
The common mistake is to regard arrangedObjects as the content of an arrayController but that, as you have seen, will lead to grief: arrangedObjects is a representation of the way the arrayController has currently arranged the objects in its content, not the content itself.
That said, the way to bind an array to an arrayController is:
[self.myArrayController bind:NSContentArrayBinding
toObject:self
withKeyPath:#"myView.myViewArray"
options:nil];
Are you sure, by the way, your view needs to hold the myViewArray? That usually falls under the responsibility of a controller or model object.
Now you can add objects by calling addObject on the arrayController, since that is the controller's responsibility.
[self.myArrayController addObject: anObject]

Related

NSMutableArrays are null

I have an NSViewController subclass with:
#property (retain) NSMutableArray* entities;
#property (retain) NSMutableArray* tiles;
In my -init method, both arrays are created with +new, and are given one object each. After that, I call NSLog(#"%#, %#", entities, tiles);, and it gives me just as expected:
2012-12-30 15:07:04.160 Project Land III[2177:303] (
"<RBEntity: 0x100508170>"
), (
"<RBTile: 0x100508470>"
)
I can click a button on the view, though, which calls the same log function, and it spit out this:
2012-12-30 15:07:06.071 Project Land III[2177:303] (null), (null)
I've been stuck on this problem in some form or another for days. Why in the world are the arrays null?
I'm more than happy to post more code, just let me know!
Interface:
#import <Cocoa/Cocoa.h>
#import "RBEntity.h"
#import "RBTile.h"
#interface RBMainViewController : NSViewController {
NSMutableArray* _entities;
NSMutableArray* _tiles;
}
#property (retain) NSMutableArray* entities;
#property (retain) NSMutableArray* tiles;
- (IBAction)log:(id)sender;
#end
My -init method:
- (id)init {
self = [super init];
self.entities = [NSMutableArray new];
self.tiles = [NSMutableArray new];
[self.entities addObject:[RBEntity entityWithLocation:NSMakePoint(4, 5) type:FACEEATER]];
[self.tiles addObject:[RBTile tileWithLocation:NSMakePoint(10, 2) type:GRASS]];
NSLog(#"%#, %#", self.entities, self.tiles);
return self;
}
In my -init method, both arrays are created with +new, and are given one object each. After that, I call NSLog(#"%#, %#", entities, tiles);, and it gives me just as expected:
2012-12-30 15:07:04.160 Project Land III[2177:303] (
"<RBEntity: 0x100508170>"
), (
"<RBTile: 0x100508470>"
)
I can click a button on the view, though, which calls the same log function, and it spit out this:
2012-12-30 15:07:06.071 Project Land III[2177:303] (null), (null)
This is a very common novice mistake.
You have two RBMainViewController objects. One of them, you presumably created in code in one of your other .m files, by saying something like [[RBMainViewController alloc] init]. The other, you created in a nib, probably by dragging it into the nib.
(Note: The nib that you created that VC in is not the VC's nib. That would be circular, to have the VC's nib containing the VC that is loading the nib. The VC that doesn't have its arrays resides in nib A, and each VC will load nib B.)
The VC that you created in a nib is the one whose view appears on the screen. Because that object never received an init message (it was initialized with some other initWith… message instead), you never created its arrays. The view controller you created with init, which does have its arrays, is not visible on the screen (otherwise you would have clicked on its button, rather than the other's, and you'd have seen the arrays in the output).
The solution involves two changes.
The first is to change your implementation of init to be an implementation of initWithNibName:bundle: instead. Like so:
- (instancetype) initWithNibName:(NSString *)nibName
bundle:(NSBundle *)bundle
{
self = [super initWithNibName:nibName bundle:bundle];
if (self != nil) {
<#...#>
}
return self;
}
If you want to continue using init to create your VC in other code, fine, but your implementation of -[RBMainViewController init] should simply send initWithNibName:bundle: to self and return the result.
- (instancetype) init {
return [self initWithNibName:<#nibName#> bundle:<#bundle#>];
}
You also need to delete one of the two view controllers. We'd need to see the code and the nib to know which. If you delete the one you created in code, you may want to create an outlet in that class and connect it in the nib. If you delete the one in the nib, any outlet connections you established in that nib to that VC, you'll need to re-create in code.
Write a custom getter/setter for one of these properties, and put a breakpoint there to see who's resetting the value. If that doesn't catch the problem, and your property is still being reset, you're probably accessing them from an instance of your NSViewController that hasn't been initialised by your init, thus those properties were always uninitialised.
I believe you will need to use self.entites and self.tiles when working with those objects in your class.
EDIT (after interface added)
You will need to have something in your interface like:
#interface RBMainViewController : NSViewController{
NSMutableArray* _entities;
NSMutableArray* _tiles;
}
#property (retain) NSMutableArray* entities;
#property (retain) NSMutableArray* tiles;
- (IBAction)log:(id)sender;
#end
Then you will need to add this to the implementation:
#synthesize entities = _entities;
#synthesize tiles = _tiles;

How to pass values between 2 View Controllers without protocol?

I have two view controllers, call them viewA and ViewB
All the action happens in main view - ViewA
A menu button is hit, brings up ViewB, all is well and the menu comes up
Now, the user touches one IBAction button, which programmatically just needs to:
change the value of a BOOL, call it myBOOL to YES
dismiss ViewB
pass the myBOOL variables current state of YES back to ViewA
I have declared the same BOOL, set property, synthesized on both Views, but per my NSLog upon dismissal of ViewB and loading back up ViewA, it reverts back to NO
So I know I'm going off on a tangent, I just want to know if you can send the value of a BOOL between two controllers and if so, please show me an example... as searches have found Protocols and Delegate examples with NSString's, and when I attempt with a BOOL I get stuck in an import loop, however I've read that its possible to make a global BOOL, as bad design as it is, I just need to get over this block for now.
A question on this topic should really be focused more on NSNotificationCenter rather than NSUserDefaults, taking note that both are singletons.
NSUserDefaults:
The purpose of this class is NOT to pass variables between classes. It's purpose is, well, to store user's defaults. (ie preferences, settings, ... etc).
NSNotificationCenter:
This class is very handy, and has many different uses, one of which is to broadcast a variable for any class to receive. The receiving class is called the observer. This pattern is known as the Observer Pattern.
NOTE: The NSUserDefaults approach has the advantage of allowing you to set the variable before the other class is initialized, and can be retrieved at anytime. However, that's really sloppy (IMHO) and considered bad practice.
Quick and Dirty code sample on NSNotificationCenter:
// upon initializing the class that wants to observe the changes, we add it as an observer.
// So, somewhere in the A.m, upon being initialized (init, maybe?).
- (id)init {
if (self = [super init]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(calledUponNotif:)
name:#"MyObserveKey"
object:nil];
}
return self;
}
// the selector should look something like this:
- (void)calledUponNotif:(NSNotification *)notif {
id sentVar = [notif object];
}
// Somewhere in the B.m
[[NSNotificationCenter defaultCenter] postNotificationName:#"MyObserveKey"
object:varToSend];
Another note: After calling the postNotification method, the registered selector in the other class will be called synchronously, so you don't have to worry about that.
This is not a good encapsulation answer but without being able to use protocols or delegates I don't believe it will have good encapsulation.
You can also create a global variable that you can set in one view controller and access in another.
ViewControllerOne.h
extern NSString *globalVariable;
#interface ViewControllerOne
#end
ViewControllerOne.m
#import "ViewControllerOne.h"
#implementation ViewControllerOne
NSString *globalVariables = #"Some String in the variable to access in second controller";
#end
ViewControllerTwo.m
#import "ViewControllerTwo.h"
#import "ViewControllerOne.h"
#implemetation ViewControllerTwo
- (void)viewDidLoad
{
NSLog("%#", globalVariables);
}
#end
This will print out into the console
****CONSOLE****
Some String in the variable to access in second controller
There is View-independent value keeping tool. You can use:
[[NSUserDefaults standardUserDefaults]setObject:<#(id)#> forKey:<#(NSString *)#>]
For example, you inputs strings or datas in A view, you can store them in above variables. And then, in B view, you can use them by below code:
[[NSUserDefaults standardUserDefaults]objectOrKey:<#(NSString *)#>]
These are a example of NSUserDefaults data using:
View A:
- (void)textFieldDidEndEditing:(UITextField *)sender
{
if (sender == homepage) {
[[NSUserDefaults standardUserDefaults]
setURL:[NSURL URLWithString:homepage.text] forKey:Ever5secHomepagePrefKey];
if( [homepage canResignFirstResponder] ) {
[homepage resignFirstResponder];
}
} else if (sender == userId) {
[[NSUserDefaults standardUserDefaults]
setObject:userId.text forKey:Ever5secUserIdPrefKey];
objectForKey:Ever5secUserIdPrefKey]);
if( [userId canResignFirstResponder] ) {
[userId resignFirstResponder];
}
} else if (sender == password) {
[[NSUserDefaults standardUserDefaults]
setObject:password.text forKey:Ever5secPasswordPrefKey];
if( [password canResignFirstResponder] ) {
[password resignFirstResponder];
}
}
}
View B:
userId.text = [[NSUserDefaults standardUserDefaults]
objectForKey:Ever5secUserIdPrefKey];
password.text = [[NSUserDefaults standardUserDefaults]
objectForKey:Ever5secPasswordPrefKey];
homepage.text = [[[NSUserDefaults standardUserDefaults]
URLForKey:Ever5secHomepagePrefKey]
description];
You don't need to use NSNotificationCenter, NSUserDefaults or global variables.
As long as the view controllers are related (and looking at the OP's question, they certainly seem to be) you can simply set the view controllers up to hold a reference to each another (with one of the references being weak of course in order to avoid a "retain", or "strong reference", cycle). Then each view controller can set the property on the other view controller as needed. Example follows...
NB: This concept is valid for any two related view controllers. However, the following code assumes that:
The view controllers in question are related via a navigation controller and the second view controller is attached to the first via a push segue.
iOS 5.0 or above is in use (as it makes use of storyboards).
FirstViewController.h
#interface FirstViewController : UIViewController
/* Hold the boolean value (or whatever value should be
set by the second view controller) in a publicly
visible property */
#property (nonatomic, assign) BOOL someBooleanValue;
/* Provide a method for the second view controller to
request the first view controller to dismiss it */
- (void)dismissSecondViewController;
#end
FirstViewController.m
#import "FirstViewController.h"
#import "SecondViewController.h"
#implementation FirstViewController
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
/* Get the reference to the second view controller and set
the appropriate property so that the secondViewController
now has a way of talking to the firstViewController */
SecondViewController *vc = [segue destinationViewController];
vc.firstViewController = self;
}
- (void)dismissSecondViewController
{
// Hide the secondViewController and print out the boolean value
[self.navigationController popViewControllerAnimated:YES];
NSLog(#"The value of self.someBooleanValue is %s", self.someBooleanValue ? "YES" : "NO");
}
#end
SecondViewController.h
#import "FirstViewController.h"
#interface SecondViewController : UIViewController
// Create a 'weak' property to hold a reference to the firstViewController
#property (nonatomic, weak) FirstViewController *firstViewController;
#end
SecondViewController.m
#implementation SecondViewController
/* When required (in this case, when a button is pressed),
set the property in the first view controller and ask the
firstViewController to dismiss the secondViewController */
- (IBAction)buttonPressed:(id)sender {
self.firstViewController.someBooleanValue = YES;
[self.firstViewController dismissSecondViewController];
}
#end
Of course, the most correct way to handle this sort of inter-viewController communication is to use protocols/delegates/data sources so that the SecondViewController doesn't need to know the specifics of its parent/owner object. However, sometimes it is quicker/simpler to build a solution like this just to prove the concept. Then if all is well and the code is worth keeping, refactor to use protocol(s).
In the case where view controllers don't - and shouldn't - know about each other, it may be necessary to use NSNotificationCenter. Don't use global variables or NSUserDefaults for communication between view controllers.
There are two options available storing and retrieving data in different view controllers.
1)NSUserDefaults is best option for storing data and accessing in any other view controllers.
The NSUserDefaults class provides convenience methods for accessing common types such as float, double, integer, Boolean.
A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary.
This is very easy and best method for storing and retrieving data.
if you want to read about NSUserDefaults, here I am sharing document.
NsuserDefaults Document.
2) You would create properties when you want them to be accessible outside the class or other view controllers.
Create property in this way. #property (nonatomic, retain) NSArray *arrayData; and then you can use this array value in other view controllers also.
Properties replace the accessor methods for objects.
You can see my answer here. Pass value from one view controller to another
There are two options available storing and retrieving data in different view controllers.
1)NSUserDefaults is best option for storing data and accessing in any other view controllers.
The NSUserDefaults class provides convenience methods for accessing common types such as float, double, integer, Boolean.
A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary.
This is very easy and best method for storing and retrieving data.
if you want to read about NSUserDefaults, here I am sharing document.
[NsuserDefaults Document.][1]
2) You would create properties when you want them to be accessible outside the class or other view controllers.
Create property in this way. #property (nonatomic, retain) NSArray *arrayData; and then you can use this array value in other view controllers also.
Properties replace the accessor methods for objects.
I think best way to use powerful features of blocks in below ways.
In ViewB.h
typedef void (^CompletionHandler)(BOOL myBool);
#interface ViewB : UIViewController {
CompletionHandler completionHandler;
}
- (void)dismissHandler:(CompletionHandler)handler;
In ViewB.m
- (void)dismissHandler:(CompletionHandler)handler {
completionHandler = handler;
}
- (IBAction)dismiss:(id)sender {
completionHandler (YES); // your yes no logic here
}
In ViewA.m
- (IBAction)showPopup:(id)sender {
ViewB *vc = [[ViewB alloc] init];
[self.view addSubview:vc.view];
[vc dismissHandler:^(BOOL myBool) {
if (myBool) {
//Do your work;
}
}];
}

On Objective-C delegates, clarification needed

If my UIView (MyUIView) declares and #synthesize(s)
#property (nonatomic, weak) id<UIScrollViewDelegate> delegate;
and MyController is declared as
#interface MyController : UIViewController <UIScrollViewDelegate>
and the following is true (inside MyController)
if ([self.view isKindOfClass:[MyUIView class]]){
NSLog(#"yes"); // We see this
}
Why is the following a problem?
self.view.delegate = self; // Does not see "delegate"
Meanwhile, if i manually create an instance of MyView, it works:
MyView *c = [MyView new];
c.delegate = self; // works fine
Because the view property of UIViewController is declared as UIView *, and UIView doesn't have a delegate property. Note that property (and method) lookup by the compiler is not dynamic, as it's done at compile time.
Use
((MyUIView *)(self.view)).delegate = self;
instead.
Because just by checking that the view property is a MyUIView does not automatically cast it to one.
You want something like this:
if ([self.view isKindOfClass:[MyUIView class]]) {
MyUIView *myView = (MyUIView*)self.view;
myView.delegate = self;
}
Notice how after checking, I create a variable of type MyUIView* which the compiler will then be able to "see" the delegate property on it. It's all about types :-).
[self.view isKindOfClass:[MyUIView class]] is a run-time check and can determine what kind of object has actually been assigned to view. On the other hand, the compiler is complaining because it doesn't have that information and only knows that view is some kind of UIView.
Use a cast on self.view to tell the compiler that you know what's going to be stored there.

Singleton NSMutableArray accessed by NSArrayController in multiple NIB's

Early warning - code sample a little long...
I have a singleton NSMutableArray that can be accessed from anywhere within my application. I want to be able to reference the NSMutableArray from multiple NIB files but bind to UI elements via NSArrayController objects. Initial creation is not a problem. I can reference the singleton NSMutableArray when the NIB gets loaded and everything appears fine.
However, changing the NSMutableArray by adding or removing objects does not kick off KVO to update the NSArrayController instances. I realize that "changing behind the controller's back" is considered a no-go part of Cocoa-land, but I don't see any other way of programmatically updating the NSMutableArray and letting every NSArrayController be notified (except it doesn't work of course...).
I have simplified classes below to explain.
Simplified singleton class header:
#interface MyGlobals : NSObject {
NSMutableArray * globalArray;
}
#property (nonatomic, retain) NSMutableArray * globalArray;
Simplified singleton method:
static MyGlobals *sharedMyGlobals = nil;
#implementation MyGlobals
#synthesize globalArray;
+(MyGlobals*)sharedDataManager {
#synchronized(self) {
if (sharedMyGlobals == nil)
[[[self alloc] init] autorelease];
}
return sharedMyGlobals;
}
-(id) init {
if(self = [super init]) {
self.globals = [[NSMutableArray alloc] init];
}
return self
}
// ---- allocWithZone, copyWithZone etc clipped from example ----
In this simplified example the header and model for objects in the array:
Header file:
#interface MyModel : NSObject {
NSInteger myId;
NSString * myName;
}
#property (readwrite) NSInteger myId;
#property (readwrite, copy) NSString * myName;
-(id)initWithObjectId:(NSInteger)newId objectName:(NSString *)newName;
#end
Method file:
#implementation MyModel
#synthesize myId;
#synthesize myName;
-(id)init {
[super init];
myName = #"New Object Name";
myId = 0;
return self;
}
#end
Now imagine two NIB files with appropriate NSArrayController instances. We'll call them myArrayControllerInNibOne and myArrayControllerInNib2. Each array controller in the init of the NIB controller sets the content of the array:
// In NIB one init
[myArrayControllerInNibOne setContent: [[MyGlobals sharedMyGlobals].globalArray];
// In NIB two init
[myArrayControllerInNibTwo setContent: [[MyGlobals sharedMyGlobals].globalArray];
When each NIB initializes the NSArrayController binds correctly to the shared array and I can see the array content in the UI as you would expect. I have a separate background thread that updates the global array when content changes based on an external event. When objects need to be added in this background thread, I simply add them to the array as follows:
[[[MyGlobals sharedMyGlobals].globalArray] addObject:theNewObject];
This is where things fall apart. I can't call a willChangeValueForKey and didChangeValueForKey on the global array because the shared instance doesn't have a key value (should I be adding this in the singleton class?)
I could fire off an NSNotification and catch that in the NIB controller and either do a [myArrayControllerInNibOne rearrangeObjects]; or set the content to nil and reassign the content to the array - but both of these seems like hacks and. moreover, setting the NSArrayController to nil and then back to the global array causes a visual flash within the UI as the content is cleared and re-populated.
I know I could add directly to the NSArrayController and the array gets updated, but I don't see a) how the other NSArrayController instances would be updated and b) I don't want to tie my background thread class explicitly to a NIB instance (nor should I have to).
I think the correct approach is to either fire off the KVO notification somehow around the addObject in the background thread, or add something to the object that is being stored in the global array. But I'm at a loss.
As a point of note I am NOT using Core Data.
Any help or assistance would be very much appreciated.
Early warning - answer a little long…
Use objects that model your domain. You have no need for singletons or globals, you need a regular instance of a regular class. What Objects are your storing in your global array? Create a class that represents that part of your model.
If you use an NSMutableArray as storage it should be internal to your class and not visible to outside objects. eg if you are modelling a zoo, don't do
[[[MyGlobals sharedMyGlobals].globalArray] addObject:tomTheZebra];
do do
[doc addAnimal:tomTheZebra];
Dont try to observe a mutable array - you want to observe a to-many property of your object. eg. instead of
[[[MyGlobals sharedMyGlobals].globalArray] addObserver:_controller]
you want
[doc addObserver:_controller forKeyPath:#"animals" options:0 context:nil];
where doc is kvo compliant for the to-many property 'anaimals'.
To make doc kvo compliant you would need to implement these methods (Note - you don't need all these. Some are optional but better for performance)
- (NSArray *)animals;
- (NSUInteger)countOfAnimals;
- (id)objectInAnimalsAtIndex:(NSUInteger)i;
- (id)AnimalsAtIndexes:(NSIndexSet *)ix;
- (void)insertObject:(id)val inAnimalsAtIndex:(NSUInteger)i;
- (void)insertAnimals:atIndexes:(NSIndexSet *)ix;
- (void)removeObjectFromAnimalsAtIndex:(NSUInteger)i;
- (void)removeAnimalsAtIndexes:(NSIndexSet *)ix;
- (void)replaceObjectInAnimalsAtIndex:(NSUInteger)i withObject:(id)val;
- (void)replaceAnimalsAtIndexes:(NSIndexSet *)ix withAnimals:(NSArray *)vals;
Ok, that looks pretty scary but it's not that bad, like i said you don't need them all. See here. These methods dont need to be part of the interface to your model, you could just add:-
- (void)addAnimal:(id)val;
- (void)removeAnimal:(id)val;
and write them in terms of the kvc accessors. The key point is it's not the array that sends notifications when it is changed, the array is just the storage behind the scenes, it is your model class that send the notifications that objects have been added or removed.
You may need to restructure your app. You may need to forget about NSArrayController altogether.
Aaaaaannnnnyyywaaayyy… all this gets you nothing if you do this
[[[MyGlobals sharedMyGlobals].globalArray] addObject:theNewObject];
or this
[doc addAnimal:tomTheZebra];
from a background thread. You can't do this. NSMutableArray isn't thread safe. If it seems to work then the best that will happen is that the kvo/binding notification is delivered on the background as well, meaning that you will try to update your GUI on the background, which you absolutely cannot do. Making the array static does not help in any way i'm afraid - you must come up with a strategy for this.. the simplest way is performSelectorOnMainThread but beyond that is another question entirely. Threading is hard.
And about that static array - just stop using static, you don't need it. Not because you have 2 nibs, 2 windows or anything. You have an instance that represents your model and pass a pointer to that to you viewControllers, windowControllers, whatever. Not having singletons/static variables helps enormously with testing, which of course you should be doing.

NSCollectionView draws nothing

I'm trying to set up an NSCollectionView (I have done this successfully in the past, but for some reason it fails this time).
I have a model class called "TestModel", and it has an NSString property that just returns a string (just for testing purposes right now). I then have an NSMutableArray property declaration in my main app delegate class, and to this array I add instances of the TestModel object.
I then have an Array Controller that has its Content Array bound the app delegate's NSMutableArray. I can confirm that everything up to here is working fine; NSLogging:
[[[arrayController arrangedObjects] objectAtIndex:0] teststring]
worked fine.
I then have all the appropriate bindings for the collection view set up, (itemPrototype and content), and for the Collection View Item (view). I then have a text field in the collection item view that is bound to Collection View Item.representedObject.teststring. However NOTHING displays in the collection view when I start the app, just a blank white screen. What am I missing?
UPDATE: Here is the code I use (requested by wil shipley):
// App delegate class
#interface AppController : NSObject {
NSMutableArray *objectArray;
}
#property (readwrite, retain) NSMutableArray *objectArray;
#end
#implementation AppController
#synthesize objectArray;
- (id)init
{
if (self = [super init]) {
objectArray = [[NSMutableArray alloc] init];
}
return self;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
TestModel *test = [[[TestModel alloc] initWithString:#"somerandomstring"] autorelease];
if (test) [objectArray addObject:test];
}
#end
// The model class (TestModel)
#interface TestModel : NSObject {
NSString *teststring;
}
#property (readwrite, retain) NSString *teststring;
- (id)initWithString:(NSString*)customString;
#end
#implementation TestModel
#synthesize teststring;
- (id)initWithString:(NSString*)customString
{
[self setTeststring:customString];
}
- (void)dealloc
{
[teststring release];
}
#end
And then like I said the content array of the Array Controller is bound to this "objectArray", and the Content of the NSCollectionView is bound to Array Controller.arrangedObjects. I can verify that the Array Controller has the objects in it by NSLogging [arrayController arrangedObjects], and it returns the correct object. Its just that nothing displays in the NSCollectionView.
UPDATE 2: If I log [collectionView content] I get nothing:
2009-10-21 08:02:42.385 CollViewTest[743:a0f] (
)
The problem is probably there.
UPDATE 3: As requested here is the Xcode project:
http://www.mediafire.com/?mjgdzgjjfzw
Its a menubar app, so it has no window. When you build and run the app you'll see a menubar item that says "test", this opens the view that contains the NSCollectionView.
Thanks
The problem is that your not correctly using KVC. There is two things you can do.
Method 1: Simple but not so elegant
Use the following code to add the object to the array
[[self mutableArrayValueForKey:#"objectArray"] addObject:test];
This isn't so elegant as you have to specify the variable using a string value, so you will not get compiler warnings when spelt incorrectly.
Method 2: Generate the KVO methods needed for the array "objectArray".
Select the property in your interface declaration
Select Scripts (the script icon in the menubar) > Code > Place
accessor decls on Clipboard
Paste the declarations in the
appropriate spot in your interface file
Select Scripts > Code > Place
accessor defs on Clipboard
Paste the definitions in the
appropriate spot in your implementation file
You can then use a method that looks like
[self insertObject:test inObjectArrayAtIndex:0];