How to pass values between 2 View Controllers without protocol? - objective-c

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

Related

Objective-c: Singleton - passing variables

I have a singleton that I'd like to use to manage the onscreen animation of my views. Here's my.
#import <Foundation/Foundation.h>
#interface OAI_AnimationManager : NSObject {
NSMutableDictionary* sectionData;
}
#property (nonatomic, retain) NSMutableDictionary* sectionData;
+(OAI_AnimationManager* )sharedAnimationManager;
- (void) checkToggleStatus : (UIView* ) thisSection;
#end
.m file
#import "OAI_AnimationManager.h"
#implementation OAI_AnimationManager
#synthesize sectionData;
+(OAI_AnimationManager *)sharedAnimationManager {
static OAI_AnimationManager* sharedAnimationManager;
#synchronized(self) {
if (!sharedAnimationManager)
sharedAnimationManager = [[OAI_AnimationManager alloc] init];
return sharedAnimationManager;
}
}
- (void) checkToggleStatus : (UIView* ) thisSection {
//get the section data dictionary
NSLog(#"%#", sectionData);
}
#end
You'll see in the .h file I added a NSMutableDictionary and am using #property/#synthesize for it's getter and setter.
In my ViewController I instantiate the animation manager as well as a series of subclasses of UIView called Section. With each one I store the data (x/y w/h, title, etc.) in a dictionary and pass that to the dictionary delcared in animation manager. In the Section class I also instantiate animation manager and add a UITapGestureRecognizer which calls a method, which passes along which section was tapped to a method (checkToggleStatus) in animation manager.
As you can I see in the method I am just logging sectionData. Problem is I am getting null for the value.
Maybe my understanding of singletons is wrong. My assumption was the class would only be instantiated once, if it was already instantiated then that existing object would be returned.
I do need all the other Section classes data as if one animates others animate in response and I can get around it by passing the tapped Section to the animation manager and doing [[Section superview] subviews] and then looping and getting the data from each that way but it seems redundant since that data is available in the ViewController when they are created.
Am I doing something wrong in trying to transfer that data? Is there a better solution? I am open to suggestions and criticisms.
Thanks
h file
#interface OAI_AnimationManager : NSObject
#property (nonatomic, retain) NSMutableDictionary* sectionData;
+(OAI_AnimationManager* )sharedAnimationManager;
- (void) checkToggleStatus : (UIView* ) thisSection;
#end
m file
static OAI_AnimationManager* _sharedAnimationManager;
#implementation OAI_AnimationManager
#synthesize sectionData = _sectionData;
+(OAI_AnimationManager *)sharedAnimationManager {
#synchronized(self) {
if (!_sharedAnimationManager) {
_sharedAnimationManager = [[OAI_AnimationManager alloc] init];
}
}
return _sharedAnimationManager;
}
- (void) checkToggleStatus : (UIView* ) thisSection {
//get the section data dictionary
NSLog(#"%#", _sectionData);
}
#end
Notice I moved your sectionData variable from the header and moved it to the implementation file. A while back, they changed it to where you can synthesize properties and specify their instance variable names along side it... hence:
sectionData = _sectionData;
I also added and underscore to the instance variable... this is a universal convention for private variables and it also will throw a compile error now if you try to type just sectionData as you did in the return statement of checkToggleStatus:. Now you either have to type self.sectionData or _sectionData.
You didn't include the code that creates an instance of your dictionary but I bet you didn't set it as self.sectionData = [[NSDictionary alloc] init] which means it would not retain the value and you would get null the next time you called it. Classic memory management mistake... I know it well because I learned the hard way hehehe

share NSArray between different UIViewControllers

I am making an application that uses a webService to get data in a JSON format... I get the data I parse them into a object NSArray ... and i use it .. it works fine ...
Now, if the user clicks a button I need to send him to an other Uiview ... which contains more data about the clicked object ..
The problem is here ... I don't want to request again and download the result from the server ... because i already did ... All I want is to have access to that NSArray that I have in the first UIViewController.
You can add on AnotherView.h another property:
#property (nonatomic, retain) NSArray *jsonData;
On AnotherView.m synthesize it. When you are going to to call AnotherView from InitialView, you can set jsonData with the data you retrieved on InitialView.
Create a custom initializer in your other view controller like so:
#import <UIKit/UIKit.h>
#interface OtherViewController : UIViewController
#property (nonatomic, strong) NSArray *myArray;
- (id)initWithArray:(NSArray *)anArray;
#end
Then implement it like so:
#import "OtherViewController.h"
#implementation OtherViewController
#synthesize myArray=_myArray;
- (id)initWithArray:(NSArray *)anArray {
if (!(self = [self initWithNibName:#"OtherViewController" bundle:nil]))
return nil;
if (!anArray) {
#throw [NSException exceptionWithName:#"OtherViewControllerBadInitCall" reason:#"array is nil" userInfo:nil];
}
_myArray = anArray;
return self;
}
//...
#end
You can then init and display your controller like so:
OtherViewController *otherViewController = [[OtherViewController alloc] initWithArray:greatJSONArray];
[self.navigationController pushViewController:otherViewController animated:YES];
There you go.
You can set the array as the property. You can either create a new class and set the array as the property and after you fetch the array, set the property. Or, you can create a property of the existing UIVIewController Class and pass the object.
Either way, you have to set property.
You could define a new property in your second ViewController that holds an NSArray and pass the firt array to the second ViewController before show it.
Well you have not outlined whether you send the data forward or backward. In the later case you will need to implement protocol and delegate(Define your own protocol) but for the prior case you just need to create the property of the Object you want to access in any other class. In case of web-services it is better to use protocol and delegates if u abide by the norms of MVC architecture.

What is best practice to interaction with object in objective-c?

My questions is next:
For example I have object A (this is data model object). Assume that object A have some property (for example request property). Also I have object B (this is my view object).
So my problem is next: when my data model will be changed (the value for request property changed) I want to know about this events in my view (object B)
How to create this interaction between object.
For example in request is written to "some_value" and after this object B immediately know about it.
Thanks for response!
You can use delegation pattern, NSNotifications, callback blocks and even KVO. Choice depends on situation, in your case delegate or callback block would work.
I would use Key Value Observing. Your view controller (not the view itself) would set itself up as an observer for the data model object and when it gets observer notifications, it would update the view.
[myDataObject addObserver: myViewController
forKeyPath: #"request"
options: NSKeyValueObservingOptionNew
context: nil];
// in the view controller you need
-(void) observeValueForKeyPath: (NSString*) path
ofObject: (id) aDataObject
change: (NSDictionary*) changeDictionary
context: (void*) context]
{
if (aDataObject == myDataObject
&& [path isEqualToString: #"request"])
{
// change you are interested in
}
// Call suoer implementation of this method if it implements it
}
Don't forget to remove the observer when you are done with it.
Also, be careful in a threaded environment. Observations are notified on the same thread that the change happens on. If this is not the main thread, you'll need to use -performSelectorOnMainThread:withObject:waitUntilDone: to make any changes to the UI.
If you just want object B to know whats up I would suggest using delegation.
If maybe later you want object C, D and E to know too what happend in object A i would suggest using NSNotification.
For example I have class DataModel. In this step I add observer for my property str. For object I will send my view controller.
.h
#import <Foundation/Foundation.h>
#interface DataModel : NSObject
#property (strong, nonatomic) NSString *str;
- (void)setUpObserver:(id)object;
#end
.m
#import "DataModel.h"
#implementation DataModel
#synthesize str;
- (void)setUpObserver:(id)object
{
[self addObserver:object forKeyPath: #"str" options: NSKeyValueObservingOptionNew context: nil];
}
#end
In my view controller
#import "DataModel.h"
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
dm = [[DataModel alloc] init];
[dm setUpObserver:self];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if (object == dm && [keyPath isEqualToString: #"str"])
{
NSLog(#"it's work");
}
}
- (IBAction)changeValue:(id)sender {
dm.str = #"test change value";
}
#end
This is my realization of KVO. Thanks JeremyP for explanation.

Instance Variables for Objective C Categories

I have a situation where it seems like I need to add instance variables to a category, but I know from Apple's docs that I can't do that. So I'm wondering what the best alternative or workaround is.
What I want to do is add a category that adds functionality to UIViewControllers. I would find it useful in all my different UIViewControllers, no matter what specific UIViewController subclass they extend, so I think a category is the best solution. To implement this functionality, I need several different methods, and I need to track data in between them, so that's what led me to wanting to create instance methods.
In case it's helpful, here's what I specifically want to do. I want to make it easier to track when the software keyboard hides and shows, so that I can resize content in my view. I've found that the only way to do it reliably is to put code in four different UIViewController methods, and track extra data in instance variables. So those methods and instance variables are what I'd like to put into a category, so I don't have to copy-paste them each time I need to handle the software keyboard. (If there's a simpler solution for this exact problem, that's fine too--but I would still like to know the answer to category instance variables for future reference!)
Yes you can do this, but since you're asking, I have to ask: Are you absolutely sure that you need to? (If you say "yes", then go back, figure out what you want to do, and see if there's a different way to do it)
However, if you really want to inject storage into a class you don't control, use an associative reference.
Recently, I needed to do this (add state to a Category). #Dave DeLong has the correct perspective on this. In researching the best approach, I found a great blog post by Tom Harrington. I like #JeremyP's idea of using #property declarations on the Category, but not his particular implementation (not a fan of the global singleton or holding global references). Associative References are the way to go.
Here's code to add (what appear to be) ivars to your Category. I've blogged about this in detail here.
In File.h, the caller only sees the clean, high-level abstraction:
#interface UIViewController (MyCategory)
#property (retain,nonatomic) NSUInteger someObject;
#end
In File.m, we can implement the #property (NOTE: These cannot be #synthesize'd):
#implementation UIViewController (MyCategory)
- (NSUInteger)someObject
{
return [MyCategoryIVars fetch:self].someObject;
}
- (void)setSomeObject:(NSUInteger)obj
{
[MyCategoryIVars fetch:self].someObject = obj;
}
We also need to declare and define the class MyCategoryIVars. For ease of understanding, I've explained this out of proper compilation order. The #interface needs to be placed before the Category #implementation.
#interface MyCategoryIVars : NSObject
#property (retain,nonatomic) NSUInteger someObject;
+ (MyCategoryIVars*)fetch:(id)targetInstance;
#end
#implementation MyCategoryIVars
#synthesize someObject;
+ (MyCategoryIVars*)fetch:(id)targetInstance
{
static void *compactFetchIVarKey = &compactFetchIVarKey;
MyCategoryIVars *ivars = objc_getAssociatedObject(targetInstance, &compactFetchIVarKey);
if (ivars == nil) {
ivars = [[MyCategoryIVars alloc] init];
objc_setAssociatedObject(targetInstance, &compactFetchIVarKey, ivars, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[ivars release];
}
return ivars;
}
- (id)init
{
self = [super init];
return self;
}
- (void)dealloc
{
self.someObject = nil;
[super dealloc];
}
#end
The above code declares and implements the class which holds our ivars (someObject). As we cannot really extend UIViewController, this will have to do.
I believe it is now possible to add synthesized properties to a category and the instance variables are automagically created, but I've never tried it so I'm not sure if it will work.
A more hacky solution:
Create a singleton NSDictionary which will have the UIViewController as the key (or rather its address wrapped as an NSValue) and the value of your property as its value.
Create getter and setter for the property that actually goes to the dictionary to get/set the property.
#interface UIViewController(MyProperty)
#property (nonatomic, retain) id myProperty;
#property (nonatomic, readonly, retain) NSMutableDcitionary* propertyDictionary;
#end
#implementation UIViewController(MyProperty)
-(NSMutableDictionary*) propertyDictionary
{
static NSMutableDictionary* theDictionary = nil;
if (theDictionary == nil)
{
theDictioanry = [[NSMutableDictionary alloc] init];
}
return theDictionary;
}
-(id) myProperty
{
NSValue* key = [NSValue valueWithPointer: self];
return [[self propertyDictionary] objectForKey: key];
}
-(void) setMyProperty: (id) newValue
{
NSValue* key = [NSValue valueWithPointer: self];
[[self propertyDictionary] setObject: newValue forKey: key];
}
#end
Two potential problems with the above approach:
there's no way to remove keys of view controllers that have been deallocated. As long as you are only tracking a handful, that shouldn't be a problem. Or you could add a method to delete a key from the dictionary once you know you are done with it.
I'm not 100% certain that the isEqual: method of NSValue compares content (i.e. the wrapped pointer) to determine equality or if it just compares self to see if the comparison object is the exact same NSValue. If the latter, you'll have to use NSNumber instead of NSValue for the keys (NSNumber numberWithUnsignedLong: will do the trick on both 32 bit and 64 bit platforms).
This is best achieved using the built-in ObjC feature Associated Objects (aka Associated References), in the example below just change to your category and replace associatedObject with your variable name.
NSObject+AssociatedObject.h
#interface NSObject (AssociatedObject)
#property (nonatomic, strong) id associatedObject;
#end
NSObject+AssociatedObject.m
#import <objc/runtime.h>
#implementation NSObject (AssociatedObject)
#dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, #selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
return objc_getAssociatedObject(self, #selector(associatedObject));
}
See here for the full tutorial:
http://nshipster.com/associated-objects/
It mentioned in many document's online that you can't create create new variable in category but I found a very simple way to achieve that. Here is the way that let declare new variable in category.
In Your .h file
#interface UIButton (Default)
#property(nonatomic) UIColor *borderColor;
#end
In your .m file
#import <objc/runtime.h>
static char borderColorKey;
#implementation UIButton (Default)
- (UIColor *)borderColor
{
return objc_getAssociatedObject(self, &borderColorKey);
}
- (void)setBorderColor:(UIColor *)borderColor
{
objc_setAssociatedObject(self, &borderColorKey,
borderColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
self.layer.borderColor=borderColor.CGColor;
}
#end
That's it now you have the new variable.
Why not simply create a subclass of UIViewController, add the functionality to that, then use that class (or a subclass thereof) instead?
Depending on what you're doing, you may want to use Static Category Methods.
So, I assume you've got this kind of problem:
ScrollView has a couple of textedits in them. User types on text edit, you want to scroll the scroll view so the text edit is visible above the keyboard.
+ (void) staticScrollView: (ScrollView*)sv scrollsTo:(id)someView
{
// scroll view to someviews's position or some such.
}
returning from this wouldn't necessarily require the view to move back, and so it doesn't need to store anything.
But that's all I can thinkof without code examples, sorry.
I believe it is possible to add variables to a class using the Obj-C runtime.
I found this discussion also.

How to add an object to a programmatically bound NSMutableArray?

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]