Lazy loading technique for UICollectionView - objective-c

I have an IOS application, which uses a UICollectionView for displaying a horizontal grid of cells. All i need, is a clever way of (lazy) loading data when the user reaches the end of my UICollectionView. The code i use now is :
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
GridCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:Cell forIndexPath:indexPath];
/**
TODO
- Find a good way to check if the user has reach the end of the grid
- This next piece of code is for demonstration purposes. A real check has to be
done yet!!!
**/
// releases is my datasource //
if(indexPath.row == (releases.count - 1)) {
[self getNextListData];
}
return cell; // <-- finally return the cell
}
The problem with this code is : if a user scrolls really fast to the end, he has to scroll back and forth to load more items. This is really not user friendly and i need a better way to load items when needed.
Does anyone have any suggestions?
Thanks in advance.

UICollectionView inherits from UIScrollView. UIScrollView has a delegate, UIScrollViewDelegate that has a bunch of methods related to scrolling. In one of these delegate methods (such as scrollViewDidBeginDecelerating:), you can check to see how far along the page you are, with the contentOffset and contentSize properties. If you're close to the end (defined by whatever % is the end for you), fetch more data.
An advantage to using a scrollview method is that it will not cause collectionView:cellForItemAtIndexPath: to be slower (by forcing it to run code for every collectionview unnecessarily).
This is basically the same technique that Apple suggests for lazy-loading of UITableView data's, applied slightly differently. They have some sample code for that as well (such as LazyTableImages)

When I ran into this problem with table views, I had to do some user testing. I found that if I started my load 10 cells before the end of the table, the new cell should be ready. Assuming that you are doing background loading of the data, that is.
if (indexPath.row >= [releases count] - 10)
[self getNextListData];
You would want to do some testing to see what number works for you.
In addition, I also had a loading indicator cell. In my number of rows in section callback, I always returned 1 extra.
return [releases count] + 1;
At the start of my cell for row callback I returned a special loading indicator cell.
if (indexPath.row == [releases count])
return [JLTActivityCell sharedInstance];
I'm not sure exactly how that advice will apply to collection views, but hopefully it gets you pointed in a good direction.

Related

UITableViewCell as Header or Footer View

I know about UITableView reusable header and footer view
but in my case, i have UITableView Cells, which i need to place also in section headers and also in normal rows
if i use
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
MyCell * cell = [tableView dequeueReusableCellWithIdentifier:#"MyCell"];
// ...
return cell;
}
How does it work out with the reusing? (is the message to be available for reuse even than passed), or does this disable the cell reuse
The cells get dealloc'ed when they go off-screen. So they don't get reused. An easy way to verify this is to subclass UITableViewCell with the following
- (void)dealloc
{
NSLog(#"I got dealloc'ed");
}
and observe the console output as you scroll.
These has always worked fine. You first should create a prototype with that name, or register a custom nib with your custom section identifier. HOWEVER , I noticed this breaks in iOS 7 when you add new sections to the table dynamically. Reverting to a plain non-reusing UIView works. Really a shame!

How to scroll two UITableView toghether vertically

I have two UITableView. I need to scrolling toghether vertically.
I tried to insert both tables inside a UIScrollView and disabling scrolling of UITableView.
It works but the UITableView which is below is cut it seems as if the table size was set to the maximum size of the screen).
The UITableView i put them inside the view through the storyboard.
I tried to increase the frame of the table within the viewDidLoad method but does not work. If I insert it inside the method viewDidAppear works but it makes me a failure when I click on a UICell and then came back.
Any suggest?
you should work with sections. But if this is not an option for what ever reason you could listen to the scrollViewDidScroll: delegate method and set the offset of the two tables as appropriate. Small Example:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.firstTable) {
self.secondTable.contentOffset = scrollView.contentOffset;
} else {
self.firstTable.contentOffset = scrollView.contentOffset
}
}
you might need to store the touched table in an ivar to determine which scrollevents are initialized by user-interaction and which by your code to avoid 'scroll'-loops.
EDIT:
I might misunderstood your question. You want them to behave as one Table?

Why does an empty tableView check the number of sections but a non-empty one does not?

