I am stuck on the PROPER way to get all a user's Facebook friends and load it into a table view (I would simply use FBFriendPickerViewController but its ugly and it doesn't appear that you can fix that)
All I want is the profile picture in the cell imageView and name in the cell textLabel, simple as that.
Doing it with NSURL/NSData doesn't crop the images, Id much rather use FBProfilePictureView so please try to use that in any solutions. Thanks!
Get data from Facebook
-(void)facebookOpenSession {
if (FBSession.activeSession.isOpen) {
[[FBRequest requestForMyFriends] startWithCompletionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
if (error) {
// handle error
} else {
self.facebookFriends = [NSArray arrayWithArray:[result objectForKey:#"data"]];
[[NSNotificationCenter defaultCenter] postNotificationName:#"facebookRequestForMyFriends" object:self];
}
}];
}
}
Configure data into cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
// Configure the cell...
NSDictionary<FBGraphUser> *friend = [self.facebookFriends objectAtIndex:indexPath.row];
cell.textLabel.text = friend.name;
NSURL *profilePictureUrl = [NSURL URLWithString:[NSString stringWithFormat:#"http://graph.facebook.com/%#/picture?type=normal", friend.id]];
NSData *profilePictureData = [NSData dataWithContentsOfURL:profilePictureUrl];
cell.imageView.image = [UIImage imageWithData:profilePictureData];
return cell;
}
How do people learn how to make all these things with the Facebook SDK, I spent almost 4 hours trying to do what I described above and I feel like I am completely missing something.
There are several open source libraries that make the task of downloading an image easy. My favorite is AFNetworking, but there's also SDWebImage. Using one of these categories, your task becomes as simple as:
[myImageView setImageURL:"http://test.com/test.jpg"];
Even if you don't want to use these libraries, don't do it the way you're currently attempting it (i.e. [NSData dataWithContentsOfURL:profilePictureUrl]). I believe that is a synchronous operation that will block your UI. Use code from one of the 2 links above, or make your own UIImageView category.
Related
I have added a table view, and I am display image in the cells. I have also added this code:
So that the cells resize depending on the image.
When I launch my app though, I get this : [![enter image description here][1]][1]
And the images do not load untill I start scrolling...If I scroll down half the page then go back to the top, I get this: Which is correct
[![enter image description here][2]][2]
Any ideas? I have researched on google and tried the odd solution for the older versions of Xcode, But nothing seems to work!
Here is the rest of my code from the TableViewController:
Image isn't loaded correctly in cellForRowAtIndexPath delegate method, you're (probably) downloading the image in the background, so cellForRowAtIndexPath is returned before image is ready.
Downloaded image is probably cached somewhere so next time it's loaded properly.
post.downloadImage() better have a callback closure to be called when image was downloaded, to assign the downloaded image into the proper cell.
Keep in mind that user may scroll this cell out of the screen before image is loaded, so you better use a unique id to abort downloaded image assignment if cell has already changed.
Here's an example for a method that downloads an image in the background, then assigns it to the cell -
+ (void)loadImage:(NSString *)imageUrl onComplete:(void(^)(UIImage *image, BOOL loaded, NSString *callIdentifier))callback callIdentifier:(NSString *)callIdentifier {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul), ^{
[self downloadPicture:url onComplete:^(UIImage *image, BOOL loaded) {
dispatch_sync(dispatch_get_main_queue(), ^{
callback(image, loaded, callIdentifier);
});
}];
});
callback([UIImage imageNamed:#"placeholder"], NO, callIdentifier);
}
+ (void)downloadPicture:(NSString *)url saveTo:(NSString *)filePath onComplete:(void (^)(UIImage *image, BOOL loaded))onComplete {
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url] options:NSDataReadingMappedAlways error:&error];
if (!error) {
UIImage *image = [UIImage imageWithData:data scale:GTUserPictureScale];
if (onComplete)
onComplete(image, YES);
} else {
NSLog(#"Error loading user picture: %#", [error description]);
if (onComplete)
onComplete([UIImage imageNamed:#"missing"], NO);
}
}
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ...
__weak MyClass *wself = self;
self.imageUrl = #"http://...";
[self loadImage:self.imageUrl onComplete:^(UIImage *image, BOOL loaded, NSString *callIdentifier) {
#synchronized(wself) {
if ([callIdentifier isEqualToString:wself.imageUrl]) {
if (loaded) {
wself.image = image;
}
} else
NSLog(#"Expired load image request for id %#", callIdentifier);
}
} callIdentifier:self.imageUrl];
// ...
}
Just to better give an idea of what this post is about, it ultimately ends out in this question:
How do I asynchronously download a not predefined number of images from any number of url's, add them to a dictionary (or array, if that's easier) so that they are added in the order the download is started, instead of adding them in the order they are finished?
This is the backbone question of this post, however for good measures and to actually allow one to understand what I mean, I have added my specific case involving this question in the following post. I know it's long, but it's pretty complex to explain my case.
So, here goes nothing:
I have a tableView which loads a couple of things from different arrays. All the arrays is created from a JSON fetch each time the app is launched, the idea being that I can update the info in my app by simply updating the JSON text file. One of the entries in the JSON text file contain url's to images, which I want to add to the tableView cell's contentview. I got the JSON fetch working, and a dictionary "dataDictionary" containing the info from the fetch is created.
The arrays then, get created like this:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// Array is created from the dictionary created from the JSON fetch
_dataArray = _dataDictionary[#"games"];
// Arrays that will eventually hold info from JSON file is created
_Titles = [[NSMutableArray alloc] init];
_Descriptions = [[NSMutableArray alloc] init];
_Date = [[NSMutableArray alloc] init];
//images will be added to dictionary instead
_imageDict = [[NSMutableDictionary alloc]init];
// Info parsed from the JSON fetch, is now added to arrays
for (NSDictionary *eachData in _dataArray)
{
_Title = eachData[#"Title"];
_Description = eachData[#"Description"];
_Date = eachData[#"Release"];
// Key for image url is created
_image = eachData[#"ImageLink"];
[_Titles addObject:_Title];
[_Descriptions addObject:_Description];
[_Dates addObject:_Date];
Now there is more code below this, which is where I handle the images (will come just after this short explanation and below code sample), as you can see I have specified a key named "image" for the ImageLink entry in the JSON. Now I call this method, which starts an asynchronous download of the images:
- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^) (BOOL succeeded, UIImage *image))completionBlock
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if ( !error )
{
UIImage *image = [[UIImage alloc] initWithData:data];
completionBlock(YES,image);
} else{
completionBlock(NO,nil);
}
}];
}
This method is called in the below code, so to continue the code from before:
// Just to be clear, we are now back in the "for (NSDictionary *eachData in _dataArray)" statement from the first code sample
//calling the above method here
[self downloadImageWithURL:[NSURL URLWithString:_image] completionBlock:^(BOOL succeeded, UIImage *image) {
if (succeeded) {
//All this code runs when an image is successfully downloaded
NSLog(#"image download succesfull");
UIImage *downloadedImage = [[UIImage alloc] init];
// Doing this so I can resize the downloaded image to a proper size
downloadedImage = image;
//long boring code to resize image here
UIImage *resizedImage = [[UIImage alloc] init];
// resizedImage is as the name implies, the resized image that now has a proper size for the cell's content view
_number++; //Int that start out as -1, this increments each time an image is downloaded to allow the images to be added to the dictionary with the keys 0, 1, 2...
// Wrapping the int in a NSNumber
NSNumber *number = [NSNumber numberWithInt:_number];
// Turning the NSnumber into a string
NSString *key = [number stringValue];
// Images finally being added to the dictionary, but, in the wrong "order" - or rather, with the wrong keys / numbers
[_imageDict setObject:resizedImage forKey:key];
//Update tableView when images are added to dictionary
if (_imageDict.count == _gameTitles.count) {
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
}
}];
//Update tableView when data is added to array, this is to allow info to be shown even before the images are done downloading
if (Titles.count == _dataArray.count) {
// Update tableView with loaded content
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
} // "for" statement ends here
}
To sum the most important parts up:
When filling the cell's in the tableView, I use the indexPath.row to get the proper info from each array.
The images download, and I add them to an dictionary with the keys 0, 1, 2.. when finished, however, because the download is asynchronous the download do not finish in the order it was initialized, rather, the smaller images get (not surprisingly) downloaded and added to the image dictionary first. This means that the keys of the images does not fit with the order in all my other arrays, so setting the image in the first cell with indexPath.row simply sets the image that was downloaded first, rather than the image download that was started first. Basically, even if image 5 gets downloaded first, it should be added with the key "4", and not "0".
I should also mention that because of the JSON fetch I do NOT know how many pictures will be downloaded beforehand as this can change depending on the JSON. This cancels out a lot of the answers here on stackoverflow (and other places as well).
So all this text and code and whatnot leads to the beginning question, how do I asynchronously download a not predefined number of images from any number of url's, add them to a dictionary (or array, if that's easier) so that they are added in the order the download is started, instead of adding them in the order they are finished?
Thank you very much for taking your time to read this.
So all this text and code and whatnot leads to the beginning question, how do I asynchronously download a not predefined number of images from any number of url's, add them to a dictionary (or array, if that's easier) so that they are added in the order the download is started, instead of adding them in the order they are finished?
You can use SDWebImage Library to download images,it asynchronously downloads your images from given URLs and it also caches images and manages all the things for you and it is really popular.All you have to do is to add below code to your -cellForRowAtIndexPath delegate method:
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:imageURLThatYouGetFromJSONResponse]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
Also I recommend you not to use arrays or dictionaries.You can store that all information in an object.It is easy and best-practise in many cases and called as object-oriented approach.You create an NSObject subclass and in there create properties "title", "description", "name", "imageURL".You can follow this OOP Tutorial for some better understanding.
You don't care about number of images, because you define the number of rows in -numberOfRowsInSection and -cellForRowAtIndexPath is called the times you wrote in -numberOfRowsInSection.You can add:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [arrayOfYourObjects count];
}
Edit: How to manipulate images while using SDWebImage?
Answer: You can use SDWebImageManager, it also manages caching for you
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager downloadImageWithURL:imageURL
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// progression tracking code
}
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image) {
// Code to resize
//After resizing
cell.imageView.image = resizedImage;
}
}];
I have 2 table views in 2 view controllers. When i select a cell from first screen i want to load the second table , but my problem is : the table is empty and is populating after i scroll.
This is how i populate my table :
When cell is selected :
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
NSURL *url = [NSURL URLWithString:#"http://privatereisen.com/"];
ProgViewController *cd = [self.storyboard instantiateViewControllerWithIdentifier:#"progidentifview"];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSString *path=[NSString stringWithFormat :#"dok/TV/pays/italie/json/chaine%d.json",indexPath.row];
[httpClient postPath:path parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject)
{resultofData = [NSJSONSerialization JSONObjectWithData:responseObject options:kNilOptions error:nil];
NSString *valuKey=[NSString stringWithFormat :#"chaine%d",indexPath.row];
NSArray *testArray =[resultofData valueForKey:valuKey];
cd.categ=[[NSArray alloc]initWithArray:testArray];
cd.categArray=[[NSArray alloc]initWithArray:self.categoryArray];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[[[UIAlertView alloc]initWithTitle:#":( " message:#"Pas d'internet !" delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil]show];
// NSLog(#"[HTTPClient Error]: %#", error.localizedDescription);
}];
[self.navigationController pushViewController:cd animated:YES];
[tableView cellForRowAtIndexPath:indexPath].selected = NO;
}
and in ProgViewController :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
cprogCell *cell = [tableView dequeueReusableCellWithIdentifier:#"identifproecell"];
cell.heure.text = [[_categ objectAtIndex:indexPath.row]objectForKey:#"horaire_programme"];
cell.type.text = #"";
cell.desc.text = [[_categ objectAtIndex:indexPath.row]objectForKey:#"nom_programme"];
return cell;
}
You need to reload the UITableView once your asynchronous request has completed. Currently you push the UIViewController onto the stack and then datasource for it gets set later on, so the cell's only get loaded once you scroll.
To fix this, after you set the cd.categ and cd.categArray, make sure you either call [cd.tableView reloadData], or in the setters for categ or categArray call [self.tableView reloadData]
Too late, but I think you are handling the data to be displayed in the wrong place. For a period of time the user experience is to see a blank screen with no data.
It would be better to move all the data into the controller being pushed. So in your select, create the controller and set the data to be fetched as a property. In the ProgViewController, when inside viewDidAppear start the fetch and activate an activity indicator showing that it is busy. On success or failure stop the activity indicator. On success, call self.tableView.reloadData. On failure, raise the alert and pop the controller.
This way everything is being encapsulated and handled in the right place with the UI updating itself as its internal data state changes.
If you do the fetch from within the initial controller, then you should show its busy and only push the new controller once you have a result for it to display IMHO.
I have two issues here.
1) The data in my UITableView does not load when I first open up its ViewController. The proper data does end up appearing, but but only after I add more data to it on another ViewController, and then come back to the TableViewController. Even when doing that, the UITableView is always one item behind, meaning that when I first load the app I will see nothing in my TableView, then if I add an item, for example called "hat" in a second ViewController, I will come back to the TableViewController and see all the items I'd added previously, but I will have to add another item, for example called "chair" to my table in order to see "hat" in my table.
2) While I am able to have users successfully add items to my Parse database and then view the items they've added in the UITableView (albeit with the roadblock addressed in issue 1 above), I am unable to successfully populate the UIImage in each cell in the TableView with the photo file of each cell's corresponding item. The photos are being successfully saved to Parse, so it is definitely a problem querying for them properly, or keying into the exact place they are stored, or simply configuring my subclassed TableViewCell to display them properly.
ItemsTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.navigationItem setHidesBackButton:YES];
}
-(void)viewWillAppear:(BOOL)animated
{
PFQuery *query = [PFQuery queryWithClassName:#"giveItem"];
[query whereKey:#"giver" equalTo:[PFUser currentUser]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
self.myGiveItems = [[NSMutableArray alloc]init];
for (PFObject *object in objects) {
PFGiveItem *newGiveItem = [[PFGiveItem alloc]init];
newGiveItem.giveItemName = object[#"giveItemTitle"];
// return photo files for each of the objecs
PFFile *giveItemImageFile = object[#"imageFile"];
[giveItemImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *giveItemImageForCell = [UIImage imageWithData:imageData];
newGiveItem.giveItemImage = giveItemImageForCell;
};
}];
[self.myGiveItems addObject:newGiveItem];
}
} else {
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
[self.tableView reloadData];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.myGiveItems.count;
}
-(void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section
{
tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
}
- (JFGiveItemCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"Cell";
JFGiveItemCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil){
cell = [[JFGiveItemCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
}
PFGiveItem *giveItem = self.myGiveItems[indexPath.row];
cell.giveItemLabel.text = giveItem.giveItemName;
cell.giveItemImageView.image = giveItem.giveItemImage;
return cell;
}
My Parse backend is structured as follows
I will be very receptive to any guidance on how to more properly structure my database to be less redundant and more powerful.
Class 1 "Item" with rows:
"itemTitle," a user-input string from the AddItem ViewController
"owner," the user who adds the item (and photo)
"itemPhoto," a pointer to an object of Class 2, "ItemPhoto"
Class 1 "Item" with rows:
"imageOwner," the user who adds the item (and photo)
"imageName," a string the same as the itemTitle from Class 1
"imageFile," a File uploaded by the user
All of these items are saved successfully, but for your better understanding, here is the code I use to do this.
AddItemViewController
-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
NSString *nameForGiveItem = self.giveItemTitleTextField.text;
NSData *giveItemImageData = UIImagePNGRepresentation(self.giveItemImage);
PFFile *giveItemImageFile = [PFFile fileWithName:nameForGiveItem data:giveItemImageData];
PFObject *giveItemPhoto = [PFObject objectWithClassName:#"giveItemPhoto"];
giveItemPhoto[#"imageOwner"] = [PFUser currentUser];
giveItemPhoto[#"imageName"] = nameForGiveItem;
giveItemPhoto[#"imageFile"] = giveItemImageFile;
[giveItemPhoto saveInBackground];
PFObject *giveItem = [PFObject objectWithClassName:#"giveItem"];
giveItem[#"giveItemTitle"] = self.giveItemTitleTextField.text;
giveItem[#"giver"] = [PFUser currentUser];
[giveItem setObject:giveItemPhoto forKey:#"giveItemPhoto"];
[giveItem saveInBackground];
[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:1] animated:YES];
return YES;
}
For 1)
In viewWillAppear you call [self.tableView reloadData]; outside of the block. That means you actually call it before the block has finished executing - meaning before the data was loaded.
Move it to after the for block but make sure it is executed on the main thread - otherwise it will not influence the appearance of the ui - meangin the table will not be acualized either.
For 2)
The solution for 1) should fix 2) as well. You load your images asynchronously, which is perfect. But when that is finished you do not reload the table. When you manage to get it reloaded once all the data has been loaded then the cell images will be refreshed as well.
3)
You may want to add any type of view/spinner/progress indicator or just a regular lable that indicates to the user that some data is still loading ...
I have created a custom UITableViewCell which is composed by 2 UILabels and a single UIImageView.
Data associated with cells is available with a NSObject class named CellInfo. CellInfo has 2 properties of NSString type and an UIImage property.
When I create a CellInfo instance, inside the initWithData method (CellInfo class), I do the following:
if(self = [super alloc])
{
//initialize strings variables
self.name = aName;
self.descritpion = aDescription;
[self grabImage]
}
return self;
where grabImage (within CellInfo class) using ASIHTTPrequest framework to grab images in asynchronous manner (in the following code NSURL is alaways the same but in reality it changes with data)
- (void)grabImage
{
NSURL *url = [NSURL URLWithString:#"http://myurl.com/img.png"];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
NSData *data = [request responseData];
UIImage* img = [[UIImage alloc] initWithData:data];
self.image = img;
[img release];
// Send a notification if image has been downloaded
[[NSNotificationCenter defaultCenter] postNotificationName:#"imageupdated" object:self];
}];
[request setFailedBlock:^{
NSError *error = [request error];
// Set default image to self.image property of CellInfo class
}];
[request startAsynchronous];
}
I have also a UITableViewController that loads data into the custom cell like the following:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Do stuff here...
// Configure the cell...
((CustomTableViewCell*)cell).nameOutlet.text = ((CellInfo*) [self.infoArray objectAtIndex:indexPath.row]).name;
((CustomTableViewCell*)cell).descriptionOutlet.text = ((CellInfo*) [self.infoArray objectAtIndex:indexPath.row]).descritpion;
((CustomTableViewCell*)cell).imageViewOutlet.image = ((CellInfo*) [self.infoArray objectAtIndex:indexPath.row]).image;
return cell;
}
In addiction, this UITableViewController observes notification from the CellInfo class because, at start up, images for visible cells are not displayed. This is the method that is called when the notification is captured:
- (void)imageUpdated:(NSNotification *)notif {
CellInfo * cInfo = [notif object];
int row = [self.infoArray indexOfObject:cInfo];
NSIndexPath * indexPath = [NSIndexPath indexPathForRow:row inSection:0];
NSLog(#"Image for row %d updated!", row);
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
The code works well, but I would like to know if I'm doing right or there is a better way to do this.
My doubt is the following: is it correct to save downloaded images within each CellInfo instance or is it possible to follow another way to cache images using, for example, cache policy provided by ASIHTTPRequest?
P.S. grabImage is not called if the image for a specific CellInfo instance has already been downloaded.
I believe that's pretty neat. Instead of that you might subclass UIImageView class and create an initializer like [AsyncUIImageView initWithURL:] and then put that ASIHttpRequest logic inside the view.
After it finishes loading the picture, there could be two ways:
It can call [self setNeedsDisplay] (an UIView method) so image view is redrawn.
You can pass UITableViewCell or UITableView as a delegate to AsyncUIImgeView so that it could tell table view to reload that cell.