Returning NO on canEditRowAtIndexPath, but can edit if scrolled - objective-c

So I've been looking for an answer to this interesting issue I came across but haven't had very much luck. Basically I have a UITableView preloaded on initial app launch with a few objects using CoreData, and the ability for the user to add more.
I allow deleting of cells in the table, except for the items I have initial pre-loaded. So I perform a check in my canEditRowAtIndexPath method and return NO if the selected item is one of these pre-loaded items. Everything works great until I scroll down far enough for one of the items to be offscreen, and then when it snaps back the item that shouldn't be editable, is now editable.
I'm fairly new to iOS development, so I'm hoping this is a rather amateur issue - but I can't seem to find the answer.
Any help is appreciated.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
DataLayer *dl = [DataLayer alloc];
// Get all items from Favorites
NSArray *results = [dl FetchAll:#"Favorites"];
// Get currently selected cell properties
FavoritesTableCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
Favorites *fav = [Favorites alloc];
NSMutableArray *genItems = [[NSMutableArray alloc] init];
// Get only records that are default app items
for(int a = 0; a < [results count]; a++){
fav = [results objectAtIndex:a];
if ([fav.generic isEqualToNumber:[NSNumber numberWithInt:1]]) {
[genItems addObject:fav];
}
}
// Loop through default items to determine if editable
for (int i = 0; i < [genItems count]; i++) {
fav = [genItems objectAtIndex:i];
if ([fav.title isEqualToString:[selectedCell.nameLabel text]]) {
return NO;
}
}
return YES;
}

The root of the problem is that this method is basing it's answer on the content of a table view cell (selectedCell) rather than the model.
Table views reuse cells. As they are scrolled off the view, the "new" cells that appear are really the same object's that just disappeared on the other side of the table. So that selectedCell is not a good reference for a question that ought to be put to your model.
The code needs to be structured like this:
Your model is a NSMutableArray that starts with a few items you add. You need to know which items are originals, not to be removed:
#property (nonatomic, strong) NSMutableArray *favorites;
#property (nonatomic, assign) NSMutableArray *genericFavorites;
// at init, imagine these strings are your preloaded core data
[self.genericFavorites addObject:#"generic favorite object a"];
[self.genericFavorites addObject:#"generic favorite object b"];
[self.favorites addItemsFromArray:self.genericFavorites];
You'll use self.favorites as your model, that is when table view asks numberOfRowsInSection, you'll answer self.favorites.count. In cellForRowAtIndexPath, you'll lookup the item in self.favorites[indexPath.row] and configure the cell with data from that object. self.genericFavorites just helps you remember which objects are original, not added by the user.
If the order remains fixed, then your canEditRow logic is simple:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
return indexPath.row >= self.genericFavorites.count;
}
But if, as maybe your code implies, the user can reorder these items, then your canEditRow has more work to do, but it can do that work without reference to the table cells (which as I indicated, are unreliable):
// get the object at indexPath.row from our model. Answer NO if that object was preloaded
// from the app (if genericFavorites containsObject:), YES otherwise
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
Favorite *favorite = self.favorites[indexPath.row]; // this is your model after user has edited, reordered, etc
return ![self.genericFavorites containsObject:favorite];
}

Related

Cocoa Bindings - NSTableView - Swapping Values