I have set up a demo application with a simple UITableViewController with no contents, but an 'Add' button in the toolbar. This launches a modal view controller which is again empty other than a 'cancel' button. The cancel button just tells its delegate (the UITableViewController) to dismiss the modal.
I then added an NSLog statement in the UITableViewController's numberOfSectionsInTableView method.
Ordinarily, when the table view controller loads I see two calls to numberOfSectionsInTableView. When I open and dismiss the modal (which returns to the UITableViewController) I see no further calls to numberOfSectionsInTableView.
However, if I return 0 from numberOfSectionsInTableView, in addition to the two calls on display, I also see an additional numberOfSections call when the modal is dismissed.
This only happens when numberOfSectionsInTableView returns 0, and I have added no additional code to my project besides that mentioned. This is easily verifiable by setting up a couple of controllers as I've described and modifying the result from numberOfSectionsInTableView.
My questions:
Why is the UITableView calling numberOfSectionsInTableView on return from a modal view?
Why is it only doing this if numberOfSectionsInTableView returns 0?
In addition to numberOfSectionsInTableView, the UITableViewController is also calling cellForRowAtIndex: when the modal is dismissed. In fact, it is attempting to display the new contents of its dataSource. How am I meant to manually animate a row insertion if the first row added is going to already be updated automatically? Shouldn't it be left to me to make sure that my UITableView is consistent with its dataSource?
What property is the UITableViewController checking to know that there is one or more sections (and therefore ask my delegate how many sections)? It can't be numberOfSectionsInTableView itself, since I would see it called whenever I return from the modal, not only when numberOfSections = 0.
From UITableViewController docs:
When the table view is about to appear the first time it’s loaded, the
table-view controller reloads the table view’s data... The
UITableViewController class implements this in the superclass method
viewWillAppear:
If you watch in the debugger, the second call upon app launch is from UITableViewController's viewWillAppear: implementation - specifically the part referred to above, where tableView is sent the reloadData message.
Now, the first call to numberOfSectionsInTableView: on launch is also from UITableViewController's implementation of viewWillAppear: but not directly from that implementation's call to -[UITableView reloadData]. I'm not sure what the first call is all about.
But, to your question, the call to numberOfSectionsInTableView: that happens when dismissing the modal has exactly the same call stack as the second call from applicationDidFinishLaunching:withOptions:. My hypothesis then is that UITableView interprets having zero sections as being in a state where it has not loaded at all. That does make some sense actually. I'd consider an "empty" table view to be one without any rows, but one without any sections seems almost "uninitialized" to me. Furthermore the UITableViewDataSource documentation implies UITableView has by default one section. Returning zero from this method would be inconsistent with that assumption of the docs as well.
Now, to your concern about animation - if you give the table an empty section to work with, you will be able to have full control over inserting the first row with whatever animation you'd like, and not be locked in to when you need to reload.
I think the moral of the story is, don't return zero sections unless you really, really need to for some reason. The title of your post refers to this table view being "empty" as well but I think it's clear the framework finds zero sections to not be empty but unloaded.
Hope this helps! And thanks for posting the sample project for me to play around with.
Perhaps the delegate just couldn't believe its eyes. But seriously, since a table view has to have at least one section, passing 0 doesn't make any sense. Why do it? You pass it an invalid argument and it gives you back a weird response. As to why it doesn't ask for number of sections when you pass 1, I think it's because it doesn't need to know at that point (coming back from the modal view controller) -- the table view has already been populated (if there were some data) and you haven't changed anything in the model, so it doesn't need to update.
I added a few lines to your example project to slide in a row each time you return from the modal view controller, which is what I think you're trying to do. I added an int property num for the return value of numberOfRowsInSection, added an array to populate the table, and a call to insertRowsAtIndexPaths from the modal view controller dismissal method.
- (void)viewDidLoad
{
_num = 0;
self.theData = #[#"one",#"two",#"three"];
[super viewDidLoad];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self
action:#selector(addRecipe)];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSLog(#"# sections requested");
//when 0, this fires on return from the modal. When 1, it does not.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"in numberOfRows in section");
return _num;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"in cellForRowAtIndexPath");
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:nil];
cell.textLabel.text = [self.theData objectAtIndex:indexPath.row];
return cell;
}
- (void)addRecipe
{
//create the modal and suscribe for delegate notifications
AddRecipeViewController *addRecipeController = [[AddRecipeViewController alloc]initWithStyle:UITableViewStyleGrouped];
addRecipeController.delegate = self;
//display the modal in a navigation controller
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:addRecipeController];
[self.navigationController presentModalViewController:navController animated:YES];
}
- (void)addRecipeVC:(AddRecipeViewController *)addRecipeVC didAddRecipe:(NSString *)recipe
{
[self dismissModalViewControllerAnimated:YES];
_num += 1;
[self performSelector:#selector(addRow) withObject:nil afterDelay:.5];
}
-(void)addRow {
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:_num-1 inSection:0]] withRowAnimation:UITableViewRowAnimationRight];
}
The tableview checks number of sections when it's populating the table view with data!
Since the table can be divided into sections, it has to know specifically how many sections to divide it into.
When you reload the data, the number of sections is also checked.
Because every time the table view has to take action in accessing either the data of the table, like what row you tapped, and in what section, or populating the data table, the number of sections has to be known!
Hope this helped!

