Background: I'm working on my first iOS app that uses a UITableView for data being loaded dynamically from a web source. No, this is not homework.
Problem: When I scroll down the list bounces back to the top.
Things I've Tried:
This suggestion (reducing the size of the table view) works but now I have large space at the bottom of the screen.
I've tried using [tableView reloadData] but this doesn't changing the behavior. I even hooked this up to a button to make sure it was firing after the view was populated.
I also read a few post about contentSize and how to calculate based on the amount of data loaded in the table. This looks like the solution I need but am having trouble getting a clear explanation of how to implement it for my setup.
Code:
I started a new project with single view application template and added a UITableView to the default view in the main storyboard.
In view controller I have the following (relevant) code:
#interface ERViewController ()
#property (nonatomic, strong) NSMutableArray *myArray;
#property (nonatomic,retain) UITableView *tableView;
#end
#implementation ERViewController
{
NSMutableArray *tableData;
}
Here I am loading the data from Parse.com into my table and then calling [self loadView] to get the view to update after the query finishes.
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableArray *returnedData = [[NSMutableArray alloc] init];
PFQuery *query = [PFQuery queryWithClassName:#"Persons"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *object in objects) {
//NSLog(#"display name: %#",object[#"firstName"]);
[returnedData addObject:[NSString stringWithFormat:#"%#, %#", object[#"lastName"],object[#"firstName"]]];
}
tableData = returnedData;
// Reload view to display data
[self loadView];
} else {
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"SimpleTableItem";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier];
}
// Configure the cell
cell.textLabel.text = [tableData objectAtIndex:indexPath.row];
NSArray *itemArray = [NSArray arrayWithObjects: #"✔︎", #"?", #"X", nil];
UISegmentedControl *mainSegment = [[UISegmentedControl alloc] initWithItems:itemArray];
mainSegment.frame = CGRectMake(200,0, 100, 43);
self.navigationItem.titleView = mainSegment;
mainSegment.selectedSegmentIndex = [itemArray count];
[mainSegment addTarget:self
action:#selector(mainSegmentControl:)
forControlEvents: UIControlEventValueChanged];
mainSegment.tag = indexPath.row;
[cell.contentView addSubview:mainSegment];
return cell;
}
Sample screen shot:
Since I'm waiting for the data to load I'm not sure where to resize the view and how exactly I need to resize the view.
You should never ever manually call loadView.
Once you have your tableData call reloadData on the tableView.
You should also load you table data in viewDidAppear:, and if you only want to do it once, create a flag to see if its already been loaded. While waiting for the data to load, tableData will be nil so if you query the count of it, you will be messaging a nil object and therefore it would act like returning 0.
Finally you should not be touching the contentSize of the table view, it handles that itself. You just need to make sure that in Interface Builder, the table view's frame is the same as the root views frame.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSMutableArray *returnedData = [[NSMutableArray alloc] init];
PFQuery *query = [PFQuery queryWithClassName:#"Persons"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
for (PFObject *object in objects) {
//NSLog(#"display name: %#",object[#"firstName"]);
[returnedData addObject:[NSString stringWithFormat:#"%#, %#", object[#"lastName"],object[#"firstName"]]];
}
tableData = returnedData;
// Reload view to display data
[self.tableView reloadData];
} else {
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
}
If you just want a UIViewController with only a UITableView in it, consider using a UITableViewController.
Posted for others dealing with the same problem I had and can't just move to a different view controller.
To solve the problem with a UITableView within a general view controller (not UITableViewController) click the little icon below the view controller that looks like this:
The button is titled "Resolve Auto Layout Issues"
Select "Reset to suggested constraints in View Controller"
This fixed all my scrolling issues since I must have been incorrectly snapping to the auto layout edges.
Related
I have a TableView (self.tableView) inside of a ViewController (DashboardViewController). Inside of this tableview, I have a custom cell that contains a UICollectionView.
Everything works fine, however in order to populate the UICollectionView, I'm currently calling data inside my UITableViewCell, which I don't want to do, as every time I scroll up and down, data is called.
Is it possible to make the UICollectionView's datasource an NSMutableArray inside DashboardViewController, instead of having to populate the NSMutableArray inside the UITableViewCell in which it is sitting?
My setup right now:
UITableViewCell.h
#property (weak, nonatomic) IBOutlet UICollectionView *collectionView;
#property (strong, nonatomic) NSMutableArray *clientsWeek;
UITableViewCell.m
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
[self.collectionView registerNib:[UINib nibWithNibName:#"ClientCollectionViewCell" bundle:nil] forCellWithReuseIdentifier:#"ClientCollectionViewCell"];
[_collectionView setDataSource:self];
[_collectionView setDelegate:self];
NSMutableDictionary *viewParams1 = [NSMutableDictionary new];
[viewParams1 setValue:#"cdata" forKey:#"view_name"];
[DIOSView viewGet:viewParams1 success:^(AFHTTPRequestOperation *operation, id responseObject) {
self.clientsWeek = [responseObject mutableCopy];
NSLog(#"The people are here %#", self.clientsWeek);
[self.collectionView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failure: %#", [error localizedDescription]);
}];
}
Under the premise that the CellReuseIdentifier is set correctly.
Of course, you can choose to get all the data in DashboardViewController, including the datasource of UICollectionView. Here you need to combine the data. The data source of the tableview must be an array, so the data source of UICollectionView can be placed in the array. Like this
[
{
"UITableViewCellData":{},
"UICollectionViewData":{}
},
...
]
Of course you can also use data model objects to replace.
Then you need to expose a method for receiving the data source to the cell, in which you can refresh your UICollectionView.
UITableViewCell.h
- (void)cellWithDataSource:(NSDictionary *)dataSource;
UITableViewCell.m
- (void)cellWithDataSource:(NSDictionary *)dataSource{
NSDictionary *tableViewDataSource = dataSource[#"UITableViewCellData"];
NSDictionary *collectionViewDataSource = dataSource[#"UICollectionViewData"];
...
}
DashboardViewController passes data to cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CellID"];
// first obtains the datasource of each cell, and passes it to each cell
[cell cellWithDataSource:datasource];
return cell;
}
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.
Once a user finished entering text in a UITextfield I wand the data to be in a dictionary first and then the dictionary added to array. But for some reason after inserting into array .. It logs null..
.h
#interface Bread_TableViewController : UITableViewController <UITextFieldDelegate>
{
NSMutableArray * inventoryarray;
}
**.m**
- (void)textFieldDidEndEditing:(UITextField *)textField
{
// Make sure to set the label to have a ta
NSString*textfieldclicked;
if (textField.tag == 1) {
textfieldclicked=#"Unit";
} else if (textField.tag ==2) {
textfieldclicked=#"Case";
}
id textFieldSuper = textField;
while (![textFieldSuper isKindOfClass:[UITableViewCell class]]) {
textFieldSuper = [textFieldSuper superview];
}
NSIndexPath *indexPath = [self.tableView indexPathForCell:(UITableViewCell *)textFieldSuper];
InventoryTableViewCell *cell = (InventoryTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
NSMutableDictionary * productinfo = [[NSMutableDictionary alloc] init];
[productinfo setObject:cell.product.text forKey:#"product"];
[productinfo setObject:textField.text forKey:textfieldclicked];
NSLog(#"productin %#", productinfo);
[inventoryarray addObject: productinfo];
}
-(IBAction)save{
NSLog(#"array %#", inventoryarray);
}
The non-visible cells do not actually exist. Only the visible ones plus one outside the screen at each end are actually in memory. The other ones are created when you scroll and their data is pulled from the data source.
As soon as a cell is pushed out of the screen (+1 cell) it will be removed from the hierarchy and added to the reuse pool.
TL;DR: You can't loop through out-of-screen cells since they don't exist. If you want to access the data for each item, do it in your data source.
alloc-init the array in which you want to add your objects.
do alloc-init of your dictionary outside the textFieldDidEndEditing method and where it scope it visible might be in viewDidLoad
like
#interface yourViewController ()
#property (strong, nonatomic) NSMutableDictionary * product info;
- (void)viewDidLoad {
self.productinfo = [[NSMutableDictionary alloc] init];
}
I am stucked in a stupid problem since two days. I have got a UITableViewController pushed in Navigation Controller. When it loads, since there is no data, so empty table is visible:
But when I receive data from server, and call [self.tableView reloadData], both numberOfRowsInSection and heightForRowAtIndexPath get invoke except cellForRowAtIndexPath and my controller is shown without table:
I can't really understand that why it is happening. All datasource methods are called except for cellForRowAtIndexPath. Please someone guide me... Thanks..
ActLogController.h
#interface ActLogController : UITableViewController<ASIHTTPRequestDelegate,UITableViewDataSource,UITableViewDelegate>
#property(strong) NSMutableArray *activityKeys;
#end
ActLogController.m
- (void)viewDidLoad
{
[super viewDidLoad];
activityKeys = [[NSMutableArray alloc] init];
self.tableView.dataSource = self;
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self retrieveActivityLogFromServer];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return activityKeys.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell...
ActivityLogUnit *act = [activityKeys objectAtIndex:indexPath.row];
cell.textLabel.text = act.price;
return cell;
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 50.0;
}
-(void) requestFinished:(ASIHTTPRequest *)request
{
NSArray *list = [request.responseString JSONValue];
for (int i = 0; i < list.count; i++) {
NSArray *singleTrade = [[list objectAtIndex:i] JSONValue];
ActivityLogUnit *unit = [[ActivityLogUnit alloc] init];
unit.symbol = [singleTrade objectAtIndex:0];
unit.type = [singleTrade objectAtIndex:1];
unit.price = [singleTrade objectAtIndex:2];
unit.volTraded = [singleTrade objectAtIndex:3];
unit.remVol = [singleTrade objectAtIndex:4];
unit.actualVol = [singleTrade objectAtIndex:5];
unit.recordID = [singleTrade objectAtIndex:6];
unit.orderNo = [singleTrade objectAtIndex:7];
unit.time = [singleTrade objectAtIndex:8];
[activityKeys addObject:unit];
}
if(activityKeys.count > 0)
{
[self.tableView reloadData];//it is called and I have got 6 items confirm
}
}
EDIT
I set some dummy data in my array activityKeys, Data is being displayed in table, and cellforrowatindexpath is called successfully. But as I reload data after sometime, other methods are called except this one and table disappears as shown in 2nd pic. Any ideas?
Your problem is that you probably download the data content on a background thread. Since you cannot update the UI on a background you need to call [self.tableView reloadData] on the main thread once the download is finished!
Hope it helps!
Looks like you in secondary thread, do reloadData in main thread by using following code
[self.tableView performSelectorOnMainThread#selector(reloadData) withObject:nil waitUntilDone:NO]
You can always use [NSThread isMainThread] to check whether you are in main thread or not.
you have to write in viewdidload
self.tableView.dataSource = self;
self.tableView.delegate = self;
Edit
You have no xib then where you are declared/sets your tableview's properties. Like
self.tableView.frame = CGRectMake(0, 45, 320, 500);
self.tableView.rowHeight = 34.0f;
self.tableView.separatorStyle=UITableViewCellSeparatorStyleNone;
[self.view addSubview:self.tableView];
[self.tableView setBackgroundColor:[UIColor clearColor]];
self.tableView.showsVerticalScrollIndicator=NO;
self.tableView.dataSource = self;
self.tableView.delegate = self;
Try with
#property(nonatomic,strong) NSMutableArray *activityKeys;
Firstly I strongly believe that the instance name of the tableview should not be similar to the local variable (i.e. tableView in class should not be equal to tableView in delegate and data source methods).
Second in your question posted I could not see the delegate set for the table view.
answer Posted By Samir Rathod should work if you have #property for the table view set in you .h or .m file.
You can also do this if you have a XIB file.
Press ctrl and click + drag the tableview to the files owner and set the delegate and datasource.
For me the problem was my stubbed-out code returning 0 as the number of sections (so it never asked how many rows were in the section, and never got their data). Just change that to 1 if it's your problem also. Additionally, I was working in Swift, where the issue mentioned by #shahid-rasheed is coded (slightly) differently:
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
At last I got it worked. cellForRowAtIndexPath was not being called because of a line of code I didn't mention here... which was actually removing some color background layer from view. It was causing reloading issue. After removing it, everything works fine.
Thank you all of you for your cooperation :)
I had the same symptoms too. In my case, the first time I loaded the data (from core data) in viewDidLoad, NSSortDescriptor was used to sort the data.
On the click of a button, the core data was fetched again (this time with changes) and tableView data reloaded. It initially gave me a blank table view after the button was clicked because I forgot to sort the data the second time I fetched it.
Learning points: Remember to call all methods which modify the cell (like background color mentioned by iAnum, or NSSortDescriptor in my case) if you have used them in the beginning!
So, simply put, I have the following setup:
UIViewController - this is the main, parentController
Within this main view, I have a UIScrollView at the very top, and a UITableView below it.
The UITableView is populated with core data via NSFetchedResultsController...
UIScrollView (The UIViewController is the delegate of this UIScrollView)
|- Within this, I have 2 UIViews that I show: 1 contains a UISegmentedControl, the other just contains nothing but UILabels
When the segmentedControl's value changes (ie: A user clicks on it), I just want to reload the UITableView's list of rows by issuing a new fetchRequest (by just changing the predicate and refetching) to the NSFetchedResultsController.
However, that's exactly where I began having an issue.
Whenever I issue [tableView reloadData], the top-most UIScrollView (not the UITableView's ScrollView, but the extra one I added to the UIViewController) stop responding.
Before I ever issue [tableView reloadData], scrolling works just fine. After I issue it, the scrollView stops responding, and scrolling stops completely.
I've tried all of the following:
[self.topScrollView setScrollingEnabled:YES]
self.topScrollView.delegate = self;
[self.topScrollView setContentSize:CGSizeMake(640.0f, self.topScrollView.frame.size.height)];
[self.topScrollView setContentOffset:CGPointMake(0, 0)];
I removed the UISegmentedControl completely. What I did instead was put a button at the very top of the view, and just issued [tableView reloadData];. It is when the tableView is reloaded, that the UIScrollView stops responding.
Is this a bug? Am I completely missing something here? The only thing I can even begin to think of is that the UITableView inherits from UIScrollView, and so that when the table reloads, it must be doing something to cause the topScrollView to either lose all sense of contentOffset/Size and/or its delegate or something...
Has anyone ever tried this? Has anyone ever had this issue?
The only way I could solve this was by creating a ContainerView, splitting the header out into its own UIViewController, and then setting that UIViewController as the UIVScrolLView delegate and just handling the entire header there.
While this works - I'm kind of confused, and would really like to know if a better alternative exists...
Edit: Here is a basic code example of what I was doing before I split it out the header into a ContainerView and just loaded it via embededSegue:
-(void)viewDidLoad {
[super viewDidLoad];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.topScrollView setContentSize:CGSizeMake(640.0f, self.topScrollView.frame.size.height)];
[self.topScrollView setContentOffset:CGPointMake(0, 0)];
}
// .....
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"listItemCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Load the cell with what is in core data at this index path..
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
#pragma mark SegmentedControl Switch
- (IBAction)segmentedControlValueChanged {
NSFetchRequest *aRequest = [[self fetchedResultsController] fetchRequest];
NSPredicate *predicate;
switch(self.segmentedControl.selectedSegmentIndex) {
case 0:
predicate = [NSPredicate predicateWithFormat:#"status = %#", #"incomplete"];
break;
case 1:
predicate = [NSPredicate predicateWithFormat:#"status = %#", #"complete"];
break;
case 2:
predicate = [NSPredicate predicateWithFormat:#"status = %#", #"deleted"];
break;
default:
predicate = [NSPredicate predicateWithFormat:#"status = %#", #"none"];
break;
}
[aRequest setPredicate:predicate];
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.tableView reloadData];
// self.topScrollView.delegate = self;
// [self.topScrollView setContentSize:CGSizeMake(640.0f, self.topScrollView.frame.size.height)];
// [self.topScrollView setContentOffset:CGPointMake(0, 0)];
// [self.topScrollView setScrollEnabled:YES];
}