I struggle with tableview datasource and delegate methods. I've a tableview if user navigate to that table view controller I'm calling web service method which is block based service after the request finished successfully reloaded tableview sections but tableView:cellForRowAtIndexPath:indexPath called twice. Here's my code.
viewDidLoad
[[NetworkManager sharedInstance].webService getValues:self.currentId completion:^(NSArray *result, BOOL handleError, NSError *error) {
self.data = result[0];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 4)] withRowAnimation:UITableViewRowAnimationAutomatic];
});
}
but cellForRowAtIndexPath self.data value is null in the first time.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"%#", self.data); // first time it print null
}
So is there any ideas about this? Thank you so much!
Did you initialise the data array in viewDidLoad? If you didn't it will return null.
if you want to avoid two calls to the tableview try this:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if(!data)
return 0;
return yourNumberOfSections;
}
The tableview calls cellForRowAtIndexPath when it needs to render a cell on screen. If the tableview appears before it has any data, self.data will be empty.
In viewDidLoad set [[self.data = NSMutableArray alloc] init] (for example) and in the datasource/delegate methods of UIITableView it should correctly return numberOfRows etc as zero until your web service populates your data.
It sounds like -tableView:cellForRowAtIndexPath: is being called simply because the view is loaded and the table is trying to populate itself before you've received any data from your web service. At that point, you won't have set self.data (whatever that is) to any useful value, so you get null instead. When your web service returns some data, the completion routine causes the relevant sections to be reloaded, and the table draws the data.
Related
I'm using a UICollectionView to display some data received from a remote server and I'm making two reloadData calls in a short time, first one to show a spinner (which is actually a UIActivityIndicatorView in a UICollectionViewCell) and the second one after the data was retrieved from server. I store the models based on which the collection view cells are created in a self.models NSArray and in cellForItemAtIndexPath I dequeue cells based on this model. My issue is that when I call reloadData after the request was completed (the second time) the collection view seems to be still in the process of creating the cells for the first reloadData call and it shows just the first two cells and a big blank space in place where the rest of the cells should appear.
I wrote some logs to get some insights of what's happening and the current workflow is:
I'm populating self.models with the model for the spinner cell and then I call reloadData on the UICollectionView.
numberOfItemsInSection is called, which is just returning [self.models count]. The returned value is 2, which is fine (one cell which acts like a header + the second cell which is the one with the UIActivityIndicator inside).
I'm making the request to the server, I get the response and I populate self.models with the new models received from the remote server (removing the model for the spinner cell but keeping the header cell). I call reloadData on the UICollectionView instance.
numberOfItemsInSection is called, which is now returning the number of items retrieved from the remote server + the model for the header cell (let's say that the returned value is 21).
It's just now when cellForItemAtIndexPath is called but just twice (which is the value returned by numberOfItemsInSection when it was called first).
It seems like the collection view is busy with the first reloadData when I call this method for the second time. How should I tell the collection view to stop loading cells for the first reloadData call? I tried with [self.collectionView.collectionViewLayout invalidateLayout] which seemed to work but the issue reappeared, so it didn't did the trick.
Some snippets from my code:
- (void)viewDidLoad {
[super viewDidLoad];
[ ...... ]
self.models = #[];
self.newsModels = [NSMutableArray array];
[self.collectionView.collectionViewLayout invalidateLayout];
[self buildModel:YES]; // to show the loading indicator
[self.collectionView reloadData];
[self updateNewsWithBlock:^{
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView.collectionViewLayout invalidateLayout];
[self buildModel:NO];
[self.collectionView reloadData];
});
}];
}
- (void) buildModel:(BOOL)showSpinnerCell {
NSMutableArray *newModels = [NSMutableArray array];
[newModels addObject:self.showModel]; // self.showModel is the model for the cell which acts as a header
if ([self.newsModels count] != 0) {
// self.newsModels is populated in [self updateNewsWithBlock], see below
[newModels addObjectsFromArray:self.newsModels];
} else if (showSpinnerCell) {
[newModels addObject:[SpinnerCellModel new]];
}
self.models = [NSArray arrayWithArray:newModels];
}
- (void) updateNewsWithBlock:(void (^)())block {
// Here I'm performing a GET request using `AFHTTPSessionManager` to retrieve
// some XML data from a backend, then I'm processing it and
// I'm instantiating some NewsCellModel objects which represents the models.
[ ..... ]
for (NSDictionary *item in (NSArray*)responseObject) {
NewsCellModel *model = [[NewsCellModel alloc] init];
model.itemId = [item[#"id"] integerValue];
model.title = item[#"title"];
model.headline = item[#"short_text"];
model.content = item[#"text"];
[self.newsModels addObject:model];
}
block();
}
I am populating a tableview with the contents of an array. Right now, I create the array in viewdidload and I calculate the number of rows in the delegate method
//in viewdidload
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL: kItemsURL];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:data waitUntilDone:YES];
});
[self.tableView reloadData];
//method called in viewdidload to create array...
- (void)fetchedData:(NSData *)responseData {
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseData //1
options:kNilOptions
error:&error];
NSLog(#" %#",json);
NSArray* latestItems = [json objectForKey:#"items"];
NSLog(#" array:%#",latestItems);
//getItems is a property in .h file
self.getItems = latestItems;
NSLog(#"getItems %#",_getItems); //logs out array ok
int size = [_getItems count];
NSLog(#"there are %d objects in the array", size);//provides correct number
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSLog(#"getItems %#",_getItems); //logs (null)
int size = [_getItems count];
NSLog(#" %d objects in the array", size);//logs 0
When I count rows in viewdidload after creating the array, I get the correct number, however, when I call count on the array in the delegate method, it returns zero possibly because the tableview is created before Viewdidload is called.
Where should I create the array so that is known by the time numberofrows counts the number of rows in the array?
Edit:
After constructing the array, I save it to a property. However, I have discovered that this property is empty when I then log it to console in the numberofrowsinsection method so the problem seems to lie in how I am storing this array.
Right now, I have a property in the .h file and I've also tried it in the implementation but either way it is not persisting for some reason.
I'm not to familiar with obj-C, but I know you need to initialize your array outside your viewDidLoad() function. The reason why your .count is returning zero, is because your array is acting as a local variable to your viewDidLoad() function. Instead you could initialize the array as field in your UITableViewController class. This is how you would do so in swift, but it applies to obj-C as well:
class YourTableViewController: UITableViewController {
var yourArray = [AnyObject]()
override func viewDidLoad() {
//You can still do any programming to set up values and elements in yourArray[] here
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return yourArray.count
}
//Plus all your other tableView functions...
}
Also if you are passing information to your array between other UIViewController's you can add this function to your class, so every time you come back to your table view it loads the correct table cell count:
override func viewWillAppear(animated: Bool) {
self.tableView.reloadData()
}
I suggest to load the content of your array in viewDidLoad(), that is called once and before the table view use the array. The table view do not load the items before viewDidLoad. Are you doing something much different than this example structure below?
#implementation ViewController {
NSArray *arrayList;
}
- (void)viewDidLoad {
[super viewDidLoad];
arrayList = #[#"item 1", #"item 2"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
cell.textLabel.text = [arrayList objectAtIndex:indexPath.row];
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return arrayList.count;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
Consider abstracting the creation of your data - the array instantiated in your tableview, into a model instead. This is generally considered to be a better software engineering practice (read : https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html ).
What I would do is have another file as a Class or Struct, and populate the data for the array used in your table view in there. I would also recommend having setter/getter methods in your array class/struct as well. When loading the tableview, we would then grab the data from the class/struct in your tableview viewDidLoad() method. The data from your array would also be available at any other point of your application as well, as it is no longer dependant on the tableview.
Side note : You can consider making the class/struct a singleton as well, if the model is supposed to only get instantiated once.
In the .m file ClassroomCollectionViewController, I have the following instance variable declared:
#implementation ClassroomCollectionViewController
{
NSMutableArray *students;
}
This array is populated in the following delegate method of the NSURLConnectionDataDelegate protocol, which ClassroomCollectionViewController implements.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if (connection == _getStudentsEnrolledInClassConnection)
{
// Parse the JSON that came in
NSError *error;
NSArray *jsonArray = [NSJSONSerialization JSONObjectWithData:_receivedData options:NSJSONReadingAllowFragments error:&error];
if (jsonArray.count > 0)
{
students = [[NSMutableArray alloc] init];
// Populate the students array
for (int i = 0; i < jsonArray.count; i++)
{
Student *studentInClass = [Student new];
studentInClass.name = jsonArray[i][#"name"];
studentInClass.profile = jsonArray[i][#"profile"];
studentInClass.profileImageName = jsonArray[i][#"profile_image_name"];
[students addObject:studentInClass];
}
}
}
}
In the following delegate method of another protocol, namely the UICollectionViewDelegate, the students array populated above is used to construct the individual cells of the collection view.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
ClassmateCollectionViewCell *myCell = [collectionView
dequeueReusableCellWithReuseIdentifier:#"ClassmateCell"
forIndexPath:indexPath];
UIImage *image;
long row = [indexPath row];
image = [UIImage imageNamed:[students[row] profileImageName]];
myCell.imageView.image = image;
myCell.classmateNameLabel.text = [students[row] name];
return myCell;
}
The problem is that the students array is not yet populated by the time that the second of the two delegate methods above executes, which results in there being no data for the cells in the collection view to display.
The obvious solution to this problem is to delay the execution of the second method until the first one has finished executing (thus ensuring that the students array will be populated by the time the cells in the collection view are constructed). But I couldn't for the life of me figure out how to make this so in this particular context - since I have no control over when the second delegate method is invoked. I have considered using blocks and multithreading in order to solve this, but have failed at coming up with a solution that is relevant for this specific problem.
Could anyone point me in the right direction?
Thanks very much,
Jay
Try this, connect an IBOutlet to collection view and
in your connectionDidFinishLoading: method
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//All other codes for populating `students` array
[self.collectionView reloadData];
}
To delay execution of the collection view, you need to wait until the connectionDidFinishLoading calls.
Its pretty simple you can reload collection view from connectionDidFinishLoading method.
Thats it.
This is design problem. It suppose to go like this (MVC patern):
some service is receiving data by NSURLConnectionDataDelegate sends results to model.
model enforces main thread, updates its data and posts some notification
view controller is registered for model notification and properly reacts on those notifications. Like [self.collectionView reloadData]; or [self.collectionView insertItemsAtIndexPaths: indexPaths]; and so on.
I'm using a webservice to fill an array of items, which then are used to fill cells of my table view.
Now i'm having an issue with the table view itself. When I check the " numberofrows" method, the array isn't loaded yet. For some reason it loads "right after" that (according to my nslogs & breakpoints), even though i've put every loading method as early as i could in that controller.
Now what i don't know is :
is there a way to delay the table view creation so i'm sure my array is loaded?
is there a way to load my array earlier? It's currently my first line in the viewDidLoad. (calling another class, which then calls the webservice and returns an array, but by then, the table view is already loaded and empty).
What i've tried : Putting a tableview reloadData. But it simply doesn't work. For some reason the compiler reads the line but doesn't load anything new, even though at that point the array is full.
Am i missing something?
(My tableview works just fine if I put hardcoded objects in my array)
I can edit and add some code that you guys would request, but since this looks like a school problem here, maybe i just forgot something.
Guys, i'm all ears !
Edit :
My different methods ; i've removed unecessary code for clearer reading.
The compiler NEVER goes in the cellForRow because numberOfRows is returning a zero number.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"CustomCellTableViewCell";
CustomCellTableViewCell *cell = [self.tbUpcomingEvents dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
cell.lbTitle = [_eventList objectAtIndex:indexPath.row];
return cell;
}
number of rows ; the nslog returns zero.
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"numOfRows : %i", [_eventList count]);
return [_eventList count];
}
my webservice method, this is the method calle from the "webservice class" to load the data.
Here, the NSLog shows a full array.
- (void)loadData:(NSMutableArray*)arrayEvent
{
//arrayEvent is full from the internet data. eventList is also full on the NSLog.
_eventList = arrayEvent;
NSLog(#"LoadData : %i", [_eventList count]);
[self.tbUpcomingEvents reloadData];
}
my viewdidLoad method
- (void)viewDidLoad
{
[super viewDidLoad];
//Calling my UseCaseController to load the data from the internet.
UCC *sharedUCC = [UCC sharedUCC];
[sharedUCC getUserEventsInDbDis:self :_user];
}
As you said you are using webservices. So it is executing your code in the block which means it is running in separate thread. Now as per apple documentation UI updates should happen in main thread. So for table creation you need to include the same in below GCD code:-
dispatch_async(dispatch_get_main_queue,()^{
// write your code for table creation here
});
The first time the view controller is pushed (from the previous view controller) all the delegate methods are called (inside a navigation controller).
When pressing back to return to the previous view controller , and then pushing it again (for the second time)
cellForRowAtIndexPath isn't called but numberOfRowsInSection and numberOfSectionsInTableView are called.
The reloadData is called within
-(void)viewWillAppear:(BOOL)animated
{
[self.tableView reloadData];
}
and I have tried in
-(void)viewDidAppear:(BOOL)animated
{
[self.tableView reloadData];
}
and it doesn't help.
Edit
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 1; // called
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 3; // called
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// relevant code for cell - THIS METHOD IS NOT CALLED SECOND TIME
}
If you don't find any valid reason as explained above I will highlight
another mistake one can make is (as I once did).
The steps to note are
1) initialise your custom table view as first step
2) set the delegate and datasource before you add the tableView to you view.
If one has done these steps mistakenly after adding to the tableview to your self.view object, then it would be a silent way to make yourself struggle.Correct order is as under:
self.myTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, myViewwidth, 120) style:UITableViewStylePlain];
self.myTableView.tag = MY_TABLEVIEW_TAG;
self.myTableView.delegate = self;
self.myTableView.dataSource = self;
[self.view addSubview:self.myTableView];
This always happens when you define your underlying data source (array or Dictionary) as weak, the first time it gets pushed, the data is there, when deallocated, it will release and you lose control over it.
Double check the weak/strong condition and optionally set the data source before pushing again.
Please check if you have set/connected Delegate and Datasource to the File's Owner.
And check the array count of your model, if it contains value of not?
NSLog in the numberOfRowsInSection method and check it by using breakpoints and step over.