What is the purpose of the delegate method 'canMoveRowAtIndexPath'?

I'm working on a UI component right now, and as it behaves similarly to UITableView, I'm heavily modeling the delegate and data source protocols after those of UITableView. However, I noticed one method that I don't quite understand- 'canMoveRowAtIndexPath'.
This essentially allows the delegate to specify whether it wants the given cell to be 'movable'. However, wouldn't dropping another movable cell into a higher index than the immovable cell (i.e. 'above' it in the table) cause it to indirectly move anyway? (since every cell below the moved one would be pushed down one row).
So basically, my question is what is the point of this method? Can anyone provide an example use-case for it? Because I'm debating whether I should bother including it in my component or not.
If anything, I would think perhaps a more useful delegate method would be something such as 'canMoveRowInSection', which would allow you to specify whether any rows in a given section can be moved. That would then allow you to disable reordering of a particular section, and moving other rows outside of that section would not affect the ordering of the rows inside it.
I know Apple engineers provided this method for a reason, I just can't see what that reason might be.
Thanks for any insight!
canMoveRowAtIndexPath tells the UITableView that if the table is in editing mode, the cells (or cell, if you choose specifically) can be moved up and down.
It's up to you the developer to handle the other side of that move.
For instance, say you have an array (NSMutableArray to be exact) of "A", "B", "C" and you want to rearrange that array to be "B", "C", "A". You need to make that change in the array based on the location of the cell being moved and save that array.
Example
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
id objectToMove = [[array objectAtIndex:fromIndexPath.row] retain];
[array removeObjectAtIndex:fromIndexPath.row];
[array insertObject:objectToMove atIndex:toIndexPath.row];
[objectToMove release];
}
Section Example
This example says that if the table section is 0, then no cells can move. Any other section (say you have 3), those cells in section 1 and 2 CAN move. You will still need to handle the array accordingly.
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
if ( indexPath.section == 0 )
return NO;
return YES;
}

UITableView: on row selection load a second UITableView with related data

I have two UITableViews on one view and would like to implement the following logic:
- when a row is selected on tableView1 populate tableView2 with related information
I have set both UITableViews delegate and datasource, and am processing the initial population of tableView1 and didSelectRowAtIndexPath on tableView1. I am also checking to make sure the right UITableView is being handled i.e. tableView == MyTableViewVariable
Within handling didSelectRowAtIndexPath for tableView1 I finish off by calling [tableView2 reloadData] expecting that this would initiate the chain of method calls to force tableView2 to load up. But calling that method doesn't do anything.
A couple of posts indicate to me that people have got this working by putting a delay in their code, or forcing the call to be made from the main thread.
Can anyone point me to the best way to do this?
Cheers, James
use tags. But Its easier if you have two separate view controllers.
UITableView *a, *b;
a.tag = 0;
b.tag = 1;
- (void)tableView:(UITableView *)tv didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if(tv.tag==0 && indexpath.row==0)
b.frame = CGRectMake(0,0,320,480);
[self.view addSubView:b];
[b reloadData];
}