I'm working on an app where I want to pull data from a remote web service and populate a UITableView. When I get new data I want the currently visible cells to remain and add the new data above it, much like most Twitter clients does. My load method currently looks like this:
- (void)loadPostsInBackground
{
NSURL *url = [NSURL URLWithString:#"[URL]"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation;
operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id jsonObject) {
[self createPostsFromDict:jsonObject];
[self.refreshControl endRefreshing];
[self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id jsonObject) {
NSLog(#"Received an HTTP %d", response.statusCode);
NSLog(#"The error was: %#", error);
}];
[operation start];
}
This works, but it updates the currently visible cells with the new data. So what I want is, get new data, add it above the currently visible cells (or stay at the currently visible cells). What is the best way to do this?
Note: I will require iOS6.
Instead of calling reloadData on the table view. You should call insertRowsAtIndexPaths:withRowAnimation:. Do that after updating the data used by the table view's data source.
Related
I'm starting to learn Objective-C for iOS Development, and a got a issue that is driving me crazy.
All that I want is to do a request, retrieve e JSON and then set this JSON into an instance property.
-(NSArray *) retrieveAtEndpoint:(NSString *)endpointURL withRootNode:(NSString *)rootNode
{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat: endpointURL, fuegoWSURL]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSDictionary *dict = (NSDictionary *) JSON;
[self setJSONObjectsCollection: [dict objectForKey:rootNode]];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Communication Error: %#", error);
}];
[operation start];
return _JSONObjectsCollection;
}
-(void) setJSONOBjectsCollectionAttribute: (NSArray *) arrayWithCollection
{
NSLog(#"Outside Method %#", arrayWithCollection);
self.JSONObjectsCollection = arrayWithCollection;
}
However, my self.JSONObjectsCollection property are ok inside the block, but outside is always null.
Can you help me guys ?
It's because the setting of JSONObjectsCollection happens asynchronously. So your method is returning JSONObjectsCollection before it is set.
Thus, it might look like:
- (void)retrieveAtEndpoint:(NSString *)endpointURL withRootNode:(NSString *)rootNode
{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat: endpointURL, fuegoWSURL]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSDictionary *dict = (NSDictionary *) JSON;
[self setJSONObjectsCollection: [dict objectForKey:rootNode]];
// do here whatever you want to do now that you have your array, e.g.
//
// [self.tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Communication Error: %#", error);
}];
[operation start];
}
Note, retrieveAtEndpoint now has a void return type, but in the completion block, I'm invoking whatever code you want to perform once the JSON objects collection has been updated.
If this is a method inside your model object, but you want to provide an interface by which the view controller can supply a block of code that should be executed upon successful retrieval of the JSON, use a completion block:
- (void)retrieveAtEndpoint:(NSString *)endpointURL withRootNode:(NSString *)rootNode completion:(void (^)(NSError *error))completion
{
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat: endpointURL, fuegoWSURL]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSDictionary *dict = (NSDictionary *) JSON;
[self setJSONObjectsCollection: [dict objectForKey:rootNode]];
if (completion)
{
completion(nil);
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
if (completion)
{
completion(error);
}
}];
[operation start];
}
Or, if you want to simplify your use of a block parameter, you can define a type for the completion block at the start of your model object's .h file (before the #interface block):
typedef void (^RetrievalCompleteBlock)(NSError *);
And then the method is simplified a bit:
- (void)retrieveAtEndpoint:(NSString *)endpointURL withRootNode:(NSString *)rootNode completion:(RetrievalCompleteBlock)completion
{
// the code here is like it is above
}
Anyway, regardless of whether you use the typedef or not, the view controller could do something like:
ModelObject *object = ...
NSString *rootNode = ...
[object retrieveAtEndpoint:url withRootNode:rootNode completion:^(NSError *error) {
if (error)
{
// handle the error any way you want, such as
NSLog(#"%s: retrieveAtEndPoint error: %#", __FUNCTION__, error);
}
else
{
// do whatever you want upon successful retrieval of the JSON here
}
}];
The details here will vary based upon how your view controller is accessing the model object, knows that the root node should be, etc. I often will include another parameter to my completion block which is the data being retrieved, but given that you updated your model object and can access it that way, perhaps that's not necessary. I simply don't have enough details about your implementation to know what is right here, so I kept my implementation as minimalist as possible.
But hopefully this illustrates the idea. Give your retrieveAtEndpoint method a completion block parameter, which lets the view controller specify what it wants to do upon completion (or failure) of the communication with the server.
I have a problem with completion block in AFNetworking, my app needs to get json data about car and use id from this json file to request another api to retrieve images before displaying all cars in uitableview. But the uitableview reload is always called before retrieving information so it displays empty table.
I have a Model.m
typedef void (^CompletionBlock)(NSArray *results);
-(void)getPhotosWithCompletionBlock: (CompletionBlock)completionBlock failureBlock: (FailureBlock)failureBlock{
NSURLRequest *request = [NSURLRequest alloc] initWithURL:[NSURL alloc] initWithString:
[NSString stringWithFormat:#"api.getPhoto"]]];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
completionBlock(JSON);
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
failureBlock(JSON);
}];
[operation start];
}
ModelViewController.m
- (void)getAllModels{
NSURLRequest *request = ...;
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
for(NSDictionary *model in JSON){
Model *model = [Model alloc] initWithId:...;
[model getPhotosWithCompletionBlock: ^(NSArray *results){
model.photos = results;
}failureBlock:^(NSError *error) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
}
[_tableView reloadData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
[operation start];
But [_tableView reload] is always called before completionBlock of getPhotos method finish so there's an empty tableView, so how to make completion block finish before calling reload tableView method? I can put reload method inside completion block but it forces tableView reload many times makes app very unresponsive, I have 1 more method to retrieve model info so I cannot put reload method inside every completion blocks.
P/S: any idea for my situation without using nested afnetworking operations?
This is not an AFNetworking related question, rather a general Objective-C or
asynchronous question.
You may try to reload the table view whenever the images of one model have been loaded:
- (void)getAllModels{
NSURLRequest *request = ...;
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
for(NSDictionary *model in JSON){
Model *model = [Model alloc] initWithId:...;
[model getPhotosWithCompletionBlock: ^(NSArray *results){
dispatch_async(dispatch_get_main_queue(), ^{
model.photos = results;
[self.tableView reloadData];
});
}failureBlock:^(NSError *error) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request Failed with Error: %#, %#", error, error.userInfo);
}];
You likely need to fix some edge cases when an error occurs.
If there are performance issues, you need to check where they occur. One usual suspect is the creation of UIImages on the main thread. You may schedule them to another embedded async task whose completion block then forces an update of the table view, or perform this in your getPhotosWithCompletionBock method.
You may also consider to be more explicit about which cell you need to update instead just simply sending reloadData: which will also reload images which are already up to date.
I am loading a bunch of images using AFNetworking and I would like to scale and apply rounded corners to these images before AFNetworking caches them.
I started out scaling and applying rounded corners to the images each time they were loaded but the completion block will also be called when the image is loaded from the cache and therefore this uses too many resources when a user scrolls a collection view filled with images.
[self.imageView setImageWithURLRequest:[NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReturnCacheDataElseLoad
timeoutInterval:10.0f]
placeholderImage:kVideoCollectionViewCellVideoImagePlaceholder
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
/**
* The image is edited here and this block is called
* when the image is loaded from web and from the cache.
*/
[self.imageView setImage:image];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
DDLogError(#"%#", error);
}];
AFNetworking seems to provide a great cache for my use, especially when I enable disk caching therefore I would like to use it but I can't figure out if there's a way to edit the image before it is cached.
Does anyone know if this is possible and if so, how it can be done?
After posting the question it hit me that I may have too look in another direction that using the UIImageView+AFNetworking category. Using the AFImageRequestOperation directly solved the problem.
__weak NZVideoCollectionViewCell *weakSelf = self;
NSURLRequest *request = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReturnCacheDataElseLoad
timeoutInterval:10.0f];
AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request imageProcessingBlock:^UIImage*(UIImage *image) {
/**
* Edit image.
*/
return editedImage;
}
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[weakSelf.imageView setImage:image];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
DDLogError(#"%#", error);
}];
[operation start];
As the title says, i need to get the image and put it in an array.
UIImageView *myImage = [[UIImageView alloc] init];
[myImage setImageWithURL:[NSURL URLWithString:#"http://host.com/images/1/1.png"] placeholderImage:[UIImage imageNamed:#"page3.png"]];
[items addObject:myImage];
the problem is that while the image is being loaded, the code continues and nothing gets put in the array, actually I guess an empty UIImageView gets inserted.
How do I go about fixing this?
On a side note I am using iCarousel to display images, in the data source it gets the images array and shows them. Can i modify iCarousel somehow?
Thanks in advance.
You can use the setImageWithURLRequest method to give a success message or execute the UI update when it has finished for example using the following method:
- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest
placeholderImage:(UIImage *)placeholderImage
success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;
Here is an example (not tested)
[image setImageWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"host.com/images/1/1.png"]] placeholderImage:[UIImage imageNamed:#"page3.png"]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image){
//reload data here
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error){
//handle errors here
}
i have a little problem with this, I'm loading an Image from a Url like this:
+ (void)getImageFromURL:(NSString *)imageFilename urlMode:(NSString *)mode block:(id (^)(UIImage *responseImage))aImage {
NSURL *url = [NSURL URLWithString:[mainURL stringByAppendingString:mode]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"GET"];
AFImageRequestOperation *requestOperation = [AFImageRequestOperation imageRequestOperationWithRequest:request
imageProcessingBlock:nil
cacheName:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
aImage(image);
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error)
{
// manage errors
}];
[[[NSOperationQueue alloc]init] addOperation:requestOperation];
}
I'm trying to set an iVar UIImage *userAvatar to the response from this request, but the problem is, since its an async request I'm not getting the iVar set before my Code moves on, so my iVar is empty when I'm accessing it and passing it to another method.
That's the nature of asynchronous programming! You are going to have to redesign the dependencies on userAvatar to take into account that it's availability is nondeterministic.
So, rather than having your operation's success block simply set the userAvatar ivar, it takes care of whatever needs to happen once that image is available. For example if you want to set a UIImageView's image, then in your success block:
dispatch_async(dispatch_get_main_queue(), ^{
myImageView.image = image;
});
(Without knowing the details of your goals and details of your implementation, this is just a "for example...")
You forgot to add [requestOperation start]; at the end.