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
Related
I have a table view with custom cells (all configured in a subclass using auto layout).
The cells load fine, display fine, everything is fine.
The issue is when I am inserting more rows (at the bottom). The table view is representing a feed for posts, so when the user scrolls to the bottom, before reaching the last cell, I load new posts, and then insert them into the table.
When I do this, I get this weird glitchy effect where the cells randomly come down (behind the previous cells) into place, the table view scrolls up a bit, messy.
CODE AT BOTTOM
I've uploaded a clip of me scrolling. When you see the activity indicator,
I stop scrolling. The rest of the movement is from the glitchy behavior.
Is the reason for the glitch because the cells are being drawn with auto-layout?
I would hope not, but idk..I'm not sure what to do regarding a solution. If anyone has any ideas please let me know.
FYI:
I have this (of course, since the cells are all using auto layout)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
I've tried setting the estimated height to an "average" of the expected cell heights, around 65. No difference.
Update
Here's some code:
HomeViewController.m --> viewDidLoad
...
self.tableView = [KATableView.alloc initWithFrame:CGRectZero style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.refreshDelegate = self;
self.tableView.estimatedRowHeight = 75;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.view addSubview:self.tableView];
// Constrains to all 4 sides of self.view
[SSLayerEffects constrainView:self.tableView toAllSidesOfView:self.view];
my table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (!self.dataManager.didFinishFetchingData) return 4;
if (self.contentObjects.count == 0) return 1;
if (self.dataManager.moreToLoad) return self.contentObjects.count + 1;
return self.contentObjects.count + 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MYObject *object = self.contentObjects[indexPath.row];
SomeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:object.documentID];
if (!cell) {
cell = [SomeTableViewCell.alloc initWithStyle:UITableViewCellStyleDefault reuseIdentifier:object.documentID];
cell.delegate = self;
} else [cell startListeningForChanges];
return cell;
}
Here is how I am loading more data and adding it to the table view..
- (void)getHomeFeedData:(nullable void(^)(BOOL finished))completed {
[self.dataManager fetchHomeFeedDataForFeedOption:self.homeNavController.feedFilterOption completion:^(NSError * _Nullable error, NSArray<__kindof KAObject *> * _Nullable feedObjects) {
if (error != nil) {
NSLog(#"something went wrong: %#", error.localizedDescription);
if (completed) completed(NO);
return;
}
NSInteger originalCount = self.contentObjects.count;
if (self.dataManager.isFirstTimeLoading) self.contentObjects = feedObjects.mutableCopy;
else {
if (self.dataManager.isGettingNew) for (MYObject *obj in feedObjects) [self.contentObjects insertObject:obj atIndex:0];
else if (feedObjects.count > 0) [self.contentObjects addObjectsFromArray:feedObjects];
}
if (feedObjects.count > 0) {
if (self.dataManager.isFirstTimeLoading) [self.tableView reloadData];
else {
[self.tableView insertCells:feedObjects forSection:0 startingIndex:self.dataManager.isGettingNew? 0 : originalCount];
}
} else if (self.dataManager.isFirstTimeLoading) [self.tableView reloadData];
if (completed) completed(YES);
}];
}
NOTE:
[self.tableView insertCells:feedObjects forSection:0 startingIndex:self.dataManager.isGettingNew? 0 : originalCount];
is simply this:
- (void)insertCells:(nullable NSArray *)cells forSection:(NSInteger)section startingIndex:(NSInteger)start {
if (!cells) return;
NSMutableArray *indexPaths = #[].mutableCopy;
for (id obj in cells) {
NSInteger index = [cells indexOfObject:obj] + start;
[indexPaths addObject:[NSIndexPath indexPathForRow:index inSection:section]];
}
[self insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
}
Update 2
My UITableViewCell subclass content is hidden ATM (too much difficulty in editing all my post content for the purpose of this post). I just have the subviews of each cell set to alpha = 0.f. It's just an image view, some labels, and some buttons.
No constraint issues in console, cells render perfectly when calling [self.tableView reloadData] so maybe there is something I'm doing wrong when inserting the cells?...
When you dealing with UITableView glitches:
Make sure you call UIKit API's on a main thread - turn on Main Thread checker
In your case, there might be an issue that fetchHomeFeedDataForFeedOption:completion: completion block is called not on a main thread.
Your insert is definitely wrong - all delete/insert/update/move calls for UITableView should be wrapped in beginUpdates/endUpdates
Your "load more" component at the bottom might be an issue. You need to address how it's managing contentSize/contentOffset/contentInset of table view. If it does anything but manipulating contentInset - it does wrong job.
While it's hard without debugging the whole solution, I bet options 2 & 3 are the key problems out there.
This is my error:
-[__NSArrayM objectAtIndex:]: index 12 beyond bounds for empty array
I know this error means I'm trying to access an "empty array".
This error only happens in viewX when it is popped back from viewY. When you press 'back button' on navigation bar in viewY and scroll the tableView immediately, it will crash and cause this error. I am using the RETableViewManager to load my tableView.
In viewX's viewDidLoad:
[[RACSignal combineLatest:#[RACObserve(self, record), RACObserve(self, restaurant)]] subscribeNext:^(id x) {
[self setupItems];
}];
in setupItems:
RecordManager *recordManager = [[EZRecordManager alloc] initWithRecord:self.record restaurant:self.restaurant sender:self.navigationController];
self.items = [recordManager items];
self.section = [RETableViewSection section];
[self.items each:^(id data) {
if ([data isKindOfClass:[NSString class]]) {
self.navigationItem.title = (NSString *)data;
} else {
[self registerItem:[data class]];
[self.section addItem:data];
}
}];
[self.manager addSection:self.section];
[self.tableView reloadData];
I NSLogged my array 'self.items'. and this is what logs according to the method:
viewDidAppear - (
"\U5df2\U8a02\U4f4d\Uff0c\U5c1a\U672a\U7528\U9910",
"<REReservationHeaderItem: 0x14015b0b0>",
"<REAttributedStrItem: 0x14015b1b0>",
"<REAttributedStrWithNextItem: 0x140191a70>",
"<REAttributedStrItem: 0x140193f60>",
"<RESpacerItem: 0x140194870>",
"<REAttributedStrWithNextItem: 0x14019ce10>",
"<REAttributedStrItem: 0x140199230>",
"<RESpacerItem: 0x1401a04e0>",
"<REActionItem: 0x14019e490>",
)
The NSLog logs the same array in setupItems so I know the array is still there because self.item is saved as a property:
#property (nonatomic, strong) NSArray *items;
So this algorithm works as expected when I'm loading viewX for the first time, but as soon as I go into another view(viewY) and press the 'back button' on viewY to pop to viewX and then immediately scroll, it crashes with the above error. If I wait for a second (maybe even half a second), viewX will work properly and have no issue. I know this is minor but my PM is stressing that this shouldn't happen. How can I solve this problem?
The method the error occurs in (part of the RETableViewManager library):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
RETableViewSection *section = [self.mutableSections objectAtIndex:indexPath.section];
RETableViewItem *item = [section.items objectAtIndex:indexPath.row];
UITableViewCellStyle cellStyle = UITableViewCellStyleDefault;
if ([item isKindOfClass:[RETableViewItem class]])
cellStyle = ((RETableViewItem *)item).style;
NSString *cellIdentifier = [NSString stringWithFormat:#"RETableViewManager_%#_%li", [item class], (long) cellStyle];
Class cellClass = [self classForCellAtIndexPath:indexPath];
if (self.registeredXIBs[NSStringFromClass(cellClass)]) {
cellIdentifier = self.registeredXIBs[NSStringFromClass(cellClass)];
}
if ([item respondsToSelector:#selector(cellIdentifier)] && item.cellIdentifier) {
cellIdentifier = item.cellIdentifier;
}
RETableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
void (^loadCell)(RETableViewCell *cell) = ^(RETableViewCell *cell) {
cell.tableViewManager = self;
// RETableViewManagerDelegate
//
if ([self.delegate conformsToProtocol:#protocol(RETableViewManagerDelegate)] && [self.delegate respondsToSelector:#selector(tableView:willLoadCell:forRowAtIndexPath:)])
[self.delegate tableView:tableView willLoadCell:cell forRowAtIndexPath:indexPath];
[cell cellDidLoad];
// RETableViewManagerDelegate
//
if ([self.delegate conformsToProtocol:#protocol(RETableViewManagerDelegate)] && [self.delegate respondsToSelector:#selector(tableView:didLoadCell:forRowAtIndexPath:)])
[self.delegate tableView:tableView didLoadCell:cell forRowAtIndexPath:indexPath];
};
if (cell == nil) {
cell = [[cellClass alloc] initWithStyle:cellStyle reuseIdentifier:cellIdentifier];
loadCell(cell);
}
if ([cell isKindOfClass:[RETableViewCell class]] && [cell respondsToSelector:#selector(loaded)] && !cell.loaded) {
loadCell(cell);
}
cell.rowIndex = indexPath.row;
cell.sectionIndex = indexPath.section;
cell.parentTableView = tableView;
cell.section = section;
cell.item = item;
cell.detailTextLabel.text = nil;
if ([item isKindOfClass:[RETableViewItem class]])
cell.detailTextLabel.text = ((RETableViewItem *)item).detailLabelText;
[cell cellWillAppear];
return cell;
}
Usually when "waiting a little fixes the problem", it's because you have an async problem.
Something to check first :
Make sure your reload code is called when you move back. Maybe your tableview didn't get emptied, but the array did. Moving back would let you scroll the old content (still loaded) but the delegate methods won't be able to create new cells because the array is now empty.
If you wait, your async method does it's job and the array is now full again, which makes everything work fine.
Possible solution :
Empty then reload the tableview in viewWillAppear. This will cause a visual flash of the tableview going empty and then full again. It will also scroll you to the first element. That being said, it's really easy and fast, and with a spinner it will appear much smoother.
Other possible solution :
Keep the data loaded after leaving the page, so when you come back it's still there. You can use anything that will keep the data loaded while in the app. It could be a singleton class that stays instantiated, or save in a database and reload from it (it's much faster than straight up loading from the internet), or anything that you can think of.
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.
I have a fitness plan page with an app which is a UITableViewController with multiple custom uitableviewcells. It relies on a reasnably large data feed - I'd like to show a loader when accessing this page whilst the data feed is being pulled back from the server.
I have setup a custom uiTableviewCell containg a styled loader message / activity indicator and would like to show this on load - then when the data is available - refresh the tableview and deque the data into the relevant cells.
Currently I have the following method in my viewdidload method, which currently shows an alert if the feed hasn't completed its load-
[[RKObjectManager sharedManager].HTTPClient setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
if(status == AFNetworkReachabilityStatusNotReachable)
{
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:nil
message:#"There is no network connection!"
delegate:nil
cancelButtonTitle:#"Dismiss"
otherButtonTitles:nil];
[alert show];
}
else
{
i'd like to alter this to show the loader cell instead - then refresh the view once the data load is complete -
I've altered to the following -
[[RKObjectManager sharedManager].HTTPClient setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
if(status == AFNetworkReachabilityStatusNotReachable)
{
_WoHpTV.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
workoutBannerCell *cell = [_WoHpTV
dequeueReusableCellWithIdentifier:#"loaderCell" ];
}
else
{
so i've got a refernce to the custom cell in the above - but my question is how do I add it to my TableView?
i've got a refernce to the custom cell in the above - but my question
is how do I add it to my TableView?
You need to implement datasource methods of UITableView. Since you want to show loading indicators in each of the table cell you would need a temp data (You cannot load 0 cells in table view even to show loader you need some visible cells). In view did load create an array of temp objects and call reloadData of tableview.
for(int i = 0; i < 5; i++) {
[_dataArray addObject:#{#"text":#"text_value"}];
_isDataLoaded = NO;
[_table reloadData];
Now this will populate your table with Temp data. Once HTTP call is done set that BOOL isDataLoaded to YES
Then in Data Source methods -
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (!_isDataLoaded)
return _dataArray.count; // No data return count for Temp Data
else
return _feedArray.count; // Return correct feed items count
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCustomCell *cell = [_tableView dequeueReusableCellWithIdentifier:#"cell_id"];
if (cell == nil)
// Initialise cell here
if (!_isDataLoaded) { // Data has not yet loaded. Set loaders
NSDictionary *data = [_dataArray objectAtIndex:indexPath.row];
// set Cell properties here
} else {
// Fetch data from feed array
}
}
And in your question you are trying to detect if feed is loaded or not based on AFNetworkReachabilityStatusNotReachable. Which is actually incorrect, as this state indicates no internet connection. To track availability of data a simple BOOL as shown above will do.
I have two UItableViews and the problem i am having is in going back from the second UITableView to the first UITableView. I have applied the back button using:
- (IBAction)BackButtonClicked:(id)sender
{
[self dismissModalViewControllerAnimated:YES];
}
The issue is that it takes me back to the previous UITableView but when I click on the row of that UITableView it returns an empty UITableView where there should be data in it.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
int i = 0;
if(indexPath.row == 0)
{
i = 0;
}
else
{
i = (indexPath.row * 4);
}
NSLog(#"%#", [ParsedData objectAtIndex:i]);
SubList *SubListNib = [[SubList alloc] initWithNibName:#"SubList" bundle:nil];
self->newSubList = SubListNib;
newSubList.folderid = [ParsedData objectAtIndex:i];
[self presentModalViewController:SubListNib animated:YES];
}
The Data gets entered in the Array the first time.
On going to the previous screen and coming back the data does not enter the array.
You should reload first tableview, when you returns from second tableview to first by
[firstTableViewName reloadData];
This method must also be include in backbutton click.