View-based NSTableView filtering + animation - objective-c

I have a view based NSTableView that I sometimes filter using NSPredicate. Is there any way to animate the items being removed/added/reordered throughout the tableview to have the same effect as beginUpdates, endUpdates and insertRowsAtIndexes:withAnimation, etc?
I've explored ways such as manually filtering out my array but my attempts proved to be futile so now I am wondering if there is a better (or built in way) to do this. I have wondered if NSArrayController does this automatically but I don't think it does.

I've written code to do this myself - given 'before' and 'after' arrays, compute the required parameters to insertRowsAtIndexPaths:, deleteRowsAtIndexPaths:, etc. The code is a bit fiddly so probably has bugs - use at your discretion!
#interface NSArray (ArrayDifference)
- (void) computeDifferenceTo:(NSArray *)newArray returningAdded:(NSMutableArray **)rowsAdded andDeleted:(NSMutableArray **)rowsDeleted;
#end
#implementation NSArray (ArrayDifference)
// Given two arrays that are expected have items added or removed but not re-ordered, compute the differences
// in a way usable for UITable insertRows and deleteRows
- (void) computeDifferenceTo:(NSArray *)newArray returningAdded:(NSMutableArray **)rowsAdded andDeleted:(NSMutableArray **)rowsDeleted
{
NSArray *oldArray = self;
*rowsAdded = [[[NSMutableArray alloc] init] autorelease];
*rowsDeleted = [[[NSMutableArray alloc] init] autorelease];
NSUInteger oldCount = [oldArray count];
NSUInteger newCount = [newArray count];
// Step through the two arrays
NSInteger oldIndex = 0, newIndex=0;
for (; newIndex < newCount && oldIndex < oldCount; )
{
id newItem = [newArray objectAtIndex:newIndex];
id oldItem = [oldArray objectAtIndex:oldIndex];
// If the two objects match, we step forward on both sides
if (newItem == oldItem) {
++newIndex;
++oldIndex;
}
else {
// Look for the old item to appear later in the new array, which would mean we have to add the rows in between
NSRange range = { newIndex+1, newCount - newIndex-1 };
NSUInteger foundIndex = [newArray indexOfObject:oldItem inRange:range];
if (foundIndex != NSNotFound)
for (; newIndex < foundIndex; ++newIndex)
[*rowsAdded addObject:[NSIndexPath indexPathForRow:newIndex inSection:0]];
else {
// Look for the new item to appear later in the old array, which would mean we have to remove the rows in between
NSRange range = { oldIndex+1, oldCount - oldIndex-1 };
NSUInteger foundIndex = [oldArray indexOfObject:newItem inRange:range];
if (foundIndex != NSNotFound)
for (; oldIndex < foundIndex; ++oldIndex)
[*rowsDeleted addObject:[NSIndexPath indexPathForRow:oldIndex inSection:0]];
else {
// Old item must be removed and new item added, then we carry on
[*rowsAdded addObject:[NSIndexPath indexPathForRow:newIndex++ inSection:0]];
[*rowsDeleted addObject:[NSIndexPath indexPathForRow:oldIndex++ inSection:0]];
}
}
}
}
// Once the loop is finished, add in what's left in the new array and remove what is left in the old array
for (; newIndex < newCount; ++newIndex)
[*rowsAdded addObject:[NSIndexPath indexPathForRow:newIndex inSection:0]];
for (; oldIndex < oldCount; ++oldIndex)
[*rowsDeleted addObject:[NSIndexPath indexPathForRow:oldIndex inSection:0]];
}
#end
Then you call it like this:
NSMutableArray *rowsAdded=nil, *rowsDeleted=nil;
[myArray computeDifferenceTo:newArray returningAdded:&rowsAdded andDeleted:&rowsDeleted];
[myTableView beginUpdates];
[myTableView insertRowsAtIndexPaths:rowsAdded withRowAnimation:UITableViewRowAnimationBottom];
[myTableView deleteRowsAtIndexPaths:rowsDeleted withRowAnimation:UITableViewRowAnimationFade];
[myTableView endUpdates];

Related

Implementing copy method - copy whole row

