Im using UICollectionView to get a set of images from instagram however i want to load past images when i get to the bottom of the page. I do however recieve the past images because they are logged into the console but dont appear on the screen.
Here is my code to retrieve the images:
- (void)nextInstagramPage:(NSIndexPath *)indexPath{
NSDictionary *page = self.timelineResponse[#"pagination"];
NSString *nextPage = page[#"next_url"];
[[InstagramClient sharedClient] getPath:[NSString stringWithFormat:#"%#",nextPage] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
self.timelineResponse = responseObject;
[self.collectionView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failure: %#", error);
}];
}
Here is my code to detect when the user gets to the bottom of the screen:
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath{
[self nextInstagramPage:indexPath];
}
Edit: I have found out that the collectionview is a subclass of uiscrollview so how would i correctly implement a method which has an indexpath to detect the bottom of the scrollview!
With in your code i am making 2 assumptions, that you have an array of data to run parallel with your ui collection view and the delegate/datasource methods. you would notice at least these two methods in your code.
– collectionView:numberOfItemsInSection:
– collectionView:cellForItemAtIndexPath:
the first one tells you how many items/cells/squares you see in your ui collection view, the second makes you create what the item/cell/square will look like.
In the first method you told the uicolelction view how many there were, with in the cellforitem you should test "is this the last image" if it is the last image then download more images, if it is not the last image then continue as if nothing happened
Edit:
[[self entries] count]
you had that, now with in cell for row at index path do
if ( indexpath.item == ([[self entries] count] - 1) ) {
// download more data and add it to the 'entries' array
}
Related
I would like to put up a UICollectionView of pictures downloaded from a backend provider, and am running the issue where every time my collection view controller is initialized, the required method
- (NSInteger) collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
always returns 0, despite my providing the code [self.profilePicturesStorage count]. I understand that because I am using a block to populate the self.profilePicturesStorage property, at the view initialization it will always return 0 (since the block hasn't finished executing) My question is, how do I update CollectionView's numberOfItemsInSection: method after my block has finished downloading all the pictures?
This is the block that I execute in viewDidLoad:
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error){
if ([objects count] > 0){
//... Skip additional code that parse out the downloaded objects
[self.profilePicturesStorage addObject:userPicture];
}
}
else {
NSLog(#"There aren't any pictures for the current user");
}
}
}];
Thanks!
[collectionView reloadData] should be all you need.
I have a UITableViewController.
I want to call a URL (http://webservices.company.nl/api?station=ut) multiple times (for each train station) where "ut" is always different (it's the code of the station). And I want to put the results each time in a new tableview row. (The URL returns XML).
To call the URL, I use this:
// Create connection
NSURLConnection *urlConnection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat: #"http://webservices.company.nl/api?station=%#", station.stationCode]]] delegate:self];
[urlConnection start];
Then in "connectionDidFinishLoading" I've this for parsing the URL content with NSXMLParser:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:receivedDataFromURL];
[parser setDelegate:self];
[parser parse];
}
I've implemented all the methods like "didStartElement", "didEndElement" and it successfully reads all the elements in the file.
My question:
What's the best way to do this for every row in my tableview and how can I put the results in every row?
I don't know what the best structure is for this, because I want to do this async.
Many thanks in advance.
The pattern here is just like lazy loading images.
1) Create a custom object like TrainStation, it should have an NSString station code, some BOOL property of function that tells callers that it's been initialized from the web service, and an init method that provides a block completion handler.
// TrainStation.h
#interface TrainStation : NSObject
#property (strong, nonatomic) NSString *stationCode; // your two character codes
#property (strong, nonatomic) id stationInfo; // stuff you get from the web service
#property (strong, nonatomic) BOOL hasBeenUpdated;
#property (copy, nonatomic) void (^completion)(BOOL);
- (void)updateWithCompletion:(void (^)(BOOL))completion;
#end
2) The completion handler starts an NSURLConnection, saving the completion block for later when the parse is done...
// TrainStation.m
- (void)updateWithCompletion:(void (^)(BOOL))completion {
self.completion = completion;
NSURL *url = // form this url using self.stationCode
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
[NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
}];
}
// TrainStation does it's own parsing, then
- (void)parserDidEndDocument:(NSXMLParser *)parser
self.hasBeenUpdated = YES;
self.completion(YES);
// when you hold a block, nil it when you're through with it
self.completion = nil;
}
3) The view controller containing the table needs to be aware that tableview cells come and go as they please, depending on scrolling, so the only safe place for the web result is the model (the array of TrainStations)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// normal stuff, dequeue cell, etc.
// the interesting part
TrainStation *trainStation = self.array[indexPath.row];
if ([trainStation hasBeenUpdated]) {
cell.detailTextLabel.text = [trainStation.stationInfo description];
// that's just a shortcut. teach your train station how to produce text about itself
} else { // we don't have station info yet, but we need to return from this method right away
cell.detailTextLabel.text = #"";
[trainStation updateWithCompletion:^(id parse, NSError *) {
// this runs later, after the update is finished. the block retains the indexPath from the original call
if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) {
// this method will run again, but now trigger the hasBeenUpdated branch of the conditional
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
}
}];
}
return cell;
}
There are a few considerations:
You probably want to make each of these requests its own object so that you can have them running concurrently. The right approach is probably a custom operation for a NSOperationQueue to encapsulate the downloading and parsing of the XML. A couple of considerations here:
You should make this operation so it can operate concurrently.
You should make the operation respond to cancellation events.
Note, if you do your own NSOperation with a NSURLConnection with your own NSURLConnectionDataDelegate methods, you have to do some silliness with scheduling it in an appropriate run loop. I usually create a separate thread with its own runloop, but I see lots of people simply doing:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
You probably want to implement a caching mechanism:
At a minimum, you want to cache responses into memory (e.g. a NSCache) so that if you scroll down and then scroll back up, it doesn't need to reissue requests that it only just sent;
Depending upon the needs of your app, you might want a persistent storage cache, too. Maybe you don't in this particular situation, but it's a common consideration in these sorts of cases.
Given the network intense nature of your problem, I'd make sure you test your app in network realistic, real-world (and worst case) scenarios. On the simulator, you can achieve that with the "Network Link Conditioner" which is part of the "Hardware IO Tools" (available from the "Xcode" menu, choose "Open Developer Tool" - "More Developer Tools"). If you install the "Network Link Conditioner", you can then have your simulator simulate a variety of network experiences (e.g. Good 3G connection, Poor Edge connection, etc.).
Anyway, putting the this together, here is an example that is doing a XML request for every row (in this case, looking up the temperature for a city on Yahoo's weather service).
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
CityCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// try to retrieve the cell from the cache
NSString *key = self.objects[indexPath.row];
City *cityFromCache = [self.cache objectForKey:key];
if (cityFromCache)
{
// if successful, use the data from the cache
cell.textLabel.text = cityFromCache.temperature;
cell.detailTextLabel.text = cityFromCache.name;
}
else
{
// if we have a prior operation going for this cell (i.e. for a row that has
// since scrolled off the screen), cancel it so the display of the current row
// is not delayed waiting for data for rows that are no longer visible;
// obviously, for this to work, you need a `weak` property for the operation
// in your `UITableViewCell` subclass
[cell.operation cancel];
// re-initialize the cell (so we don't see old data from dequeued cell while retrieving new data)
cell.textLabel.text = nil;
cell.detailTextLabel.text = nil;
// initiate a network request for the new data; when it comes in, update the cell
CityOperation *operation = [[CityOperation alloc] initWithWoeid:key successBlock:^(City *city) {
// see if the cell is still visible
UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
// if the cell for this row is still visible, update it
if (updateCell)
{
updateCell.textLabel.text = city.temperature;
updateCell.detailTextLabel.text = city.name;
[updateCell setNeedsLayout];
}
// let's save the data in our cache, too
[self.cache setObject:city forKey:key];
}];
// in our custom cell subclass, I'll keep a weak reference to this operation so
// we can cancel it if I need to
cell.operation = operation;
// initiate the request
[self.queue addOperation:operation];
}
return cell;
}
In practice might move some of that logic into my cell subclass, but hopefully this illustrates the idea.
Having outlined an answer to your question, I must confess that when you described what you're trying to do, I immediately gravitated to radically different designs. E.g. I might kick off an asynchronous process that does a bunch of XML requests, updating a database, posting notification to my table view letting it know when data has been inserted. But this is a more radical departure from what you've asked, so I refrained. But it might be worthwhile to step back and consider the overall architecture.
I am making a simple reddit app for a school project. I am loading my
data from reddit via json (http://www.reddit.com/.json for example) with AFNetworking library.
I am displaying each reddit thread with a prototype cell, which
contains a UIImageView for the post thumbnails.
I am trying to use AFNetworking to lazy load the images, with the
setImageWithURLRequest method.
the problem: when the app launches all the thumbnails load lazily as they should as I scroll down the tableView. As soon as the cell is out of the view and I scroll back up to it, the thumbnail has been replaced with the placeholder image -- even though it loaded the correct thumbnail before scrolling past.
Relevant code from cellForRowAtIndexPath method. the lazy loading is being called in the setImageWithURLRequest block
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"threadCell";
SubredditThreadCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSDictionary *tempDictionary = [self.subredditArrayFromAFNetworking objectAtIndex:indexPath.row];
NSDictionary *singleThreadDict = [tempDictionary objectForKey:#"data"];
if ( [[singleThreadDict objectForKey:#"thumbnail"] isEqualToString:#"nsfw"] ){
cell.postImageThumbnail.image = [UIImage imageNamed:#"NSFWIcon"];
}
else if ([[singleThreadDict objectForKey:#"thumbnail"] isEqualToString:#"self"]){
cell.postImageThumbnail.image = [UIImage imageNamed:#"selfIcon"];
}
else if ([[singleThreadDict objectForKey:#"thumbnail"] length] == 0){
cell.postImageThumbnail.image = [UIImage imageNamed:#"genericIcon"];
}
else{
NSURL *thumbURL = [NSURL URLWithString:[singleThreadDict objectForKey:#"thumbnail"] ];
[cell.postImageThumbnail setImageWithURLRequest:[NSURLRequest requestWithURL:thumbURL]
placeholderImage:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
if (request) {
[UIView transitionWithView:cell.postImageThumbnail
duration:1.0f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
[cell.postImageThumbnail setImage:image];
}
completion:NULL];
}
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error)
{
NSLog(#"failure loading thumbnail");
}
];
}
return cell;
}
Don't directly update the cell's postImageThumbnail when the image is done downloading.
Instead, tell the UITableView to refresh just that cell, using – reloadRowsAtIndexPaths:withRowAnimation:.
To get better performance, UITableView will re-use a cell-object once it's been scrolled offscreen, to show different data that is currently visible. (This is why dequeueReusableCellWithIdentifier: starts with "dequeueReusable", instead of "makeNew".) This lets UITableView only create about as many cells as are visible, instead of having to create and destroy as many cells as there are rows in the table. By the time your networking request succeeds, the cell that the success: block captures is being used to display another row, and you're over-writing that row's image.
I have to fetch contacts from the Address Book and show photo beside each if found in a UITableView.
I fetch all contacts using ABContactsHelper library and then asynchronously fetch photos for visible rows in the UITableView using GCD blocks.
I referred to an Apple Sample code which waits for the UITableView to finish scrolling, get Visible NSIndexPaths & created threads to fetch photos. My problem so far is two fold.
First, if user scrolls, stops, scrolls & stops and does it quite a few times, too many threads are generated for fetching photos which slows down the app.
Secondly, when the thread returns to set photo in cache as well as the UITableViewCell however, the reference to UIImageView is now being reused for another record in UITableViewCell, hence the photo is placed on wrong record which eventually gets replace by correct one, when thread for that particular record returns.
Here is the code I is used both in cellForRowAtIndexPath as well as when UITableView stops scrolling.
- (void)loadImagesLazilyForIndexPath:(NSIndexPath *)indexPath photo:(UIImageView *)photo contact:(ContactModel *)contact
{
if (!self.tableView.isDragging && !self.tableView.isDecelerating) {
UIImage *thePhoto = [self.imagesForContacts objectForKey:indexPath];
if (!thePhoto) {
// NSLog(#"Photo Not Found - Now Fetching %#", indexPath);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
#autoreleasepool {
UIImage *image = [[JMContactsManager sharedContactsManager] photoForContact:contact];
if (!image)
image = self.noProfilePhoto;
[self.imagesForContacts setObject:image forKey:indexPath];
dispatch_async(dispatch_get_main_queue(), ^{
// NSLog(#"Photo Fetched %#", indexPath);
#autoreleasepool {
NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows];
BOOL visible = [visiblePaths indexOfObjectPassingTest:^BOOL(NSIndexPath * ip, NSUInteger idx, BOOL *stop) {
if (ip.row == indexPath.row && ip.section == indexPath.section) {
*stop = YES;
return 1;
}
return 0;
}];
if (visible)
photo.image = [self.imagesForContacts objectForKey:indexPath];
[[NSURLCache sharedURLCache] removeAllCachedResponses];
}
});
}
});
} else {
// NSLog(#"Photo Was Found %#", indexPath);
#autoreleasepool {
photo.image = [self.imagesForContacts objectForKey:indexPath];
}
}
}
}
For this kind of functionality I would go with an NSOperation and an NSOperationQueue, they are build on top of GCD, but it gives you the opportunity to cancel operations. You could check which operation aren't visible anymore and cancel them. In thi s way you can control reference "away".
I see also another issue that could lead into a "problem" it seems that you are caching images in an NSMutableDictionary, aren't you? Or are you using an NSCache? If it is an NScache is fine, but most of mutable object aren't thread safe "naturally"
Boost up the priority of the queue :-)
As mentioned by #Andrea, you should be using an NSOperationQueue, which gives you the ability to cancel queued tasks.
Indexing your image cache by indexPath into your table is not robust as an index path for a given element could change (although maybe not in your specific case). You might consider indexing your image cache by ABRecord.uniqueId instead.
In any case it will not solve the problem of your images being set twice or more for the same cell. This happens because UITableView does not assign a view for each item but manages a pool of UITableCellViews, which it re-uses each time. What you could do is something along the following lines:
// Assuming your "ContactCellView" inherits from UITableCellView and has a contact property
// declared as follows: #property (retain) ABRecord *contact.
- (void) setContact:(ABRecord*)contact
{
_contact = contact;
__block UIImage *thePhoto = [self.imagesForContacts objectForKey:contact.uniqueId];
if (thePhoto == nil) {
_loadImageOp = [NSBlockOperation blockOperationWithBlock:^(void) {
// Keep a local reference to the contact because it might change on us at any time.
ABRecord *fetchContact = contact;
// Fetch the photo as you normally would
thePhoto = [[JMContactsManager sharedContactsManager] photoForContact:fetchContact];
if (thePhoto == nil)
thePhoto = self.noProfilePhoto;
// Only assign the photo if the contact has not changed in the mean time.
if (fetchContact == _contact)
_contactPhotoView.image = thePhoto;
}];
} else {
_contactPhotoView.image = thePhoto;
}
}
Hello I am using dropbox api and displaying meta data from dropbox account..
I want to differentiate files and folders from loaded data..because I want to show next level if there is folder and if there is file I don't want to show next View
my code to load data
- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata {
[self.metaArray release];
self.metaArray = [[NSMutableArray alloc]init ];
for (DBMetadata *child in metadata.contents) {
NSString *folderName = [[child.path pathComponents] lastObject];
[self.metaArray addObject:folderName];
}
[self.tableView reloadData];
[self.activityIndicator stopAnimating];
}
According to the Dropbox Developer Docs the metadata includes a property called is_dir which should allow you to determine whether the particular item is a directory or not.
Looking at the header of DBMetaData it is indeed exposed as a property
#property (nonatomic, readonly) BOOL isDirectory;
So you can just do a simple test like so
- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata
{
if (metadata.isDirectory) {
// handle directory here
} else {
// handle file here
}
}
With regards pushing views based on whether or not an entry is a directory, you could subclass UITableViewCell and add an isDirectory property. Instead of adding just the name to self.metaArray you could add a dictionary containing both the name and the value of isDirectory. Then in your table view datasource where you populate the cells you'd set the isDirectory property of the UITableViewCell based on the same property in the appropriate dictionary from the array. Finally, in the table view delegate method
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
you can get the selected cell using the indexPath and then test the isDirectory property and based on it's value take the appropriate action.
Hope this helps.
Using the API V2 of Dropbox with the Dropbox SDK is:
DropboxClient *client = [DropboxClientsManager authorizedClient];
[[client.filesRoutes listFolder:path]
response:^(DBFILESListFolderResult *result, DBFILESListFolderError *routeError, DBRequestError *error) {
if (result) {
for (DBFILESMetadata *entry in result.entries) {
if ([entry isKindOfClass:[DBFILESFileMetadata class]]) {
DBFILESFileMetadata *fileMetadata = (DBFILESFileMetadata *)entry;
NSLog(#"File: %#", fileMetadata.name);
} else if ([entry isKindOfClass:[DBFILESFolderMetadata class]]) {
DBFILESFolderMetadata *folderMetadata = (DBFILESFolderMetadata *)entry;
NSLog(#"Folder: %#", folderMetadata.name);
}
}
}