Update Selected Core Data Object from Table View - objective-c

I have hit a bit of a brick wall and am looking for some help with a Cocoa OSX app i am trying to put together.
I have a single entity in core data, which is being populated from a Dictionary pulled from the net. The core data objects are then displayed in a TableView using bindings and an array controller.
Now, i want the ability to detect the selected object in the table, then when a button is pressed in the GUI for it to update a specific attribute of the selected entity.
This is where i have hit a brick wall, lots of info on how to pull/update objects when pulled with a predicate, and lots on how to bind directly to the array controller to add/remove/delete. But nothing on how to update a hidden property with a value that's stored in code.
Any help/pointers greatly appreciated, especially if it's OSX rather than iOS orientated!
Thanks
Gareth

Actually i managed to work this out.
First i implemented a function that gets the current selected object from the array controller and returns it.
-(Tweet*)getCurrentSelectedTweet {
if ([[self.twitterClientsController selectedObjects] count] > 0) {
return [[self.twitterClientsController selectedObjects] objectAtIndex: 0];
} else {
return nil;
}
}
Then i use this function bound to an IBAction to call it and modify the object:
- (IBAction)approveTweet:(id)sender {
Tweet *selectedTweet = [self getCurrentSelectedTweet];
if (selectedTweet) {
selectedTweet.approved = [NSNumber numberWithBool:TRUE];
NSLog(#"%#", selectedTweet);
}
}

Related

Adding managedObjectContext objects with an NSArrayController

I have this application that is using core data and an NSArrayController to manage some objects in a table. I have the code below to pick up some objects on a directory. My questions is about the section below labeled "Handle Files". I create a new Video object using the url, I copy the metadata attributes using a custom function I wrote. The object is now inserted in the managedObjectContext. My question is, since I have my NSArrayController bound to my managedObjectContext, why do I have to still do [self addObject:newVideo] to have the object shown on my table? Is there a way to force the array controller to pull the object from the managedObjectContext without having to manually add it? It will be a hassle having to be updating both things every time I add or remove an object.
for (NSURL *url in _dirEnumerator) {
NSNumber *_isDirectory = nil;
[url getResourceValue:&_isDirectory forKey:NSURLIsDirectoryKey error:NULL];
if (![_isDirectory boolValue]) {
if (([_mediaTypes containsObject:[[url pathExtension]uppercaseString]])) {
// Handle the files
Video *newVideo = [NSEntityDescription insertNewObjectForEntityForName:#"Video" inManagedObjectContext:_managedObjectContext];
[newVideo copyAttributesFrom:url];
[self addObject:newVideo];
NSLog(#"Inserting video: %#",[newVideo valueForKey:#"name"]);
}
}
}
Well, I had my bindings all wrong an the array controller was not feeding my table correctly. You cannot sneak objects behind the array controller, if you implement the array controller you must let him do his job and that includes adding and removing objects. He will take care of letting the tableview know when things have changed.

collectionView:cellForItemAtIndexPath: doesn't get called

I want to add new cells in my collection view, but nothing shows up when I add data.
I have a custom UICollectionViewLayout class, which has been working just fine, and I've been keeping dummy data in my datasource to adjust the layout. Now that I got rid of the dummy data, nothing's showing up.
Since the app didn't break and there weren't any warnings, it was difficult to track down where the problem was, and here's where I found a clue:
(UICollectionViewLayout class)
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSLog(#"ElementsInRect: – Visible cells info: %#", [self.collectionView.visibleCells description]);
...
}
Here, -visibleCells returns an empty array, even when I add data, call -reloadData and invalidate the layout. So I placed a breakpoint in -collectionView:cellForItemAtIndexPath:, and it turns out this method is not called at all. How did the cells show up before?
Any help would be appreciated.
The data source method, collectionView:numberOfItemsInSection:, has to return a non-zero number for collectionView:cellForItemAtIndexPath: to be called. When you had dummy data in your data source, it was. Now that you removed that dummy data, that method is probably returning 0. When you add data, it should put items into your data source, and then a call to reloadData should work. You should put a log in collectionView:numberOfItemsInSection:, and see what it's returning.
Okay, it turns out the issue was in UICollectionViewLayout. I doubt anyone else will be having this problem, but I'll write my answer for the sake of completeness:
I'd been tweaking my custom UICollectionViewLayout class, and after I'd thought that it was working well, I made the code look neat by deleting old code that was commented out, move methods, etc.
While doing that, I recalled having read somewhere that it's good practice to create attributes in -prepareLayout method, and return those attributes when -layoutAttributesForItemAtIndexPath: or -layoutAttributesForElementsInRect: is called. For me, it was a matter of moving a block of code, so I thought no biggie. And during this "cleaning process" I must have made a mistake.
What's really frustrating is that the code itself actually works regardless of where the attributes are created, and I can't tell what went wrong for the last few days.
The following is a snippet of code that I used to create the attributes objects. My initial question was asking why -collectionView:cellForItemAtIndexPath: was not called while executing the 3rd line. I did not change this part of the code, other than moving it around.
for (int i = 0; i < 3; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:self.topLayer];
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
if (cell) {
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[self.array addObject:attributes];
} else {
NSLog(#"prepLayout: the cell doesn't exist for the index path {%d – %d}", indexPath.section, indexPath.item);
}
}
Number of Rows in Section - the count that can be used will determine if the cellForItemAtIndexPath gets called.
Initially when the view loads this will be called. Within the numberOfItemsInSection, if you have an array, the [array count] might return a nil value.
Complete the procedure where the array is populated, then reload the data in the collection view which will re-assess the numberOfItemsInSection. This can be done with the following code:
[self.myCollectionView reloadData];
"myCollectionView is the name given to the collection view item in your view"

With NSTreeController, do I have to manually reload an NSOutlineView when changing the model array?

I have a tree-like model I'd like to show in an NSOutlineView using an NSTreeController.
I was able to set up the bindings and everything works fine as long as I use the NSTreeController's insert and remove functions to change my model tree. If I try to insert or remove from the model tree directly, in some cases the NSOutlineView isn't updating.
If I insert an object into an expanded group of objects, it works:
But if I try to add the first object to a node, that had no children before, nothing happens. The disclosure triangle isn't appearing, so I can't expand it to see the new node.
If I hover over that node with a new object, it is expanded and I can add the second child with no problems. But the triangle is still invisible:
Finally if I close the parent of all these nodes and open them again (triggering a reload) the triangle suddenly appears:
That's why I was wondering if I had to manually reload the NSOutlineView's rows to make the triangle visible, or if I'm messing up something? Thanks!!
UPDATE:
In my Node class I add a new child like this:
- (void)addChild:(MyNode *)child {
[self willChangeValueForKey:#"childNodes"];
[children addObject:child];
[self didChangeValueForKey:#"childNodes"];
}
And I implemented these too (which I set in IB for my NSTreeController):
- (NSArray *)childNodes {
return [NSArray arrayWithArray:children];
}
- (NSInteger)countOfChildNodes {
return [children count];
}
- (BOOL)nodeIsLeaf {
return [children count] < 1;
}
I know that this (especially childNodes) aren't very optimized, but I'm only experimenting at the moment as in the final version my children will be stored in a C array.
UPDATE 2:
I also tried sending KVO notifications for the other 2 properties too, but that didn't help either.
- (void)addChild:(MyNode *)child {
NSLog(#"%#", NSStringFromSelector(_cmd));
[self willChangeValueForKey:#"nodeIsLeaf"];
[self willChangeValueForKey:#"countOfChildNodes"];
[self willChangeValueForKey:#"childNodes"];
[children addObject:child];
[self didChangeValueForKey:#"childNodes"];
[self didChangeValueForKey:#"countOfChildNodes"];
[self didChangeValueForKey:#"nodeIsLeaf"];
}
You have to make sure that all updates to your model are performed in a Key-Value Observing-compliant manner.
Cocoa Bindings Programming Topics: Troubleshooting Cocoa Bindings

How do I persist data managed by NSArrayController without Core Data or NSKeyedArchiver?

I hope you'll excuse the seemingly broad nature of this question, but it gets quite specific.
I'm building a document-based Cocoa application that works like most others except that I am using SQLCipher for my data store (a variant of SQLite), because you don't get to set your own persistent data store in Core Data, and also I really need to use this one.
In my document sub-class, I've got an NSMutableArray property named categories. In the document nib I've got an NSArrayController bound to categories, and I've got an NSCollectionView bound to the array controller.
Each of my model objects in the array (each is a Category) is bound to a record in the underlying data store, so when some property of a Category changes, I want to call [category save], when a Category is added to the set, I want to call, again, [category save], and finally, when a category is removed, [category destroy].
I've wired up a partial solution, but it falls apart on the removal requirement, and everything about it seems to me as though I'm barking up the wrong tree. Anyway, here's what's going on:
Once the document and nib are all loaded up, I start observing the categories property, and assign it some data:
[self addObserver:self
forKeyPath:#"categories"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:MyCategoriesContext];
self.categories = [Category getCategories];
I've implemented the observation method in such a way as that I am informed of changes so that the document can respond and update the data store.
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
NSNumber *changeKind = (NSNumber *)[change objectForKey:#"NSKeyValueChangeKind"];
if (context == MyCategoriesContext)
{
switch ([changeKind intValue])
{
case NSKeyValueChangeInsertion:
{
Category *c = (Category *)[change objectForKey:NSKeyValueChangeNewKey];
NSLog(#"saving new category: %#", c);
[c save];
break;
}
case NSKeyValueChangeRemoval:
{
Category *c = (Category *)[change objectForKey:NSKeyValueChangeOldKey];
NSLog(#"deleting removed category: %#", c);
[c destroy];
break;
}
case NSKeyValueChangeReplacement:
{
// not a scenario we're interested in right now...
NSLog(#"category replaced with: %#", (Category *)[change objectForKey:NSKeyValueChangeNewKey]);
break;
}
default: // gets hit when categories is set directly to a new array
{
NSLog(#"categories changed, observing each");
NSMutableArray *categories = (NSMutableArray *)[object valueForKey:keyPath];
NSIndexSet *allIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [categories count])];
[self observeCategoriesAtIndexes:allIndexes];
break;
}
}
}
else if (context == MyCategoryContext)
{
NSLog(#"saving category for change to %#", keyPath);
[(Category *)object save];
}
else
{
// pass it on to NSObject/super since we're not interested
NSLog(#"ignoring change to %#:#%#", object, keyPath);
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
As you can see from that listing (and as you might already be aware), it's not enough to observe the categories property, I need to observe each individual category so that the document is notified when it's attributes have been changed (like the name) so that I can save that change immediately:
- (void)observeCategoriesAtIndexes:(NSIndexSet *)indexes {
[categories addObserver:self
toObjectsAtIndexes:indexes
forKeyPath:#"dirty"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:MyCategoryContext];
}
This looks to me like a big kludge, and I suspect I'm working against Cocoa here, but for the most part it works.
Except for removal. When you add a button to your interface, and assign it to the array controller's remove: action, it will properly remove the category from the categories property on my document.
In doing so, the category is deallocated while it is still under observation:
2010-09-03 13:51:14.289 MyApp[7207:a0f] An instance 0x52db80 of class Category was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debugger. Here's the current observation info:
<NSKeyValueObservationInfo 0x52e100> (
<NSKeyValueObservance 0x2f1a480: Observer: 0x2f0fa00, Key path: dirty, Options: <New: YES, Old: YES, Prior: NO> Context: 0x1a67b4, Property: 0x2f1a3d0>
...
)
In addition, because the object has been deallocated before I've been notified, I don't have the opportunity to call [category destroy] from my observer.
How is one supposed to properly integrate with NSArrayController to persist changes to the data model pre-Core Data? How would one work-around the remove problem here (or is this the wrong approach entirely?)
Thanks in advance for any advice!
It would seem, based on some initial hacking, that subclassing NSArrayController is the way to go here. Over-riding the various insertObject(s) and removeObject(s) methods in that API gives me the perfect place to add in this logic for messing with the data model.
And from there I can also begin to observe the individual items in the content array for changes, etc, stop observation before destroying/deallocating them, etc, and let the parent class handle the rest.
Thanks for this solution is due to Bill Garrison who suggested it on the cocoa-unbound list.
I would observe changes to categories list, and when the list changes, store the array of categories away in a secondary NSArray, 'known categories', using mutableCopy. Next time the list changes, compare that 'known' list to the new list; you can tell which categories are missing, which are new, etc. For each removed category, stop observing it and release it.
Then take a new mutable copy for the 'known' list of categories, ready for the next call.
Since you have an additional array holding the categories, they aren't released before you're ready.

Using Array Controllers to restrict the view in one popup depending on the selection in another. Not core data based

I am working on an app that is not core data based - the data feed is a series of web services.
Two arrays are created from the data feed. The first holds season data, each array object being an NSDictionary. Two of the NSDictionary entries hold the data to be displayed in the popup ('seasonName') and an id ('seasonID') that acts as a pointer (in an external table) by matches defined for that season.
The second array is also a collection of NSDictionaries. Two of the entries hold the data to be displayed in the popup ('matchDescription') and the id ('matchSeasonId') that points to the seasonId defined in the NSDictionaries in first array.
I have two NSPopUps. I want the first to display the season names and the second to display the matches defined for that season, depending on the selection in the first.
I'm new at bindings, so excuse me if I've missed something obvious.
I've tried using ArrayControllers as follows:
SeasonsArrayController:
content bound to appDelegate seasonsPopUpArrayData.
seasonsPopup:
content bound to SeasonsArrayController.arrangedObjects; content value bound to SeasonsArrayController.arrangedObjects.seasonName
I see the season names fine.
I can obviously follow a similar route to see the matches, but I then see them all, instead of restricting the list to the matches for the season highlighted.
All the tutorials I can find seem to revolve around core data and utilise the relationships defined therein. I don't have that luxury here.
Any help very gratefully received.
This is not an answer - more an extension of the previous problem.
I created MatchesArrayController and subclassed it from NSArrayController to allow some customisation.
Following the example in 'Filtering Using a Custom Array Controller' from 'Cocoa Bindings Topics', I followed the same idea as above:
MatchessArrayController: content bound to appDelegate matchesPopUpArrayData.
matchesPopup: content bound to MatchesArrayController.arrangedObjects; content value bound to MatchesArrayController.arrangedObjects.matchDescription.
I've derived the selected item from seasonPopUp:sender and used this to identify the seasonId.
The idea is to change the arrangedObjects in MatchesArrayController by defining the following in;
- (NSArray *)arrangeObjects:(NSArray *)objects
{
if (searchString == nil) {
return [super arrangeObjects:objects];
}
NSMutableArray *filteredObjects = [NSMutableArray arrayWithCapacity:[objects count]];
NSEnumerator *objectsEnumerator = [objects objectEnumerator];
id item;
while (item = [objectsEnumerator nextObject]) {
if ([[[item valueForKeyPath:#"matchSeasonId"] stringValue] rangeOfString:searchString options:NSAnchoredSearch].location != NSNotFound) {
[filteredObjects addObject:item];
}
}
return [super arrangeObjects:filteredObjects];
}
- (void)searchWithString:(NSString *)theSearchString {
[self setSearchString:theSearchString];
[self rearrangeObjects];
}
- (void)setSearchString:(NSString *)aString
{
[aString retain];
[searchString release];
searchString=aString;
}
I've used NSLog to check that things are happening the way they are supposed to and all seems ok.
However, it still doesn't do what I want.
[self rearrangeObjects]; is supposed to invoke the arrangeObjects method but doesn't. I have to call it explicity
(i.e.[matchesArrayController arrangeObjects:matchesPopUpArrayData]; )
Even then, although filteredObjects gets changed the way it is supposed to, the drop down list does not get updated the way I want it to.