I am trying to copy the row of the NSTableView on clipboard. Here is my code:
- (void) copy:(id)sender
{
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
// I get warning in the line bellow, unused variable changeCount
NSInteger changeCount = [pasteboard clearContents];
NSInteger row = [self.customersViewController.customersTableView selectedRow];
NSTableColumn *columnFirstName = [self.customersViewController.customersTableView tableColumnWithIdentifier:#"firstName"];
NSCell *cellFirstName = [columnFirstName dataCellForRow:row];
NSArray *objectsToCopy = #[[cellFirstName stringValue]];
// I get warning in the line bellow unused variable OK
BOOL OK = [pasteboard writeObjects:objectsToCopy];
}
This code works, and if I select the row in the NSTableView, the content of the firstName column of the selected row is indeed on the pasteboard (I can paste the value in text editor).
However this code have couple of issues:
1. I get 2 warnings as you can see from my comments.I rewrite the code to get rid of the warnings like this. Is anything wrong with the way how I re-write the code?
// warning one
NSInteger changeCount = 0;
changeCount = [pasteboard clearContents];
// warning 2
BOOL OK = NO;
OK = [pasteboard writeObjects:objectsToCopy];
In the code above I name specific which NSTableView I use
...self.customersViewController.customersTableViev....
However If the user switch the view, it may use some other NSTableView...how can I find out from which NSTableView the copy method should copy the row?
If I comment the line where I use specific NSTableView and try to use sender, my app crashes.
//NSInteger row = [self.customersViewController.customersTableView selectedRow];
NSInteger row = [sender selectedRow];
3.How could I write a loop to get all column names instead of specifically write them by hand one by one? I will not know which NSTableView is used anyway....
NSTableColumn *columnFirstName = [self.customersViewController.customersTableView tableColumnWithIdentifier:#"firstName"];
If you don't want the return value you can omit it.
To make you code table view independent you can use firstResponder of the window. Alternatively you can implement copy: in a cubclass of NSTableView. sender is the menu item.
NSTableView's property tableColumns is an array of NSTableColumn.
Here's what I did:
- (void)copy:(id)sender {
NSResponder *firstResponder = self.window.firstResponder;
if (firstResponder && [firstResponder isKindOfClass:[NSTableView class]]) {
NSTableView *tableView = (NSTableView *)firstResponder;
NSArrayController *arrayController = [[tableView infoForBinding:NSContentBinding] objectForKey:NSObservedObjectKey];
// create an array of the keys and formatters of the columns
NSMutableArray *keys = [NSMutableArray array];
for (NSTableColumn *column in [tableView tableColumns]) {
NSString *key = [[column infoForBinding:NSValueBinding] objectForKey:NSObservedKeyPathKey]; // "arrangedObjects.name"
if (key) {
NSRange range = [key rangeOfString:#"."];
if (range.location != NSNotFound)
key = [key substringFromIndex:range.location + 1];
NSFormatter *formatter = [[column dataCell] formatter];
if (formatter)
[keys addObject:#{#"key":key, #"formatter":formatter}];
else
[keys addObject:#{#"key":key}];
}
}
// create a tab separated string
NSMutableString *string = [NSMutableString string];
for (id object in [arrayController selectedObjects]) {
for (NSDictionary *dictionary in keys) {
id value = [object valueForKeyPath:dictionary[#"key"]];
if (value) {
NSFormatter *formatter = [dictionary objectForKey:#"formatter"];
if (formatter)
[string appendFormat:#"%#\t", [formatter stringForObjectValue:value]];
else
[string appendFormat:#"%#\t", value];
}
else
[string appendFormat:#"\t"];
}
[string replaceCharactersInRange:NSMakeRange([string length] - 1, 1) withString:#"\n"];
}
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
[pasteboard setString:string forType:NSPasteboardTypeString];
}
}

How can I remove all NSTableColumns from an NSTableView?

I am trying to implement a method to clear the NSTableView of all items AND columns. But I get a crash when I try to implement the following:
- (void)clearResultData
{
[resultArray removeAllObjects];
NSArray *tableCols = [resultTableView tableColumns];
if ([tableCols count] > 0)
{
id object;
NSEnumerator *e = [tableCols objectEnumerator];
while (object = [e nextObject])
{
NSTableColumn *col = (NSTableColumn*)object;
[resultTableView removeTableColumn:col];
}
}
[resultTableView reloadData];
}
Well, if it's any help you can remove all the columns like this:
- (void)removeAllColumns
{
while([[tableView tableColumns] count] > 0) {
[tableView removeTableColumn:[[tableView tableColumns] lastObject]];
}
}
The NSArray returned by tableColumns is changed by removeTableColumn. Do not assume it is unchanged.
Although it is returned as a non-mutable NSArray, the underlying implementation is being modified and it is not safe to use NSEnumerator with collections that are modified. In the while loop, you are sending a nextObject message to an enumerator whose current object was just deleted -- so bad things can happen!
Here's a more efficient implementation:
NSTableColumn* col;
while ((col = [[tableView tableColumns] lastObject])) {
[tableView removeTableColumn:col];
}
When there are no columns in the table view: tableColumns returns an empty array, lastObject on an empty array returns nil, col is assigned the value of nil, the condition is false and the while loop finishes.
[[[_tableView tableColumns] copy] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[_tableView removeTableColumn:obj];
}];
Here is a Swift implementation:
tableView.tableColumns.forEach({tableView.removeTableColumn($0)})

cancel button on search bar does not cancel correctly

I have a search bar, i can search now, but when I enter a text to search, and click the cancel button. It does not give me back my first stage, meaning full of the items in the table.
For example: I search the item with word: a, it gives me all the a items, yes, it is right now, but when i hit the cancel button, i want the programme gives me all the items exist, not just a items.
Here is the code: please help me out. Thank you so much.
- (void)searchBarCancelButtonClicked:(UISearchBar *)aSearchBar
{
searchBar.text = #"";
[searchBar resignFirstResponder];
letUserSelectRow = YES;
searching = NO;
self.tableView.scrollEnabled = YES;
NSLog(#"what text after cancel now: %#", searchBar.text);
[self.tableView reloadData];
}
- (NSMutableArray *) searchTableView {
NSString *searchText = searchBar.text;
NSLog(#"search text: %#", searchText);
NSMutableArray *resultArray = [[NSMutableArray alloc] init];
NSMutableArray *tempArr = [[NSMutableArray alloc] init];
for (NSDictionary *dTemp in arrayData)
{
NSString *tempStr = [dTemp objectForKey:#"url"];
NSLog(#"sTemp string: %#",[ NSString stringWithFormat:#"%#", tempStr]);
NSRange titleResultsRange = [tempStr rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (titleResultsRange.length > 0)
{
NSLog(#"1 count :%d", [resultArray count]);
[resultArray addObject:dTemp];
NSLog(#"2 count :%d", [resultArray count]);
[tempArr addObject:resultArray];
[resultArray release];
resultArray = [NSMutableArray new];
}
}
if (resultArray != nil) {
[resultArray release];
}
return tempArr;
}
- (void)searchBar:(UISearchBar *)aSearchBar textDidChange:(NSString *)searchText
{
NSLog(#"what text after cancel now: %#", searchBar.text);
if([searchText length] > 0) {
[sortedArray removeAllObjects];
searching = YES;
letUserSelectRow = YES;
self.tableView.scrollEnabled = YES;
NSMutableArray *searchArray = [self searchTableView];
sortedArray = [[NSMutableArray alloc] initWithArray:searchArray copyItems:YES];
for (int i = 0; i<[sortedArray count]; i++) {
NSLog(#"this is the search array: %#", [[sortedArray objectAtIndex:i] class]);
}
NSLog(#"sorted array: %d", [sortedArray count]);
}
else {
searching = NO;
letUserSelectRow = NO;
self.tableView.scrollEnabled = NO;
}
[self.tableView reloadData];
}
You don't need to override any of UISearchBar methods to accomplish this. The new way of doing this relies on the UISearchDisplay controller instead (specifically on shouldReloadTableForSearchString).
Declare your view controller to conform to UISearchDisplayDelegate protocol, and keep two instance variables: your model as NSArray (all data) and a filtered array as NSMutableArray (a subset of your data). The code you presently have in "searchTableView" would filter the content of the model and place it into the filtered NSMutableArray. Then you would override the following UITableView methods: -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section and -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath. In each, before returning, make a comparison to determine whether your tableView argument is equal to self.searchDisplayController.searchResultsTableView. If it is, the user is looking at the filtered list and your should use the content of the filtered NSMutableArray to create the view, otherwise, the user is looking at the whole data set and you should use the content of the NSArray that holds your model. Take a look at the following Apple code for a simple example of what I described:
http://developer.apple.com/library/ios/#samplecode/TableSearch/Introduction/Intro.html

How to move an item on NSMutableArray?

I want to move a string item to the top of the list.
NSMutableArray animal = "cat", "lion", "dog", "tiger";
How do I move dog to top of the list?
You would remove the item and insert it at the correct space:
id tmp=[[animals objectAtIndex:2] retain];
[animals removeObjectAtIndex:2];
[animals insertObject:tmp atIndex:0];
[tmp release];
You have to retain the object or when you tell the array to remove it, it will release the object.
If you don't know the index you could do something like this:
NSMutableArray* animals = [NSMutableArray arrayWithObjects:#"cat", #"lion", #"dog", #"tiger",nil];
for (NSString* obj in [[animals copy] autorelease]) {
if ([obj isEqualToString:#"dog"]) {
NSString* tmp = [obj retain];
[animals removeObject:tmp];
[animals insertObject:tmp atIndex:0];
break;
}
}
This method will go over all your list and search for "dog" and if it finds it will remove it from the original list and move it to index 0.
I would like to point out an inefficiency in your method. By removing/inserting an object from a NSMutableArray you potentially affect every row after the deletion/insertion. I say ‘potentially’ because it’s not clear what internal method Apple uses to maintain their mutable arrays. However, assuming it’s a simple c-array, then every row after that deletion/insertion index will be need to be moved down/up. In a very large array, this could be inefficient if the items moved are at the beginning. However, replacing items in an array are not inefficient at all. Thus the following is a category on NSMutableArray (note this code is under ARC, so no memory management):
- (void) moveObjectAtIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex{
if (fromIndex == toIndex) return;
if (fromIndex >= self.count) return; //there is no object to move, return
if (toIndex >= self.count) toIndex = self.count - 1; //toIndex too large, assume a move to end
id movingObject = [self objectAtIndex:fromIndex];
if (fromIndex < toIndex){
for (int i = fromIndex; i <= toIndex; i++){
[self replaceObjectAtIndex:i withObject:(i == toIndex) ? movingObject : [self objectAtIndex:i + 1]];
}
} else {
id cObject;
id prevObject;
for (int i = toIndex; i <= fromIndex; i++){
cObject = [self objectAtIndex:i];
[self replaceObjectAtIndex:i withObject:(i == toIndex) ? movingObject : prevObject];
prevObject = cObject;
}
}
}
Also, a small bonus to further increase functionality, if you're performing operations on the items moved (like updating a db or something), the following code has been very useful to me:
- (void) moveObjectAtIndex:(NSUInteger)fromIndex toIndex:(NSUInteger)toIndex withBlock:(void (^)(id, NSUInteger))block{
if (fromIndex == toIndex) return;
if (fromIndex >= self.count) return; //there is no object to move, return
if (toIndex >= self.count) toIndex = self.count - 1; //toIndex too large, assume a move to end
id movingObject = [self objectAtIndex:fromIndex];
id replacementObject;
if (fromIndex < toIndex){
for (int i = fromIndex; i <= toIndex; i++){
replacementObject = (i == toIndex) ? movingObject : [self objectAtIndex:i + 1];
[self replaceObjectAtIndex:i withObject:replacementObject];
if (block) block(replacementObject, i);
}
} else {
id cObject;
id prevObject;
for (int i = toIndex; i <= fromIndex; i++){
cObject = [self objectAtIndex:i];
replacementObject = (i == toIndex) ? movingObject : prevObject;
[self replaceObjectAtIndex:i withObject:replacementObject];
prevObject = cObject;
if (block) block(replacementObject, i);
}
}
}
You can remove an existing element, e.g dog and then reinsert it at the beginning of the array.
NSMutableArray *animals = [NSMutableArray arrayWithObjects:#"cat", #"lion", #"dog", #"tiger",nil];
NSString *dog = #"dog";
// Check to see if dog is in animals
if ( [animals containsObject:dog] ) {
// Remove dog from animals and reinsert
// at the beginning of animals
[animals removeObject:dog];
[animals insertObject:dog atIndex:0];
}

Difference of 2 NSArray's for animated insert/delete in UITableView

At some point in my Application, I have an NSArray whose contents change. Those contents are shown in a UITableView. I'm trying to find a way to find the diff between the contents of before and after of the NSArray so i can pass the correct indexPaths to insertRowsAtIndexPaths:withRowAnimation: and deleteRowsAtIndexPaths:withRowAnimation: in order to have the changes nicely animated. Any ideas?
thx
Here's is wat i tried and it seems to work, if anyone has anything better, i'd love to see it.
[self.tableView beginUpdates];
NSMutableArray* rowsToDelete = [NSMutableArray array];
NSMutableArray* rowsToInsert = [NSMutableArray array];
for ( NSInteger i = 0; i < oldEntries.count; i++ )
{
FDEntry* entry = [oldEntries objectAtIndex:i];
if ( ! [displayEntries containsObject:entry] )
[rowsToDelete addObject: [NSIndexPath indexPathForRow:i inSection:0]];
}
for ( NSInteger i = 0; i < displayEntries.count; i++ )
{
FDEntry* entry = [displayEntries objectAtIndex:i];
if ( ! [oldEntries containsObject:entry] )
[rowsToInsert addObject: [NSIndexPath indexPathForRow:i inSection:0]];
}
[self.tableView deleteRowsAtIndexPaths:rowsToDelete withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:rowsToInsert withRowAnimation:UITableViewRowAnimationRight];
[self.tableView endUpdates];
This question from 2010 is what I found when I was googling. Since iOS 5.0, we now also have -[UITableView moveRowAtIndexPath:toIndexPath] which you really want to handle as well. Here is a function that compares two arrays and generates suitable indexpaths for the delete, insert and move operations.
- (void) calculateTableViewChangesBetweenOldArray:(NSArray *)oldObjects
newArray:(NSArray *)newObjects
sectionIndex:(NSInteger)section
indexPathsToDelete:(NSArray **)indexPathsToDelete
indexPathsToInsert:(NSArray **)indexPathsToInsert
indexPathsToMove:(NSArray **)indexPathsToMove
destinationIndexPaths:(NSArray **)destinationIndexPaths
{
NSMutableArray *pathsToDelete = [NSMutableArray new];
NSMutableArray *pathsToInsert = [NSMutableArray new];
NSMutableArray *pathsToMove = [NSMutableArray new];
NSMutableArray *destinationPaths = [NSMutableArray new];
// Deletes and moves
for (NSInteger oldIndex = 0; oldIndex < oldObjects.count; oldIndex++) {
NSObject *object = oldObjects[oldIndex];
NSInteger newIndex = [newObjects indexOfObject:object];
if (newIndex == NSNotFound) {
[pathsToDelete addObject:[NSIndexPath indexPathForRow:oldIndex inSection:section]];
} else if (newIndex != oldIndex) {
[pathsToMove addObject:[NSIndexPath indexPathForRow:oldIndex inSection:section]];
[destinationPaths addObject:[NSIndexPath indexPathForRow:newIndex inSection:section]];
}
}
// Inserts
for (NSInteger newIndex = 0; newIndex < newObjects.count; newIndex++) {
NSObject *object = newObjects[newIndex];
if (![oldObjects containsObject:object]) {
[pathsToInsert addObject:[NSIndexPath indexPathForRow:newIndex inSection:section]];
}
}
if (indexPathsToDelete) *indexPathsToDelete = [pathsToDelete copy];
if (indexPathsToInsert) *indexPathsToInsert = [pathsToInsert copy];
if (indexPathsToMove) *indexPathsToMove = [pathsToMove copy];
if (destinationIndexPaths) *destinationIndexPaths = [destinationPaths copy];
}
An example on how to use it. Assume you're displaying a table of people, which you keep in the array self.people. The section index where the people are displayed is 0.
- (void) setPeople:(NSArray <Person *> *)newPeople {
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView beginUpdates];
NSArray *rowsToDelete, *rowsToInsert, *rowsToMove, *destinationRows;
[self calculateTableViewChangesBetweenOldArray:self.people
newArray:newPeople
sectionIndex:0
indexPathsToDelete:&rowsToDelete
indexPathsToInsert:&rowsToInsert
indexPathsToMove:&rowsToMove
destinationIndexPaths:&destinationRows
];
self.people = newPeople;
[self.tableView deleteRowsAtIndexPaths:rowsToDelete withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:rowsToInsert withRowAnimation:UITableViewRowAnimationFade];
[rowsToMove enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull oldIndexPath, NSUInteger idx, BOOL * _Nonnull stop) {
NSIndexPath *newIndexPath = destinationRows[idx];
[self.tableView moveRowAtIndexPath:oldIndexPath toIndexPath:newIndexPath];
}];
[self.tableView endUpdates];
});
}