I've got my cellForRowAtIndexPath delegate method defined as so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
PLOTCheckinTableViewCell *cell = (PLOTCheckinTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CheckinCellIdentifier forIndexPath:indexPath];
if([self.items count] == 0){
return cell;
}
NSDictionary *checkin = self.items[indexPath.row];
// configure and return custom cell
}
I'm using a custom cell class (PLOTCheckinTableViewCell).
I faced an issue where the user would pull to refresh and then attempt to pull again before the first request had completed (on completion of the request, I reload the table data). When they did this, the app would crash and say that indexPath.row was basically out of bounds, ie. the array was empty.
By putting in this IF check above, I mitigated the crash.
However,
Why exactly does my IF check "work", I see no visual implications of returning the cell before it's been configured. This is confusing
Are there any better ways to guard against this happening (ie. the table data being reloaded with an empty array)? Surely the numberOfRowsInSection would have returned array count which would be 0? (if it was an empty array)
EDIT (further code)
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
float count = [self.items count];
return count;
}
- (void)resetData {
self.items = [NSMutableArray array];
}
-(void) refreshInvoked:(id)sender forState:(UIControlState)state {
[self resetData];
[self downloadHomeTimeline];
[self.refreshControl endRefreshing];
}
- (void)downloadHomeTimeline {
[self.apiClient homeTimeline:self.page completionBlock:^(NSDictionary *data){
for (NSDictionary *obj in data[#"items"]) {
[self.items addObject:obj];
}
[self.itemsTableView reloadData];
}];
}
I couple of things that i would suggest to do. Make sure that the [self.itemsTableView reloadData] is executed on the main thread and also i would put the [self.refresControl endRefreshing] in the completion block. This way it will stop the refresh when its done and you should not let the user more then once simultaneously.
- (void)downloadHomeTimeline {
[self.apiClient homeTimeline:self.page completionBlock:^(NSDictionary *data){
for (NSDictionary *obj in data[#"items"]) {
[self.items addObject:obj];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.itemsTableView reloadData];
[self.refreshControl endRefreshing];
});
}];
}
Also in the numberOfRowsInSection just return count
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.items count];
}
To add to the answer. You should not reset the array before you receive new data. While getting new data the user can still scroll the table and that means new cells will be created but your NSMutableArray doesn't have any data. That is when you get the error and app crashes. You would have to [tableView reloadData] to clear the table so that the tableView would know that there are 0 rows, which i don't think is your intent.
Let me know if that's solves the issue.
Related
I have created expanded tableview using plist, I have populated all the values and images into my tableview. Here I want to make animation for my particular cell when user click that cell.
For example, first time images will be default, next time if I click that cell, the particular cell get expanded. Here I need to make my image has hide; again if user press it will show the image. How to do this?
Every time the user taps a cell, a UITableViewDelegate method gets called, and in this method you can reload the cell:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
When a cell is reloaded, the method below is called (between the others), and the only thing you have to do is to return a different height according with the current state of the cell:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return (/* cell state */) ? 100.0 : 50.0;
}
You have multiple ways to store your cell's state, you can do it in its model for example.
There are other ways also, this is only a solution. But the key point is that you have to reload the cell, and return a different height according with the conditions you want to consider.
You can check my full solution in the following github
Here is some of my implementation.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
movies = [[NSArray alloc] initWithObjects:
(NSArray*)[[Section alloc ] init:#"Istanbul" movieNames:#[#"Uskudar", #"Sariyer"] isExpanded:false],
(NSArray*)[[Section alloc ] init:#"Bursa" movieNames:#[#"Osmangazi", #"Mudanya", #"Nilufer"]
isExpanded:false],
(NSArray*)[[Section alloc ] init:#"Antalya" movieNames:#[#"Alanya", #"Kas"] isExpanded:false], nil
];
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return movies.count;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return ((Section*)movies[section]).movies.count ;
}
-(void)toggleSection:(ExpandableHeaderFoorterView *)headerView withSection:(int)section
{
((Section*)movies[section]).expanded = !((Section*)movies[section]).expanded;
[expandableTableView beginUpdates];
for (int i= 0; i< ((Section*)movies[section]).movies.count; i++)
{
NSArray* rowsToReload = [NSArray arrayWithObjects:[NSIndexPath indexPathForRow:i inSection:section], nil];
[expandableTableView reloadRowsAtIndexPaths:rowsToReload
withRowAnimation:UITableViewRowAnimationFade];
}
[expandableTableView endUpdates];
}
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
ExpandableHeaderFoorterView* headerView = [[ExpandableHeaderFoorterView alloc] initWithCustom:((Section*)movies[section]).genre withSection:(int)section withDelegate:self];
return (ExpandableHeaderFoorterView*)headerView;
}
http://www.iostute.com/2015/04/expandable-and-collapsable-tableview.html
You can see this tutorial. It's a very good tutorial based on expandable tableview cell.
I need to display data in a cell, when the table gets loaded and when it is scrolled. I get data into the table rows the very first time the app is run, but when I scroll the table, the table does not update all values. It shows only the last fetched value from the service.
My Service request format is :
http://vapp.sites.net/......userId=967730&rowsPerPage=10&nextCursorMark=1
Note - when i call service, the NextCursorMark is increased at response, which i m passing to next service call at scroll time.When nextCursorMark is nil i have to stop scrolling the table because there s no more new response data.
Response :
{
MESSAGE: "Success",
STATUS_CODE: 200,
REQUEST: [ ],
RESPONSE: {
OrderList: [..<items>..],
nextCursorMark: "2",
totalOrderCount: 45
}
}
when the table gets loaded for the first time nextCursorMark value is 1 and on each scroll the value will be increased based on totalOrderCount. How can I manage the table view datasource when it is scrolled and the service is called, on each scroll?
My code below:
1 - when loading the Table
#pragma mark - UITableViewDataSource Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)theTableView
{
return self.arrOrderList.count;
}
- (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section
{
OrderList * objOrderList = (OrderList *)[self.arrOrderList objectAtIndex:section];
return objOrderList.orderDetailsList.count;
}
- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"OrderDetailsListCell";
OrderDetailsListCell * cell = (OrderDetailsListCell *)[theTableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[OrderDetailsListCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
OrderList * objOrderList = (OrderList *)[self.arrOrderList objectAtIndex:indexPath.section];
OrderDetailsList * objOrderDetailsList = (OrderDetailsList *)[objOrderList.orderDetailsList objectAtIndex:indexPath.row];
[cell loadDataWithOrderDetailList:objOrderDetailsList];
return cell;
}
- (void) ServiceCallForOrderHistoryWithCurrentPageMark:(NSString *)page
{
if(![[VCSpinnerView sharedInstance] isSpinnerAddedToTheView:self.view])
[[VCSpinnerView sharedInstance] showInView:self.view];
__block __weak VCMyOrderViewController* blockMyOrderListView = self;
[[VCModelManager sharedInstance] getMyOrderDetail:[[VCConfiguration sharedConfig] getLoggedInUserId] andPageNumber:page completionBlock:^(id result, NSError *error)
{
if (!error)
{
blockMyOrderListView.orderHistoryDataObject = (OrderHistoryBaseClass *)result;
dispatch_async(dispatch_get_main_queue(), ^{
[blockMyOrderListView setDataSourceMyOrders:blockMyOrderListView.orderHistoryDataObject.rESPONSE];});
}
else
{
NSLog(#"\nError->%#",[error debugDescription]);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[VCSpinnerView sharedInstance] removeSpinnerView];
});
}];
}
- (void)setDataSourceMyOrders:(OrderRESPONSE *)response
{
self.orderHistoryDataObject.rESPONSE = response;
[self setPageMark:self.orderHistoryDataObject.rESPONSE.nextCursorMark];
[self setPageCount:[self.orderHistoryDataObject.rESPONSE.totalOrderCount integerValue]];
self.arrOrderList = [NSMutableArray arrayWithArray:self.orderHistoryDataObject.rESPONSE.orderList];
TrackOrderView *trackOrderView = [[TrackOrderView alloc] init];
[trackOrderView setDelegate:self];
[self.tblMyOrder setTableHeaderView:trackOrderView];
[self.tblMyOrder reloadData];
}
2 - when scrolling the Table
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if (self.arrOrderList.count < self.pageCount)
{
[self ServiceCallForOrderHistoryWithCurrentPageMark:self.pageMark];
}
}
You need to:
Have a mutable array to store the full list
Have variable to store the next URL (or next cursor mark)
In viewDidLoad, set the mutable array to an empty array, and the URL to the initial URL
Still in viewDidLoad, call your method to load more data
This method should append the data received to your mutable array, update the URL with the next URL to call (or nil if there's no more data)
The method should call beginUpdates, then insertRowsAtIndexPaths:withRowAnimation: and then endUpdates
In scrollViewDidScroll, check the scroll offset against the size of the table and the visible size, and if you're near the end, call the method to load more data.
Of course, the method that loads additional data should exit right away when the URL to load is nil
I have 2 UITableViewControllers in my project.
The problem I am having is that I am getting blank cell entries in the tableView opposite to the tableView where the data is entered.
I can't seem to figure out why this is the case.
It's creating blank rows in this tableView even though the information is from the other UITableViewController.
Here's the main tableView part from the one of the 2 UITableViewControllers:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSLog(#"number of addedSpaceObjects %lu",(unsigned long)[self.diaryoptions count]);
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"number of sections %ld",(long)section);
// Return the number of rows in the section.
return [self.diaryoptions count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentification = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentification
forIndexPath:indexPath];
Data2 *diary = [self.diaryoptions objectAtIndex:indexPath.row];
cell.textLabel.text = diary.diaryname;
cell.detailTextLabel.text = diary.diaryWeight;
return cell;
}
And from other UITableViewController:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSLog(#"number of addedSpaceObjects %lu",(unsigned long)[self.addedSpaceObjects count]);
// Return the number of sections.
if ([self.addedSpaceObjects count]) {
return 2;
}
else {
return 1;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"number of sections %ld",(long)section);
// Return the number of rows in the section.
if (section == 1) {
return [self.addedSpaceObjects count];
}
else {
return [self.recipes count];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentification = #"Josh";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentification
forIndexPath:indexPath];
if (indexPath.section == 1) {
Data *recipe = [self.addedSpaceObjects objectAtIndex:indexPath.row];
cell.textLabel.text = recipe.name;
}
else {
// Configure the cell...
Data *recipe = [self.recipes objectAtIndex:indexPath.row];
cell.textLabel.text = recipe.name;
}
return cell;
}
Here is the full project on GitHub. https://github.com/josher32/Plant-Diet
Appreciate any help anyone can offer!
Ok, so I checked out the app and I'll try my best to explain the problem as precisely as I can to cover it adequately.
Firstly, the classes in question are:
RecipesTableTableViewController
AddRecipeViewController
Data
DiaryTableViewController
AddDiaryViewController
Data2
Secondly, we'll need to look into your
#define ADDED_SPACE_OBJECTS2 #"Added Space Objects Array"
AddRecipeViewController
So... AddRecipeViewController basically creates a Data object that is kept in an array and eventually stored in NSUserDefaults under the key name Added Space Objects Array.
Great!! So you now have got recipe related stuff in some Data object.
AddDiaryViewController
Same thing here.
AddDiaryViewController creates a Data2 object that is eventually stored in NSUserDefaults under the same key name Added Space Objects Array.
But before storing this, you're taking the old value of the key Added Space Objects Array, which is an array, and adding a new object to it before placing it back into NSUserDefaults.
But now... this array will now have a combination of Data as well as Data2 objects!
RecipesTableTableViewController
When we come here, things get real.
- (void)viewDidLoad
{
//...
NSArray *myRecipeAsPropertyLists = [[NSUserDefaults standardUserDefaults] arrayForKey:ADDED_SPACE_OBJECTS_KEY];
for (NSDictionary *dictionary in myRecipeAsPropertyLists) {
Data *spaceObject = [self spaceObjectForDictionary:dictionary];
[self.addedSpaceObjects addObject:spaceObject];
}
}
Since we already realized that self.addedSpaceObjects can contain Data as well as Data2 objects, in the case whendictionary is containing stuff specific to type Data2, spaceObjectForDictionary will not be able to translate it properly to the required Data object.
We're expecting name, title, ingredients, directions but we're getting diaryentry, diaryname,diaryWeight.
So (in this scenario):
The values of name, title, ingredients, directions will be nil
The section-row count will be incorrect because it will give count of both Data as well as Data2 objects (and we don't care about Data2 objects in the RecipesTableTableViewController class... right?... well anyways, I assumed)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//...
if (indexPath.section == 1) {
Data *recipe = [self.addedSpaceObjects objectAtIndex:indexPath.row];
cell.textLabel.text = recipe.name;
}
//...
}
We see recipe.name is nil, for some indexPaths, ergo blank rows and vice versa in DiaryTableViewController.
Solution:
Firstly, I wouldn't recommend NSUserDefaults for your purposes but anyways...
Basically, don't use a single #"Added Space Objects Array" key for your NSUserDefaults stuff.
I'd suggest you use 2 separate keys.
//replace
//#define ADDED_SPACE_OBJECTS2 #"Added Space Objects Array"
//with
#define ADDED_SPACE_OBJECTS2 #"RecipeEntries" //in RecipesTableTableViewController
//and
#define ADDED_SPACE_OBJECTS2 #"DiaryEntries" //in DiaryTableViewController
Basically, segregate the entries instead of mixing them up under a single key name.
This seems like the quickest way to solve your problem without changing your logic.
I have a for loop in my project like this:
for (int i = 0; i < 2; i++) {
[array addObject:randomObject];
[self.tableView reloadData];
}
I have my dataSource methods like this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"Count: %lu",(unsigned long)array.count);
return [array count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"Reloading Data");
static NSString *CellIdentifier = #"Random";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
return cell;
}
But for some strange reason only numberOfRowsInSection is getting called and cellForRowAtIndexPath is not.
Also cellForRowAtIndexPath DOES get called in the last time of the loop(when x is = to 1)
the tableview only asks for cells when it updates its view hierarchy. Not when reloading its data. That will only cause it to update its internal size information.
Updating cells only happens when the runloop is pogressed. When you are in a loop, the main thread is blocked and then runloop is not progressed.
Only after your loop the OS runs the runloop and the table calls for Cells.
All the datasource methods are called on the same thread. By calling reloadData you start the "queue" of datasource methods, by calling it again you cancel the previous "queue". This mechanics protects datasource from being inconsistent as if you have the data changed (hence the next reloadData call), the first "queue" operations would have outdated state.
I´m quite new to iOS development and I´m having a terrible time by trying something that should be easy; to add an extra row in a TableView everytime the user clicks on one of the existing rows. There is no real purpose on that action, I´m just wanting to understand the behaviour of TableView.
So I did the following:
I used a Split View-based template and changed the number of rows to 30 in the RootViewController.
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return 30;
}
The method tableView:didSelectRowAtIndexPath looks in the following manner:
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
/*
When a row is selected, set the detail view controller's detail item to the item associated with the selected row.
*/
NSMutableArray* paths = [[NSMutableArray alloc] init];
NSIndexPath *indice = [NSIndexPath indexPathForRow:30 inSection:0];
[paths addObject:indice];
detailViewController.detailItem = [NSString stringWithFormat:#"Second Story Element %d with all its information and bla bla bla", indexPath.row];
[[self tableView] beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[[self tableView] endUpdates];
}
When I execute the program and click on one of the elements, I receive the following error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (30) must be equal to the number of rows contained in that section before the update (30), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted).'
I did not change any other part of the code that the template provides.
I read quite extensively the documentation from Apple and the responses to the following questions:
Add a row dynamically in TableView of iphone
and
how to properly use insertRowsAtIndexPaths?
The second question seems to address the same problem, but I´m not capable to understand what is happening. What do they mean with dataSource? The response that I understand better says the following:
It's a two step process:
First update your data source so numberOfRowsInSection and cellForRowAtIndexPath will return the correct values for your post-insert data. You must do this before you insert or delete rows or you will see the "invalid number of rows" error that you're getting.
What does this update of the data source implies?
Sample code would be HIGHLY appreciated, because I´m totally frustrated.
By the way, all that I´m trying has nothing to do with entering the editing mode, has it?
You need to keep the count returned by tableView:numberOfRowsInSection: in sync!
So when you have 30 rows and then tell the tableview to insert a new row you need to make sure tableView:numberOfRowsInSection: will now return 31.
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section
{
return self.rowCount;
}
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
self.rowCount++;
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
In practice you would probably use an array to track your rows return [self.rows count]; etc
The answer is quite simple. When you want to modify a table view you need to perform two simple steps:
Deal with the model
Deal with table animation
You already perform the second step. But you have missed the first one. Usually when you deal with a table you pass it a data source. In other words some data to display within it.
A simple example is using a NSMutableArray (it's dynamic as the name suggests) that contains dummy data.
For example, create a property like the following in .h
#property (nonatomic, strong) NSMutableArray* myDataSource;
and in .m synthesize it as:
#synthesize myDataSource;
Now, you can alloc-init that array and populate it as the following (for example in viewDidLoad method of your controller).
self.myDataSource = [[NSMutableArray alloc] init];
[self.myDataSource addObject:#"First"];
[self.myDataSource addObject:#"Second"];
Then, instead of hardcoding the number of rows you will display (30 in your case), you can do the following:
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section {
return [self.myDataSource count];
}
Now, in you didSelectRowAtIndexPath delegate you can add a third element.
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.myDataSource addObject:#"Third"];
[[self tableView] beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[[self tableView] endUpdates];
}
It looks like one big problem is with tableView:numberOfRowsInSection:. You need to return the correct number of rows in that method.
To do that, it's usually best to maintain an NSArray or NSMutableArray of items for the table view so in that function, you can say: return [arrayOfValues count];. Keep the array as a property of your view controller class so that it's readily accessible in all methods.
The array can also be used in cellForRowAtIndexPath:. If you have an array of NSString, you can say cell.text = [arrayOfValues objectAtRow:indexPath.row];.
Then, when you want to add an item to the table view, you can just add it to the array and reload the table, e.g. [tableView reloadData];.
Try implementing this concept and let me know how it goes.
You can Also do that for dayanamic table cell
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [arrayStationStore count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIndentyfire;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIndentyfire];
if (cell == nil)
{
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIndentyfire];
}
cell.textLabel.text = [arrayStationStore objectAtIndex:indexPath.row];
return cell;
}
-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Check if current row is selected
BOOL isSelected = NO;
if([tblStationName cellForRowAtIndexPath:indexPath].accessoryType == UITableViewCellAccessoryCheckmark)
{
isSelected = YES;
}
if(isSelected)
{
[tblStationName cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryNone;
[arrayReplace removeObject:indexPath];
NSLog(#"array replace remove is %# ",arrayReplace);
}
else
{
[tblStationName cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryCheckmark;
[arrayReplace addObject:indexPath];
NSLog(#"array replace add is %# ",arrayReplace);
}
return indexPath;
}