UICollectionView: I'm doing it wrong. I just don't know how.
My Setup
I'm running this on an iPhone 4S with iOS 6.0.1.
My Goal
I have a table view in which one section is devoted to images:
When the user taps the "Add Image..." cell, they are prompted to either choose an image from their photo library or take a new one with the camera. That part of the app seems to be working fine.
When the user first adds an image, the "No Images" label will be removed from the second table cell, and a UICollectionView, created programmatically, is added in its place. That part also seems to be working fine.
The collection view is supposed to display the images they have added, and it's here where I'm running into trouble. (I know that I'm going to have to jump through some hoops to get the table view cell to enlarge itself as the number of images grows. I'm not that far yet.)
When I attempt to insert an item into the collection view, it throws an exception. More on that later.
My Code
I've got the UITableViewController in charge of the table view also acting as the collection view's delegate and datasource. Here is the relevant code (I have omitted the bits of the controller that I consider unrelated to this problem, including lines in methods like -viewDidLoad. I've also omitted most of the image acquisition code since I don't think it's relevant):
#define ATImageThumbnailMaxDimension 100
#interface ATAddEditActivityViewController ()
{
UICollectionView* imageDisplayView;
NSMutableArray* imageViews;
}
#property (weak, nonatomic) IBOutlet UITableViewCell *imageDisplayCell;
#property (weak, nonatomic) IBOutlet UILabel *noImagesLabel;
#end
#implementation ATAddEditActivityViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UICollectionViewFlowLayout* flowLayout = [[UICollectionViewFlowLayout alloc] init];
flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
imageDisplayView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 290, 120) collectionViewLayout:flowLayout]; // The frame rect still needs tweaking
imageDisplayView.delegate = self;
imageDisplayView.dataSource = self;
imageDisplayView.backgroundColor = [UIColor yellowColor]; // Just so I can see the actual extent of the view
imageDisplayView.opaque = YES;
[imageDisplayView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:#"Cell"];
imageViews = [NSMutableArray array];
}
#pragma mark - UIImagePickerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
/* ...code defining imageToSave omitted... */
[self addImage:imageToSave toCollectionView:imageDisplayView];
[self dismissModalViewControllerAnimated:YES];
}
#pragma mark - UICollectionViewDelegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
#pragma mark - UICollectionViewDatasource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
[[cell contentView] addSubview:imageViews[indexPath.row]];
return cell;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [imageViews count];
}
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return ((UIImageView*)imageViews[indexPath.item]).bounds.size;
}
#pragma mark - Image Handling
- (void)addImage:(UIImage*)image toCollectionView:(UICollectionView*)cv
{
if ([imageViews count] == 0) {
[self.noImagesLabel removeFromSuperview];
[self.imageDisplayCell.contentView addSubview:cv];
}
UIImageView* imageView = [[UIImageView alloc] initWithImage:image];
/* ...code that sets the bounds of the image view omitted... */
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
[cv reloadData];
}
#end
To summarize:
The collection view is instantiated and configured in the -viewDidLoad method
The UIImagePickerDelegate method that receives the chosen image calls -addImage:toCollectionView
...which creates a new image view to hold the image and adds it to the datasource array and collection view. This is the line that produces the exception.
The UICollectionView datasource methods rely on the imageViews array.
The Exception
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (1) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
If I'm parsing this right, what this is telling me is that the (brand new!) collection view thinks it was created with a single item. So, I added a log to -addImage:toCollectionView to test this theory:
NSLog(#"%d", [cv numberOfItemsInSection:0]);
With that line in there, the exception never gets thrown! The call to -numberOfItemsInSection: must force the collection view to consult its datasource and realize that it has no items. Or something. I'm conjecturing here. But, well, whatever: the collection view still doesn't display any items at this point, so I'm still doing something wrong and I don't know what.
In Conclusion
I get an odd exception when I attempt to add an item to a newly-minted-and-inserted collection view...except when I call -numberOfItemsInSection: before attempting insertion.
Even if I manage to get past the exception with a shady workaround, the items still do not show up in the collection view.
Sorry for the novel of a question. Any ideas?
Unfortunately the accepted answer is incorrect (although it's on the right track); the problem is that you were calling reloadData & insertItems when you should have just been inserting the item. So instead of:
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
[cv reloadData];
Just do:
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
Not only will this give you a nice animation, it prevents you from using the tableview inefficiently (not a big deal in a 1-cell collection view, but a huge problem for larger data sets), and avoids crashes like the one you were seeing, where two methods were both trying to modify the collection view (and one of them -- reloadData -- does not play well with others).
As an aside, reloadData is not very UICollectionView-friendly; if you do have a sizable &/or complex collection, and an insertion happens shortly before or after a call to reloadData, the insertion might finish before the reloadData finishes -- which will reliably cause an "invalid number of items" crash (same goes for deletions). Calling reloadSections instead of just reloadData seems to help avoid that problem.
Faced same issue but the reason with me was that I forgot to connect the collection view Data Source to view controller
It is because the [cell count] don't equal the [real index count] + [insert indexes].
Sometimes the dispatch_async don't include block of the array insert data and insertItemsAtIndexPaths.
I got the problem with somtimes crash. It is not cause each time crash.
Just a guess: at the time you are inserting the first image, the collection view may not yet have loaded its data. However, in the exception message, the collection view claims to "know" the number of items before the insertion (1). Therefore, it could have lazily loaded its data in insertItemsAtIndexPaths: and taken the result as "before" state. Also, you don't need to reload data after an insertion.
Long story short, move the
[cv reloadData];
up to get
if ([imageViews count] == 0) {
[self.noImagesLabel removeFromSuperview];
[self.imageDisplayCell.contentView addSubview:cv];
[cv reloadData];
}
Related
I have a collection view that displays instances of CatViewController views:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
CatCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCatCellId forIndexPath:indexPath];
[cell updateCat:[self.dataSource catAtIndex:indexPath.row]]; //catAtIndex will return a Cat Core Data object
return cell;
}
CatCell looks like this:
- (void)prepareForReuse {
[super prepareForReuse];
[self.catVC.view removeFromSuperview];
self.catVC = nil;
}
- (void)updateCat:(Cat*)cat {
self.catVC = [[CatViewController alloc] initWithNibName:nil bundle:nil];
self.catVC.view.frame = self.bounds;
[self.contentView addSubview:self.catVC.view];
self.catVC.cat = cat;
}
self.catVC.cat is what causes the CatViewController to configure itself with all the view data associated with a Cat object. The problem is that when the UICollectionView scrolls, it pauses briefly as the new CatViewController is created and displayed. Obviously I want the collection view to be completely smooth, and have the view for each cell appear when it's ready, without blocking the main thread.
This is easy and well-documented to do with images, but I'm struggling to do the same with a view controller's view.
Reuse as much of your controller and view infrastructure as possible. There is a reason that table and collection views offer cell reuse - tear down and recreation is expensive.
Collection view memory management is the art of reuse. By not reusing the controller and it's view you are subverting the cell reuse (because you destroy and recreate 90% of the cell content each time.
I have a UIViewController which has the following implementation for the didSelectItemAtIndexPath
#interface
id section1Item
NSMutableArray *section2Items
NSMutableArray *section3Items
#end
#implementation
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0) {
;
} else if (indexPath.section == 1) {
self.section1Item = [self.section2Items objectAtIndex:indexPath.row];
} else { // indexPath.section == 2
id newSection2Item = [self.section3Items objectAtIndex:indexPath.row];
[self.section2Items addObject:newSection2Item];
[self.section3Items removeObject:newSection2Item];
}
[collectionView reloadData];
}
#end
The idea behind the code is that my collectionView has a static number of sections, and taping on an item in section 3 moves the item to section 2, and tapping on an item in section 2 makes it an item in section 1.
However once I make the changes to my dataStructure (section1Item, section2Items and section3Items), and call reloadData, all my UICollectionView cells disappear. A few symptoms of the issue
After the reloadData call, non of my dataSource methods get recalled. I tried putting a breakpoint in my implementation of numberOfSectionsInCollectionView and collectionView:numberOfItemsInSection but they don't get hit.
I tried debugging using RevealApp, and I found out that after reloadData call, all my UICollectionViewCell's have their hidden property set to "YES", even though I don't have any code in my code base calling .hidden = YES;
I also tried overriding UICollectionViewCell#setHidden to detect what (if any) part of the UIKit framework calls it, and again there was no breakpoint triggers.
Tools details: I'm working with XCode5-DP6 on iOS7 simulator.
UPDATE: My UICollectionView shows all the cells correctly on first render.
Ok peeps, so I was able to figure out the issue. The delegate (self) was a subclass of UIViewController. In the init, I was assigning self.view = viewFromStoryBoard where viewFromStoryBoard was passed in by the caller and which was setup in a storyboard.
Since I was not really using any of the facilities offered by subclass UIViewController, I decided to switch to subclassing NSObject and manually retaining the pointer to the UICollectionView.
This fixed my problem. However I'm not a 100% on the exact nature of the issue. I'm guessing somehow overriding a UIViewController's view isn't all that it seems.
There are lots of bugs with iOS 7 and UICollectionView... In my case reloadData doesn't work correctly, it works with delay.
I'm swapping out the data being displayed in my collection view by changing the datasource. This is being done as part of a tab-like interface. When the new data loads, I would like to flash the scroll indicators to tell the user that there's more data outside of the viewport.
Immediately
Doing so immediately doesn't work because the collection view hasn't loaded the data yet:
collectionView.dataSource = dataSource2;
[collectionView flashScrollIndicators]; // dataSource2 isn't loaded yet
dispatch_async
Dispatching the flashScrollIndicators call later doesn't work either:
collectionView.dataSource = dataSource2;
dispatch_async(dispatch_get_main_queue(), ^{
[collectionView flashScrollIndicators]; // dataSource2 still isn't loaded
});
performSelector:withObject:afterDelay:
Executing the flashScrollIndicators after a timed delay does work (I saw it somewhere else on SO), but leads to a bit of lag with the scroll indicators being shown. I could decrease the delay, but it seems like it'll just leads to a race condition:
collectionView.dataSource = dataSource2;
[collectionView performSelector:#selector(flashScrollIndicators) withObject:nil afterDelay:0.5];
Is there a callback that I can hook on to to flash the scroll indicators as soon as the collection view has picked up on the new data and resized the content view?
Subclassing UICollectionView and overriding layoutSubviews can be a solution. You can call [self flashScrollIndicators] on the collection. Problem is that layoutSubviews gets called in multiple scenarios.
Initially when collection is created and datasource is assigned.
On scrolling, cells which go beyond the viewport get re-used & re-layout.
Explicitly change frame/reload the collection.
Workaround to this can be, keeping a BOOL property which will be made YES only when reloading datasource, otherwise will remain NO. Thus flashing of scroll bars will happen explicitly only when reloading collection.
In terms of source code,
MyCollection.h
#import <UIKit/UIKit.h>
#interface MyCollection : UICollectionView
#property (nonatomic,assign) BOOL reloadFlag;
#end
MyCollection.m
#import "MyCollection.h"
#implementation MyCollection
- (void) layoutSubviews {
[super layoutSubviews];
if(_reloadFlag) {
[self flashScrollIndicators];
_reloadFlag=NO;
}
}
Usage should be
self.collection.reloadFlag = YES;
self.collection.dataSource = self;
Put your call to flashScrollIndicators inside UICollectionViewLayout's method -finalizeCollectionViewUpdates.
From Apple's documentation:
"... This method is called within the animation block used to perform all of the insertion, deletion, and move animations so you can create additional animations using this method as needed. Otherwise, you can use it to perform any last minute tasks associated with managing your layout object’s state information."
Hope this helps!
Edit:
Ok, I got it. Since you mentioned the finalizeCollectionViewUpdates method was not being called I decided to try it myself. And you're right. The problem is (sorry I didn't notice this earlier) that method is only called after you update the Collection View (insert, delete, move a cell, for example). So in this case it doesn't work for you. So, I have a new solution; it involves using UICollectionView's method indexPathsForVisibleItems inside UICollectionViewDataSource's method collectionView:cellForItemAtIndexPath:
Every time you hand a new UICollectionViewCell to your collection view, check if it is the last of the visible cells by using [[self.collectionView indexPathsForVisibleItems] lastObject]. You will also need a BOOL ivar to decide if you should flash the indicators. Every time you change your dataSource set the flag to YES.
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
NSIndexPath *iP = [[self.collectionView indexPathsForVisibleItems] lastObject];
if (iP.section == indexPath.section && iP.row == indexPath.row && self.flashScrollIndicators) {
self.flashScrollIndicators = NO;
[self.collectionView flashScrollIndicators];
}
return cell;
}
I tried this approach and it's working for me.
Hope it helps!
I have been struggling with this for a week and my head is about to explode.
Basically I use Prototype Cell, in CellWillAppear I did a little customizations like background color. Nothing fancy.
Due to this, my table view is always empty at start up (no cell) unless the array (data source) is filled with something. So what I did was in NumberOfRowsInSection:
return dataArray.count < 10? 10 : dataArray.count
I am doing this because I would like to see at least some empty cells when there is no data.
Meaning it will show on start up at least 10 empty cells.
To add data to the cell, I call the delegate method in my tableviewcontroller each and every time to add one single entity in the data array (am doing this, because I think it would be faster than waiting until the whole array is filled then call [self.tableView reloadData];) and then refresh it by using reloadRowsAtIndexPaths. But it crashed every single time when it reached to index 10 (error: ... before update number of data was 10, but after update is 11).
What I really want is:
1.) prepare some data
2.) send it to uitableview controller and add it to an array there, instead of waiting and then sending a whole array to table view and refresh at once.
3.) reload just one row after the update (instead of using reloadData -> since I have different color of cell, the whole reload thing cause my table view flash madly).
The one thing I am doing to cell customization is in willDisplayCell:
What I did there is to change the background color of the cell. Again, nothing fancy.
But since there is no data at start up, no cell is ever visible (ui tablew with no cell at displayed at all), unless I did this
return dataArray.count < 10? 10 : dataArray.count;
just so there are at least 10 empty cells showing (WHY do I have to do the above just to display some customized empty cells beats me...).
Using reloadData is to refresh no problem, but since I am updating the data source array in table view every time data is ready instead of saving all prepared data to this array and send it over to table view to update by using reloadData, I would like to update row by row.
I kind of feel that the error comes from the fact that, if I add one item in the array and then call reloadRowsAtIndexPath, it will say "Ok, you had one item before, but after update there is 2! Inconsistency.."
I have already tried using [tableView beginUpdate]; and [tableView endUpdate];
Nothing has worked so far.....
So to sum up: how can I have different colors of cells showing even when the data array is empty on start up (just like the default ui table view with cells displaying completely even with no data) and update just one of the cells once a piece of data is ready instead of updating the whole ui table view with reloadData?
Many thanks in advance, please advise. Regards.
"how can I have different colors of cells showing even when the data array is empty"
Don't have an empty array, have a mutable array where all the members are initially empty strings, and replace those with your real data when you get it.
"update just one of the cells once a piece of data is ready"
Update your array with the new data, and then use reloadRowsAtIndexPaths:withRowAnimation: to update the table. If you want to see the table update row by row (slow enough to see), then put your data in a temporary array first, then add it one element at a time using performSelector:withObject:afterDelay:, calling reloadRowsAtIndexPaths:withRowAnimation: after each addition.
It's a little hard to tell exactly what you want, but here is an example of what I mean. This table displays 20 empty rows, all with different colors, for 2 seconds, then it replaces the empty strings in displayData with the strings in theData one by one at a rate of 10 per second.
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#property (strong,nonatomic) NSMutableArray *displayData;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.displayData = [#[#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#""] mutableCopy];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine",#"ten",#"Black",#"Brown",#"Red",#"Orange",#"Yellow",#"Green",#"Blue",#"Violet",#"Gray",#"White"];
[self.tableView reloadData];
[self performSelector:#selector(addData) withObject:nil afterDelay:2];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.displayData.count;
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UIColor *cellTint = [UIColor colorWithHue:indexPath.row * .05 saturation:1.0 brightness:1.0 alpha:1.0];
cell.backgroundColor = cellTint;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.displayData[indexPath.row];
return cell;
}
-(void)addData {
static int i = 0;
[self.displayData replaceObjectAtIndex:i withObject:self.theData[i]];
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
if (i < self.displayData.count) [self performSelector:#selector(addData) withObject:nil afterDelay:.1];
}
If you don't want any delay between row updates, and you want to make it work when displayArray has a different number of rows that theData, this version of addData should work:
-(void)addData {
static int i = 0;
if (i < self.displayData.count && i< self.theData.count) {
[self.displayData replaceObjectAtIndex:i withObject:self.theData[i]];
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
[self addData];
}else if (i >= self.displayData.count && i< self.theData.count) {
[self.displayData addObject:self.theData[i]];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
[self addData];
}
}
The code below is creating a search for many strings. Initially there are 5 rows, when you reach row five, it adds another row. Instead of just directly editing the row, i load a filter controller (another view controller that as you type it completes words for you). When the user finishes finding a word he clicks it and comes back to this view controller. Now i want to fill the cell that was originally tapped with the text from the filter.
I tried asking earlier and didn't get any concrete answers.
I am running into a problem where when i scroll (after adding a new row), it starts filling in those rows with info already in the table, (as opposed to staying blank)
Please help me where i am going wrong
//global indexpath to remember which cell tapped
NSIndexPath *globalPath;
#interface SearchViewController ()
#end
#implementation SearchViewController
//Load implementation once per launch
- (void)viewDidLoad
{
[super viewDidLoad];
[self linkInputTableToDelegate];
_temporaryResultsArray =[[NSMutableArray alloc]init];
_flurryArray=[[NSMutableArray alloc]init];
_numberOfSections=6;
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:NO];
[InputTable reloadData];
textFromUserDefaults=[[[HelperMethods alloc]init]getObjectUserDefault:#"textFiltered"];
[self addTextToFlurryArrayForFlurryAndSavedLists:_textFromUserDefaults];
}
-(void)viewDidDisappear:(BOOL)animated{
}
- (IBAction)searchButtonPressed:(UIButton *)sender {
self.tabBarController.selectedIndex = 1;
}
//Makes the input table respond to delegate table view methods
-(void)linkInputTableToDelegate{
_inputTable.dataSource=self;
_inputTable.delegate=self;
}
-(void)performSearch:(NSString*)text{
//do search
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
int numberOfRows=_numberOfSections;
//Rows for iPhone 4
if ([[UIScreen mainScreen]bounds].size.height==480) {
numberOfRows=numberOfRows;
//Rows for iPhone 5
}else if ([[UIScreen mainScreen]bounds].size.height==568){
numberOfRows=numberOfRows+1;
}
return numberOfRows;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//In reality groups are created with 1 row inside, this is to allow spacing between the rows
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *kCellID = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellID];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellID];
}
//Is the cell the same as the one clicked when going to ingredient filter
BOOL cellIndexPathSameAsSelected=[self isCellIndexSameAsPreviousClicked:indexPath];
cell.textLabel.textColor=[UIColor blackColor];
if (cellIndexPathSameAsSelected && _textFromUserDefaults!=nil) {
if (![cell.textLabel.text isEqualToString:_textFromUserDefaults]) {
cell.textLabel.text=_textFromUserDefaults;
[self performTextSearch:_textFromUserDefaults];
}
}
return cell;
}
//Compares the previous clicked cell with the cell now selected
-(BOOL)isCellIndexSameAsPreviousClicked: (NSIndexPath*)cellPath{
if (cellPath.row == globalPath.row && globalPath.section==cellPath.section) {
return YES;
}
else{
return NO;
}
}
- (void)updateTableViewWithExtraRow :(NSIndexPath*)rowSelected{
NSLog(#"number of sections =%i",_numberOfSections);
if (rowSelected.section == _numberOfSections) {
_numberOfSections ++;
}
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellText = [tableView cellForRowAtIndexPath:indexPath].textLabel.text;
[[[HelperMethods alloc]init]saveObjectToUserDefaults:cellText :#"textFiltered"];
globalPath = indexPath;
[self updateTableViewWithExtraRow:indexPath];
}
-(void)addTextToFlurryArrayForFlurryAndSavedLists:(NSString*)text{
if ([_flurryArray count]==0 &&[text length]>0) {
[_flurryArray addObject:text];
}
for (int i=0;i<[_flurryArray count];i++) {
NSString *textInArray=[_flurryArray objectAtIndex:i];
if (![textInArray isEqualToString:text]) {
[_flurryArray addObject:text];
}
}
NSLog(#"Total number of saved items = %i",[_flurryArray count]);
}
// Dispose of any resources that can be recreated.
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
I have a couple of reactions looking at the code:
A couple of observations about the proper use of the UITableViewDataSource methods, specifically numberOfRowsInSection, numberOfSectionsInTableView, and cellForRowAtIndexPath:
These really should be driven by some model data structure (e.g. a NSMutableArray) and nothing else;
These methods should be stateless. They should not relying on the value of some NSString instance variable, like _textFromUserDefaults) but rather always look up the value in the NSMutableArray model structure on the basis of the value of the indexPath parameter. You simply cannot make any assumptions about when cellForRowAtIndexPath will be called. This may well account for your duplicate values.
None of these should be doing anything besides responding to the UITableView inquiry. For example, your cellForRowAtIndexPath is invoking performTextSearch. It really shouldn't do anything except return the cell.
Your cellForRowAtIndexPath currently has conditional logic and only updates the cell if certain conditions holds. Because cells are reused, you really want to make sure that you initialize the cells regardless. You can't be assured that the cell is blank when you get it, nor that the previous contents are the previous values for that indexPath. Because cells are reused, it could be for an entirely different row. This could also account for your duplicative entries.
Regarding the interaction of the master view controller and the details view controller, there are more elegant ways than passing data back and forth via NSUserDefaults. For example when you initiate the details view controller, you could just pass it the information it needs. And when it's done, it should call a method in the master view controller to update the data in the master view. To do that, the master view controller should conform to some protocol of your own creation. If you see the example I shared via chat, you can see what that might look like. Anyway, by having some delegate method in the master view controller that the detail view controller calls when it's done, that eliminates the rather fragile technique of using viewDidAppear to control the updating of the master table view.
You might want to contemplate employing "edit" (which allows you to delete, possibly also edit a particular row) and "add" buttons like the standard "master-detail" template that Xcode provides. There are a number of standard conventions here that might be better than having an array of blank cells that you can then tap on. Clearly, your user experience is entirely up to you, but you can always contemplate whether there are existing, familiar conventions that you might employ.
Rob's feedback is good. In broader terms, you can't rely on the cells in a UITableView to hold onto their data. For efficiency, it will be creating, using, and destroying cells at will, and using cellForRowAtIndexPath to figure out what they should look like. Instead of testing what's in a cell, you need to have your own set of data which describe the value of each cell, and just set the value based on the indexPath. I'd recommend storing all your cell information in an NSMutableArray which contains NSStrings or something more complicated if necessary. It will be easy to set default values when you add cells to the array. Then cellForRowAtIndexPath can just access the array rather than attempting its own logic based on current cells.