Pass index back to parent view controller - objective-c

The NSMutableArray detailsDataSource and int detailIndex is passed on to next View Controller from
MainDetailViewController.m:
#import "UsersDetailViewController.h"
...
- (void)swipeDetectedUp:(UISwipeGestureRecognizer *)sender
{
UsersDetailViewController *usersController = [[self storyboard] instantiateViewControllerWithIdentifier:#"UsersController"];
[self.navigationController pushViewController:usersController animated:NO];
usersController.usersDataSource = [[NSMutableArray alloc] initWithArray:detailsDataSource];
usersController.userDetailIndex = detailIndex;
}
Swipe through the index in UserDetailViewController.m:
- (void)swipeDetectedRight:(UISwipeGestureRecognizer *)sender
{
if (userDetailIndex != 0)
userDetailIndex--;
}
When swipeDetectedDown to pop back, MainDataViewController needs to know which object at index to display:
- (void)swipeDetectedDown:(UISwipeGestureRecognizer *)sender
{
//jump to correct object at index, same as current object at index in this view
[self.navigationController popViewControllerAnimated:NO];
}
Code suggestions?

Use NSNotificationCenter to send an object back to the MainDataViewController...
Example:
In UsersDetailViewController populate an NSDictionary with a key=>value pair then send it over to where you want it to go.
NSArray *key = [NSArray arrayWithObject:#"myIndex"];
NSArray *object = [NSArray arrayWithObject:detailIndex];
NSDictionary *dictionary = [NSDictionary dictionaryWithObjects:object forKeys:key];
[[NSNotificationCenter defaultCenter] postNotificationName:#"MainDataViewController" object:self userInfo:dictionary];
Note: You need to setup an identifier on MainDataViewController called MainDataViewController or whatever you want to call it. Using the VC name keeps it simpler.
Then on MainDataViewController do this in the viewDidLoad() method.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(receiveNotification:) name:#"MainDataViewController" object:nil];
And then receive the notification by using the following method:
- (void)receiveNotification:(NSNotification *) notification
{
if([[notification name] isEqualToString:#"MainDataViewController"])
{
NSDictionary *dictionary = [NSDictionary dictionaryWithDictionary:[notification userInfo]];
if([dictionary valueForKey:#"myIndex"])
{
// do whatever you need to do with the passed object here. In your case grab the detailIndex and use it for something...
}
}
}

The easy part is to put the UsersDetailViewController pointer into a property of MainDetailViewController so it can access self.usersController.usersDataSource & self.usersController.userDetailIndex later. Then the only trick is to have it know when the UsersDetailViewController was popped.
In code I used to write, I often tried something like making MainDetailViewController be a delegate of UsersDetailViewController, and having a delegate method in MainDetailViewController be called when the UsersDetailViewController want to close programmatically, and in that do both the popViewControllerAnimated: and update the MainDetailViewController's state. In other words, always have the parent's code pop the child off. This works, but not in the case where you have the child view controller pop automatically via the navigation controller's back button say, so overall I'd argue against that technique.
I think there's better solutions for having the parent's code get called when its child is popped. Perhaps implement a viewWillAppear method and if self.usersController is set there, then you know you're coming back from the UsersDetailViewController, at that point access the other controller's properties and finally clear self.usersController.

Related

How to name Undo menu entries for Core Data add/remove items via bindings and NSArrayController?

I have a NSTableView populated by a Core Data entity and Add Item / Remove Item buttons all wired with a NSArrayController and bindings in Interface Builder.
The Undo/Redo menu items can undo or redo the add / remove item actions.
But the menu entries are called only „Undo“ resp. „Redo“.
How can i name them like „Undo Add Item“, „Undo Remove Item“, etc.
(I am aware, something similar was asked before, but the accepted answers are either a single, now rotten link or the advice to subclass NSManagedObject and override a method that Apples documentation says about: "Important: You must not override this method.“)
Add a subclass of NSArrayController as a file in your project. In the xib, in the Identity Inspector of the array controller, change the Class from NSArrayController to your new subclass.
Override the - newObject method.
- (id)newObject
{
id newObj = [super newObject];
NSUndoManager *undoManager = [[[NSApp delegate] window] undoManager];
[undoManager setActionName:#"Add Item"];
return newObj;
}
Also the - remove:sender method.
- (void)remove:(id)sender
{
[super remove:sender];
NSUndoManager *undoManager = [[[NSApp delegate] window] undoManager];
[undoManager setActionName:#"Remove Item"];
}
Register for NSManagedObjectContextObjectsDidChangeNotification:
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(mocDidChangeNotification:)
name:NSManagedObjectContextObjectsDidChangeNotification
object: nil];
And parse the userInfo dictionary in the corresponding method:
- (void)mocDidChangeNotification:(NSNotification *)notification
{
NSManagedObjectContext* savedContext = [notification object];
// Ignore change notifications for anything but the mainQueue MOC
if (savedContext != self.managedObjectContext) {
return;
}
// Ignore updates -- lots of noise from maintaining user-irrelevant data
// Set actionName for insertion
for (NSManagedObject* insertedObject in
[notification.userInfo valueForKeyPath:NSInsertedObjectsKey])
{
NSString* objectClass = NSStringFromClass([insertedObject class]);
savedContext.undoManager.actionName = savedContext.undoManager.isUndoing ?
[NSString stringWithFormat:#"Delete %#", objectClass] :
[NSString stringWithFormat:#"Insert %#", objectClass];
}
// Set actionName for deletion
for (NSManagedObject* deletedObject in
[notification.userInfo valueForKeyPath:NSDeletedObjectsKey])
{
NSString* objectClass = NSStringFromClass([deletedObject class]);
savedContext.undoManager.actionName = savedContext.undoManager.isUndoing ?
[NSString stringWithFormat:#"Insert %#", objectClass] :
[NSString stringWithFormat:#"Delete %#", objectClass];
}
}
I've tested this in my own code-- it's rough. Can spend a lot more time making the actionName nicer. I deleted parsing of updates because: 1) insertions and deletions of objects in to-many relationships generate updates of other objects 2) I don't care to figure out how to discover what properties changed at this time
I also have class names that aren't user-friendly, so this is a great time to implement the description function for all entities, and use that rather than the class name.
But this at least works for all object controllers in a project, and easily enough for insert and delete.
[edit] Updated with mikeD's suggestion to cover redo having an inverse name. Thanks!

Display Model's Data in NSMenuItem

I'd like to display some data of my model within a status bar menu. So, I bound my entity-object to the title of an NSMenuItem:
[self.statusMenu setAutoenablesItems:NO];
NSMenuItem * exportMenuItem = [[NSMenuItem alloc] init];
[exportMenuItem bind:#"title" toObject:expo withKeyPath:#"menuItemTitle" options:nil];
[exportMenuItem setEnabled:NO];
[self.statusMenu insertItem:exportMenuItem atIndex:3];
It works fine so far from init. But when I update my Model it does not updates the title of the NSMenuItem.
For reference, the canonical solution to this issue is to implement a class method like this:
+ (NSSet *)keyPathsForValuesAffectingMenuItemTitle
{
return [NSSet setWithObjects: #"propertyMenuItemTitleDependsOn1",
#"propertyMenuItemTitleDependsOn2",
nil];
}
If you implement a method like this, then the framework will handle calling willChangeValueForKey: and didChangeValueForKey: for the key menuItemTitle any time any of the other properties are changed.
Okay I got it:
menuItemTitle is a dynamic getter method which combines two actual properties of expo. So the reason for NSMenuItem's title to not get updated is probably, that menuItemTitle probably never gets actually set.
So how do I tell, that menuItemTitle was changed, when one of my properties was set?
Overriding expo's setters to add [self willChangeValueForKey:#"menuItemTitle"]; and [self didChangeValueForKey:#"menuItemTitle"]; does not work as it causes an endless loop in calling the setter itself again and again.
So here is my solution: I overrode [NSManagedObject setValue:(id)value forKey:(NSString *)key]:
- (void)setValue:(id)value forKey:(NSString *)key {
[self willChangeValueForKey:#"menuItemTitle"];
[super setValue:value forKey:key];
[self didChangeValueForKey:#"menuItemTitle"];
}

NSNotificationCenter is not working?

as i told here i am using NSNotificationCenter .
on class A (observer) on the init method i have got :
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(getSensorsData:) name:#"HotSpotTouched" object:nil];
on classB i have got :
//FILL NSDICTIONARY WITH DATA
[dict setObject:#"SPOT1" forKey:[array objectAtIndex:0]];
[dict setObject:#"SPOT2" forKey:[array objectAtIndex:1]];
[dict setObject:#"SPOT3" forKey:[array objectAtIndex:2]];
[dict setObject:#"SPOT4" forKey:[array objectAtIndex:3]];
[dict setObject:#"SPOT5" forKey:[array objectAtIndex:4]];
[[NSNotificationCenter defaultCenter] postNotificationName:#"HotSpotTouched" object:dict];
the function in class A getSensorsData is not being called.
whats wrong here ??
thanks !
You are passing in dict as notificationSender when posting the notification, and nil when you add the observer. This way your notification is filtered out because the senders mismatch.
Update:
As pointed out by joerick in the comments, passing nil when adding an observer will disable sender filtering. So this isn't the problem here.
I just created a small sample project and for me notifications are delivered.
#Rant: If you want to pass arbitrary data along with your notification, you should use the userInfo dictionary (as pointed out by Cyrille in the comment).
The calls to the notification center look correct. I suspect the problem is due to the liffe cycle of object A. You say that you're registering for the notification in the init method. Have you correctly assigned self?:
-(id)init
{
//self does not have a meaningful value prior to the call to [super init]
self = [super init];
if (self != nil)
{
//ensure addObserver is called in the if code block
}
return self;
}
Also, it's good practice to use constants for notification names as they mitigate against typos. See Constants in Objective C.
problem solved:
if you passing a null argument, the observer is not getting the call !
my NSDictionary argument was null( because a reason i still dont know), so the call is not fired.

How do I tell a (managed) object to notify its KVOs that one of its properties needs to be recached?

When we have an object who has a property that's generated based on other properties, usually we implement the +keyPathsForValuesAffecting{PropertyName} class method.
What I'm trying to do is basically the same thing for a property on my NSManagedObject, but traversing a relationship.
My model is simple; I have two Entities, App and Version (I'm creating an appcast-generating app). When the App's properties are changed, because I implemented the method above, the -appcast string is changed, and all bindings are updated appropriately.
However, when any properties on any of a specific App's Versions (to-many relationship) change, the -appcast property is not generated appropriately. I can haz fix/workaround?
This is a bit of a late answer, but I think it's a common situation, and the answer definitely isn't readily-apparent.
I generally watch for the managedObjectContext to change and then check if the any of the changed objects are ones that I want to look out for. So, in your NSManagedObject subclass:
// We need to register for the notification in both awakeFromFetch
// AND awakeFromInsert, since either one could be called, depending on
// if the object was previously-created or not
- (void)awakeFromFetch {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object:[self managedObjectContext]];
}
- (void)awakeFromInsert {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object:[self managedObjectContext]];
}
- (void)managedObjectContextDidChange:(NSNotification *)notification {
// Get a set containing ALL objects which have been changed
NSSet *insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
NSSet *updatedObjects = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
NSSet *deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];
NSSet *changedObjects = [insertedObjects setByAddingObjectsFromSet:updatedObjects];
changedObjects = [changedObjects setByAddingObjectsFromSet:deletedObjects];
if ([changedObjects intersectsSet:[self versions]]) {
[self willChangeValueForKey:#"appCast"];
[self didChangeValueForKey:#"appCast"];
}
}
This is certainly not ideal from a performance perspective, since this notification is going to fire every time anything in your object graph changes, but I've found it to be the most straightforward way to accomplish this.

How to get notified of changes to models via an NSArrayController?

I have an NSView subclass which is bound to the arrangedObjects of an NSArrayController. When the array has an item inserted or removed the view is notified. How do I get it to be notified if a model stored in the array has an attribute changed?
Do I need to add my view as an observer to every (relevant) attribute of every item added to the array?
When an item is added to or removed from the array I am notified via observeValueForKeyPath:ofObject:change:context: in my NSView subclass. I am not notified of changes to the models stored in the array but I could, every time I am notified of an insertion, add the view as an observer to the new item's attributes. Is this the best way to do this?
I overrode addObserver for the model class so that I could see what happens and noticed that NSTableView columns bound to the arrangedObjects add themselves as observers to the appropriate attributes. Can this be made to happen automagically or do I set up the observations manually?
A big thank you to dreamlax but I think I didn't do a good enough job explaining my problem. My model class was observable and produced the right notifications but I couldn't work out how to observe them without observing every item in the array directly.
I think the documentation for key paths could be improved because I couldn't find anything that explained the very simple change I needed to make. There's some good info the array magic keypaths but no simple "these are the common things" documentation.
Anyway. Previously in my NSView subclass I had the following:
- (void) bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
if ([binding isEqualToString:#"observedObjects"]) {
[observable addObserver:self forKeyPath:#"arrangedObjects" options:0 context:nil];
} else {
[super bind:binding toObject:observable withKeyPath:keyPath options:options];
}
}
To get notification of changes to the models within the NSArrayController's arrangedObjects all I needed to add was observation of arrangedObjects.name (for the name property of my model). So the above code became:
- (void) bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
if ([binding isEqualToString:#"observedObjects"]) {
[observable addObserver:self forKeyPath:#"arrangedObjects" options:0 context:nil];
[observable addObserver:self forKeyPath:#"arrangedObjects.name" options:0 context:nil];
} else {
[super bind:binding toObject:observable withKeyPath:keyPath options:options];
}
}
That's it! Now if any object in arrangedObjects gets its name changed I am notified.
Maybe rather than observing potentially many key value paths, why not have each object in the array post a notification when something has changed, then only one object needs to observe one notification instead of one object observing many key value paths.
EDIT:
Also, your arrayed objects could also respond to a class method called +keyPathsForValuesAffecting<key> where <key> is your key name. Here's an example: paymentDue is a key, which is affected when the values invoiceItems.count or paymentsMade have changed. When invoiceItems.count or paymentsMade has changed, anything bound to paymentDue is sent a notification.
+ (NSSet *) keyPathsForValuesAffectingPaymentDue:
{
return [NSSet setWithObjects:#"invoiceItems.count", #"paymentMade", nil];
}
If you are running on 10.4, or are targeting 10.4 or earlier, you'll need to use this method instead, but it essentially boils down to the same thing.
EDIT 2:
To clarify your other comment; you can still have each object in the array manually call
[[NSNotificationCenter defaultCenter] postNotificationName:#"ModelDidChange" object:self];
Then, with some controller code you can register for notification updates from your objects. If you choose a unique notification name then you won't need to manually listen from specific objects, you can tell the NSNotificationCenter that you want to receive notifications from any object. Your controller can work out which object has changed quite easily.
Register with the notification center (these methods should be in a controller object):
// This string could really be just about anything you want, but make it conspicuous.
static NSString * const ModelDidChangeName = #"ModelDidChange";
- (void) awakeFromNib
{
// using nil for the object parameter will make the notification center
// invoke modelDidChange: regardless of who the sender is.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(modelDidChange:) name:ModelDidChangeName object:nil];
}
Implement a method to handle the notification.
- (void) modelDidChange:(NSNotification *) notification
{
MyModelClass *myObject = [notification object];
// do stuff with your model if necessary.
// do stuff with your view too
}
Get your model objects to post notifications when parts of them change:
#implementation MyModelClass
- (void) setSomething:(NSString *) newThing
{
[something autorelease];
something = [newThing copy];
if (something == nil)
{
// special case scenario for when something is nil
// do stuff, modify MyModelClass instance's attributes
[[NSNotificationCenter defaultCenter] postNotificationName:ModelDidChange object:self];
// the controller's modelDidChange: method is automatically invoked.
}
}
#end
But
If your model is properly KVC compliant and made with the appropriate KVO callbacks, manual notifications won't be necessary.