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

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.

Related

Populate and bind an NSTableView to multiple array controllers

I have an API provided NSArray with a bunch of content objects – we'll call this acquiredFruit – and an empty NSMutableArray called likedFruit.
I've created NSArrayControllers for both arrays and bound my TableView to acquiredFruit.arrangedObjects. The first column of the tableView is bound to arrangedObjects.name and correctly shows all the delicious fruit.
I've created a second column with a checkbox – when the user fills the box I'd like to add the fruit to my likedFruit array. Unchecking the box should remove the fruit object from the likedFruit array.
Essentially I'd like my NSTableView to join between two array controllers. I have a feeling I should be making a single separate controller for this, but I'm unsure how to approach the problem.
I should also mention that I'm aware I could iterate through my array and construct another object with the fields I need, but my goal is to do this by using bindings, if possible.
Thoughts?
I think you should use one array controller.
You can have an attribute on Fruit called liked. Now your "liked" checkbox column is connected to arrangedObjects.liked. Later, when you want to determine the set of all liked fruits, you can query your fruits array:
NSArray * likedFruits = [ allFruitsArray filteredArrayUsingPredicate:[ NSPredicate predicateWithFormat:#"liked = YES"] ] ;
If in another part of your UI you are displaying only liked fruit, you can set your array controller's filterPredicate to the predicate above to get just those fruits.
EDIT: Let's say NSFruit is provided via someone else's API. Let's use the "General Technique for Adding Properties to Someone Else's Class":
#interface NSFruit (Liking)
#property ( nonatomic ) BOOL liked ;
#end
#implementation NSFruit (Liking)
-(BOOL)liked
{
return [ objc_getAssociatedObject( self, "_abliked" ) boolValue ] ;
}
-(void)setLiked:(BOOL)b
{
objc_setAssociatedObject( self, "_abliked", [ NSNumber numberWithBool:b ], OBJC_ASSOCIATION_RETAIN_NONATOMIC ) ;
}
#end
(I've written this same code for like 100 posts recently!)
I'm not at my Xcode computer right now, so i can't test this, but it seems like you don't really need another array controller, but just another array to hold the likedFruits. I think you need to create an array of dictionaries from your acquiredFruits array that would have one key for the fruit name and another key with a bool value for whether the check box is checked --this bool would be bound to your second column. I'm not sure about the next step on how to tell the likedFruit array that it need to add a new fruit -- I think the check box could have an action method that you could use to have the likedFruit array add the object in the row where the check box was clicked.
After Edit:
Here is an example of how to do what I suggested. I take an array of fruits and turn it into an array of dictionaries (called theData) that include a key for the value of your check box (In IB the content array of the array controller is bound to theData, and the columns are bound to Array Controller.arrangedObjects.fruitName and Array Controller.arrangedObjects.isLiked). checkChanged is an IBAction connected to the check box (but note the sender is actually the table view), and I use the value of the check box to determine whether to add a fruit to likedFruits or delete one. I put one more method, connected to a button just to check the values in likedFruits.
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = [NSMutableArray array];
self.likedFruit =[NSMutableArray array];
NSArray *acquiredFruits = #[#"Apple",#"Orange",#"Pear",#"Peach"];
for (NSString *aFruit in acquiredFruits) {
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:aFruit,#"fruitName",[NSNumber numberWithBool:NO],#"isLiked", nil];
[self.theData addObject:[dict mutableCopy]];
}
self.theData = _theData;
// NSLog(#"%#",self.theData);
}
-(IBAction)checkChanged:(NSTableView *)sender { //connected to the button cell in the table view (but sender is the table view)
NSString *theFruit = [[self.controller.arrangedObjects objectAtIndex:sender.clickedRow ] valueForKey:#"fruitName"];
BOOL doWeLikeIt = [[[self.controller.arrangedObjects objectAtIndex:sender.clickedRow] valueForKey:#"isLiked"] boolValue];
if (doWeLikeIt) {
[self.likedFruit addObject:theFruit];
}else{
[self.likedFruit removeObject:theFruit];
}
}
-(IBAction)logLikedFruits:(id)sender {
NSLog(#"%#",self.likedFruit);
}

How to Bind Content Set of NSArrayController to More than One NSArrayController Selection?

I am new to Objective-C, and I love it so far. However, I seem to be running in circles. I am trying to do as much as possible without writing code. Is it possible to effectively bind the Content Set of one NSArrayController to the selections of two other NSArrayControllers.
For example, I want all of the Transactions (NSArrayController) for the selected User (NSArrayController) with selected Seller (NSArrayController). Then when I add new transaction it links to the selected user and seller.
What is the best way to do that so that when I click a new User in an NSTableView bound to the User Controller, the Transactions in an NSTableView bound to a Transactions controller change accordingly but still retain Transactions related to the Seller selected in an NSTableView bound to a Seller controller (and vice versa)?
I may just need to change my perspective since I am used to living in a non-binding world.
Appreciate any help.
You might regularly configure in IB a "TransactionsForUserAndSeller" NSArrayController with its contentSet bound to userArrayController.selection.transactions then filter the results using its filterPredicate bound to sellerArrayController.selection with a value transformer that returns an NSPredicate.
That value transformer’ implementation might look like this:
+(Class)transformedValueClass { return [NSPredicate class]; }
+(BOOL)allowsReverseTransformation { return NO; }
-(id)transformedValue:(id)value {
if (value == nil) return nil;
return [NSPredicate predicateWithFormat:
[NSString stringWithFormat:#"seller == %#", value]];
}
This would show the subset correctly but you need to write your own add method to handle the relationships manually, getting the current selection through an outlet to the seller array controller.

UISearchBar Search table row with text, subtext and image

I've noticed that in order to do a search of a table, a copy of that data must be inserted to a search array.
E.g.
//Initialize the array.
listOfItems = [[NSMutableArray alloc] init];
NSArray *countriesToLiveInArray = [NSArray arrayWithObjects:#"Iceland", #"Greenland", #"Switzerland", #"Norway", #"New Zealand", #"Greece", #"Rome", #"Ireland", nil];
NSDictionary *countriesToLiveInDict = [NSDictionary dictionaryWithObject:countriesToLiveInArray forKey:#"Countries"];
NSArray *countriesLivedInArray = [NSArray arrayWithObjects:#"India", #"U.S.A", nil];
NSDictionary *countriesLivedInDict = [NSDictionary dictionaryWithObject:countriesLivedInArray forKey:#"Countries"];
[listOfItems addObject:countriesToLiveInDict];
[listOfItems addObject:countriesLivedInDict];
//Initialize the copy array.
copyListOfItems = [[NSMutableArray alloc] init];
So what is searched is the objects that are stored in the copied array.
My Question is, how do I search Cell rows with text, subtext and image in that particular cell.
(1)
There isn't really any such thing as searching a table. What happens when the user enters text in a UISearchBar is totally up to you - you can make that operation mean anything you like. All you have to do is function as the delegate-and-data-source for the results table and form the results table in response to the standard Three Big Questions that form the basis for any table ("how many sections have you? how many rows in this section? what's the cell for this row?") in any way you like. The results table does often look like a reduced version of the original table, but this is not at all required! It can be any table you want it to be.
(2)
Don't confuse Model with View. The table is just a view. Your data is Model. It is the Model, your data that is the basis of the original table, that you are going to be searching. So when the user types in your UISearchBar and you start searching, you want to form a new Model that will be the basis of the results table. How you form it is completely up to you. Typically you'll want to filter the original model so that the only stuff left in your results model is stuff that counts as a valid result. You could do this by walking the whole original model, putting everything that matches the search criterial into the new model. Or, if the original model is an array, you could use one of the filteredArray methods to help you. The most flexible way is to form a predicate with a block, as in this example from my book:
NSPredicate* p = [NSPredicate predicateWithBlock:
^BOOL(id obj, NSDictionary *d) {
NSString* s = obj;
NSStringCompareOptions options = NSCaseInsensitiveSearch;
return ([s rangeOfString:sbc.searchBar.text
options:options].location != NSNotFound);
}];
self.filteredStates = [states filteredArrayUsingPredicate:p];
In that example, s (one item of the array) is a string each time, and I'm looking to see whether the user's search term occurs in that string. But if you had a dictionary or other structure holding both a title and a subtitle and info about an image, you could examine that dictionary in any way you like. It's just a matter of returning YES or NO according to whether this array item passes the test based on the search term, on whatever definition you attach to the notion of passing the test.
(3)
The big question remaining is when to form the results model. I usually start by making the results model identical to the original model in response to searchDisplayControllerWillBeginSearch, because otherwise the results table will say No Results while the user is typing. (That is probably why you think the first thing to do is copy the original model.) Then, I can either do the actual filtering in response to searchBarSearchButtonClicked (the user is done typing and has tapped Search), or if the model is small enough, I can filter it afresh after every letter the user types, in response to searchBar:textDidChange (the user has typed a letter in the search bar).
There are a few steps involved. Note that the code below is just an example that I'm typing in by hand now, so it probably won't compile, it's just to give you an idea.
1) Ensure that you have an array containing all the cell values.
2) Create a copy of that array, and use that copy as the data source when returning cells in your table delegate methods.
3) Set yourself up as delegate for the UISearchBar, and respond to its events:
- (void)searchBarButtonClicked(UISearchBar *)searchBar {
[self doSearch:searchBar.text];
}
- (void)searchBar(UISearchBar *)searchBar textDidChange:(NSString *)searchTerm {
if (searchTerm.length == 0) {
[self resetSearch];
[table reloadData];
}
else
[self doSearch:searchTerm];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
searchBar.text = #"";
[self resetSearch];
[table reloadData];
[searchBar resignFirstResponder];
}
4) Create the other methods
The resetSearch method just needs to copy your full data array to the data source array used by your table delegates:
- (void)resetSearch {
self.tableSourceArray = [self.dataSourceArray copy]; // Or write a deep copy if you want to.
}
Whereas when searching, we need to filter the datasource array. You may be able to create something more efficient - this is just an example.
- (void)doSearch:(NSString *)searchTerm {
NSMutableArray *filtered = [[NSMutableArray alloc] init];
for (NSString *item in self.self.dataSourceArray) {
if ([item rangeOfString:searchTerm options:NSCaseInsensitiveSearch].location != NSNotFound])
[filtered addObject:[item copy]];
}
self.tableSourceArray = filtered;
}
And that should be it!
Tim

Pointers and data assignment in Objective-C

I'm having trouble dealing with pointers in Objective-C. Basically, I have the following structure in my class :
UITableView *list;
NSArray *objArray;
UIPickerView *pickerCtrl;
My "list" shows the data contained in objArray, which is a temporary structure linking to custom NSObjects of various types (not stored in my current object).
Choosing one element in the list shows the "pickerCtrl", displaying appropriate data depending on which TableView line is currently selected.
My goal is to replace oldObject's data (the external object, accessed by objArray) with newObject's data (selected in the PickerView). Like this :
- (void)pickerView:(UIPickerView *)thePickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
id oldObject = [objArray objectAtIndex:[[list indexPathForSelectedRow] row]];
id newObject = [pickerData objectAtIndex:row];
*oldObject = *newObject;
}
From the debugger, oldObject and newObject both have the right memory addresses. The problem is, no assignation seems to be done, and the old data is never replaced by the data from "newObject".
What am I missing here ?
This is not the proper way to deal with mutable arrays, you are thinking too low-level.
Rather, try this:
[objArray removeObject:oldObject];
[objArray addObject:newObjec];
You can also use the insertObject:atIndex: method. See the reference for NSMutableArray for further information
Use:
- (void)exchangeObjectAtIndex:(NSUInteger)idx1 withObjectAtIndex:(NSUInteger)idx2
Example:
[objArray exchangeObjectAtIndex:[[list indexPathForSelectedRow] row],row];

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.