Is an NSValueTransform subclass a good choice for displaying Core Data attributes into UI views displaying:
A number string like (0,1,2,3,etc) into a string such as (Pending, Completed, Frozen, In progress, etc)
A number string like (0,1) into a app-based image (red.png if 0, green.png if 1)
Here's what Core Data displays for the two attributes, timer and status:
Here is what I want to be displayed instead, without changing the values in Core Data:
If not to use NSValueTransformer, in what other way is this possible?
I do not want to see the data permanently converted, only for the benefit of less data stored in Core Data and better UI view items.
I have also tried to modify the attributes in the managed object class (with out KVO notification) with no luck.
Yes, NSValueTransformer subclasses work just fine for this purpose.
You can also add read-only computed properties to your managed object class, and that should work, too. Those properties can even be added by a category in the controller code, if they don't make sense as part of the model code.
For example:
+ (NSSet*) keyPathsForValuesAffectingStatusDisplayName
{
return [NSSet setWithObject:#"status"];
}
- (NSString*) statusDisplayName
{
NSString* status = self.status;
if ([status isEqualToString:#"0"])
return #"Pending";
else if ([status isEqualToString:#"1"])
return #"Completed";
// ...
}
The +keyPathsForValuesAffectingStatusDisplayName method lets KVO and bindings know that when status changes, so does this new statusDisplayName property. See the docs for +keyPathsForValuesAffectingValueForKey: to learn how that works.
I ended up using what at first appeared to be blocking the display of different info in those cells, using:
#pragma mark - Table View Delegate
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
/* tableColumn = (string) #"AutomaticTableColumnIdentifier.0"
row = (int) 0 */
NSString *identifier = [tableColumn identifier];
NSTableCellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self];
NSManagedObject *item = [self.itemArrayController.arrangedObjects objectAtIndex:row];
if ([identifier isEqualToString:#"AutomaticTableColumnIdentifier.0"]) {
/* subviews returns array with 0 = Image View &
1 = Text Field */
/* First, set the correct timer image */
... logic ...
NSImageView *theImage = (NSImageView *)[[cellView subviews] objectAtIndex:0];
theImage.image = [NSImage imageNamed:#"green.gif"];
/* Second, display the desired status */
NSTextField *theTextField = (NSTextField *)[[result subviews] objectAtIndex:1];
... logic ...
theTextField.stringValue = #"Pending";
}
return cellView;
}
Apple's documentation states (somewhere) that bindings with an Array Controller can work in combination with manually populating the table view cells. It seems best and easiest to start with bindings and then refine display values manually.

Objective-C Update Array of objects

I have the following entry that updates a labels text with the value:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *row = [tableView dequeueReusableCellWithIdentifier:#"TICKET_ITEM"];
UILabel *itemDiscount = (UILabel *)[row viewWithTag:502];
itemDiscount.text = [[arrayOfItem objectAtIndex:indexPath.row]TICKET_ITEM_DISCOUNT];
return row;
}
My problem is that after the fact I have button that allows for setting the discount (which is initially 0). After adjusting a slider I want to be able to take that discount % and update itemDiscount.text with the new value. I figure the way I need to do this is to update the arrayOfItem TICKET_ITEM_DISCOUNT entry and then use reloadData. But how do I update just the single item in the array?
Check out Apple's documentation on NSArray.
There are a variety of methods that could solve your problem.
indexOfObject:
or
indexOfObjectPassingTest:
spring to mind.
To edit an NSArray you'll need to make a mutable copy of the array then assign it back again:
NSMutableArray *temp = [arrayOfItem mutableCopy];
//update your value here
arrayOfItem = temp;

Populate NSTableView with unknown number of columns

I have a NSTableview and a button. NSTableview has a unknown number of columns.
The first column has a image well and 2 text boxes, the others (again, unknown number) are normal textbox columns.
The button opens up a file open dialogue. Once I choose the files (.jpg) I would like to process them.
So far everything is made (chose files, columns, etc..) what is missing is the populating of the table:
I have the loop that goes through all the selected files. What is the best way to do this:
display the image in the image well of the first cell,
type the filename in the first textbox of the first cell,
type the filepath in the second cell of the textbox,
type "YES" in all other columns.
My difficulty is that I have no idea how many columns will be there since it depends from the user. The number of columns will not change during Runtime. they are set up at startup based on the configuration. if the configuration is changed then the app should be reloaded.
I am a beginner in Objective-C/Cocoa programming.
EDIT:
additional info as requested:
It is a view based NSTableView
each column represents an action that has to be taken in a later moment on an image. the program user can decide what and how many actions to take, thats the reason for a unknown amount of columns in the table view.
You can add columns programmatically using addTableColumn:. This method takes an NSTableColumn instance that you can create in your code.
The rest of your architecture (displaying images, etc.) does not particularly change from "normal" code just because the columns have been added dynamically.
Here is a snippet that should get you started:
NSTableColumn* tc = [[NSTableColumn alloc] init];
NSString *columnIdentifier = #"NewCol"; // Make a distinct one for each column
NSString *columnHeader = #"New Column"; // Or whatever you want to show the user
[[tc headerCell ] setStringValue: columnHeader];
tc.identifier = columnIdentifier;
// You may need this one, too, to get it to show.
self.dataTableview.headerView.needsDisplay = YES;
When populating the table, and assuming that the model is an array (in self.model) of NSDictionary objects, it could go something like this;
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSString *columnIdentifier = tableColumn.identifier;
NSDictionary *rowDict = [self.model objectAtIndex: row];
NSString *value = [rowDict valueForKey: columnIdentifier]; // Presuming the value is stored as a string
// Show the value in the view
}
More in the docs.
When user adds a column or row, you should reflect it in your model (by binding or by code), so you know the size of your table, when you need to populating it.
set tableView.delegate (in code or in Interface Builder), reference here
implement:
- (NSView*) tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row
{
Item* itemView = [tableView makeViewWithIdentifier:#"rowItem" owner:self];
/*Here you populate your cell view*/
id entryObject = [self.entries objectAtIndex:row];
[itemView setEntry:entryObject];
return itemView;
}
and then invoke [tableView reloadData];
maybe for you better to use this method
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
Just see the NSTableViewDataSource and NSTableViewDelegate

Adding data to a tableView using a NSMutableArray

I'm having a problem adding an item to my tableView.
I used to initialize an empty tableView at the start of my App and get it filled with scanned items every time the tableView reappears and there is an item in my variable.
Initialization of the tableView:
NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:nil];
self.listArray = array;
TableView Data Source:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return [self.listArray count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if(section == 0)
return #"Eingescannte Artikel:";
else
return nil;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"testCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
NSUInteger row = [indexPath row];
cell.textLabel.text = [listArray objectAtIndex:row];//[NSString stringWithFormat:#"Das ist Zeile %i", indexPath.row];
return cell;
}
(Not the whole thing but the ones I changed)
As you may have seen I use an NSMutableArray to add items to my tableView.
So if an item ist scanned I'm adding it to my array like this:
[listArray insertObject:sharedGS.strEAN atIndex:0]; //using a shared Instance where I implemented my variable.
I also tried to use an variable to extend my Index every time a new Item is added, but it won't work both ways.
I'm quite new to programming so an not-too-hard-to-understand-answer would be quite nice ;)
If there's any information missing, feel free to ask.
/edit: Trying to specify my question: The data from the variable is written in a TableViewCell, but if I scan another one the other one is just being replaced. Not sure if it's a problem with my array or my tableView...
/edit No.2: Found out(thanks to fzwo) that my array isn't working correctly. It just doesn't grow by an addObject: or insertObject:atIndex: command. But I just don't get why... :(
All I'm doing: [listArray addObject:sharedGS.strEAN]; not that much space for errors in one simple line. Maybe I'm just too stupid to recognize what I'm doing wrong:D
You state that your problem is "adding an item to my tableView" , since you are adding the object to your array i am guessing the problem is that you are not reloading the table or that it is missing the dataSource binding.
You have not actually asked any question (even if you added info to "specify your question") so a wild guess, after
[listArray insertObject:sharedGS.strEAN atIndex:0];
put
[yourTableView reloadData];
Are you intentionally adding new items to the top of the table ? otherwise you could do
[listArray addObject:sharedGS.strEAN]; to add new items to the bottom
Otherwise it's worth noting that you are misusing dequeueReusableCellWithIdentifier, look at the example below for proper usage:
// Try to retrieve from the table view a now-unused cell with the given identifier
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
// If no cell is available, create a new one using the given identifier
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
}

Can't get contents of file from a .plist file into an array

I'm trying to create a create a simple app where I have a TableView that should display the contents of a file. I have created a Table View in IB and dragged it's delegate and data source to the file's owner and I have manually created a .plist file with 1 array that have 2 items.
In my TableViewController.h i have declared an array.
NSArray * posts;
In my implementation file I have declared the required methods for UITableViewDataSource like this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"Returning num sections");
return posts.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// create a cell
UITableViewCell * post = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"post"];
// fill it with content
post.textLabel.text = [posts objectAtIndex:indexPath.row];
// return it
return post;
}
And in my ViewController 'viewDidLoad' method I try to add the content of my file to the 'posts' array like this:
- (void)viewDidLoad
{
NSString * postFile = [[NSBundle mainBundle] pathForResource:#"Posts" ofType:#"plist"];
posts = [[NSArray alloc] initWithContentsOfFile:postFile];
NSLog(#"%#", postFile);
NSLog(#"%i", posts.count);
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
NSLog(#"%i", posts.count); returns 0, despite that I have added values to my .plist file. And nothing is displayed in the table view.
Suggestions on how so solve this would be appreciated.
I think you need to reload your table after you've loaded your postFile NSArray. If your view controller is a UITableViewController, try adding the following line of code to the end of your viewDidLoad method:
[self.tableView reloadData]
(On an unrelated note, you should also make your call to the super class the first thing you do in the viewDidLoad method, hence the comment the xcode template gives you.)
Edit: Problem with count.
I think you also have a problem with your debugging. count isn't a property of NSArray, so you can't use the dot syntax with it. You should be sending a message to your NSArray instance i.e. [posts count].
Ok, it seems like Xcode 4 creates plists with Dictionary as it's root type. If you want to use an array you have to open the .plist file in another text editor (probably doable in Xcode too) and change < dict >< /dict /> to < array >.
Also, it wasn't necessary to use an array at all. This also worked:
// Changed my array to a dictionary.
NSDictionary * posts;
// Get the cell text.
NSString * cellText = [[NSString alloc] initWithFormat:#"%i", indexPath.row];
// fill it with content
post.textLabel.text = [posts valueForKey:cellText];