I'm using the Facebook iOS SDK and using the Graph API to upload videos to Facebook.
The uploading is working perfectly fine, but can I keep track of the progress of the upload so I can reflect the progress in a progress bar.
This is an old question but what you're trying to do is possible with latest Facebook iOS SDK v3.9. (27 Oct 2013)
Essentially, FBRequestConnection exposes a property urlRequest (NSMutableURLRequest) that you can use to send out the data any other third party networking frameworks or even the ones Apple provided.
https://developers.facebook.com/docs/reference/ios/current/class/FBRequestConnection#urlRequest
Here's an example how I get progress callbacks using AFNetworking 1.x.
Prepare Request Body
NSDictionary *parameters = #{ #"video.mov": videoData,
#"title": #"Upload Title",
#"description": #"Upload Description" };
Create FBRequest
FBRequest *request = [FBRequest requestWithGraphPath:#"me/videos"
parameters:parameters
HTTPMethod:#"POST"];
Generate FBRequestConnection (Cancel & Extract URLRequest)
FBRequestConnection *requestConnection = [request startWithCompletionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
}];
[requestConnection cancel];
NSMutableURLRequest *urlRequest = requestConnection.urlRequest;
Use AFNetworking HTTPRequestOperation
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:urlRequest];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Do your success callback.
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Do your failure callback.
}];
Set Progress Callback
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
NSLog(#"Sent %lld of %lld bytes", totalBytesWritten, totalBytesExpectedToWrite);
}];
Start the operation
[[APIClient sharedInstance] enqueueHTTPRequestOperation:operation];
// APIClient is a singleton class for AFHTTPClient subclass
I've finally found a way of doing this after looking around in NSURLConnection. It means adding the following code inside of the FBRequest.h and FBRequest.m files to create a new delegate.
At the bottom of the FBRequest.m file there are all of the methods for NSURLConnectionDelegate. Add this code here:
- (void)connection:connection
didSendBodyData:(NSInteger)bytesWritten
totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
{
float percentComplete = ((float)totalBytesWritten/(float)totalBytesExpectedToWrite);
if ([_delegate respondsToSelector:#selector(request:uploadPercentComplete:)])
{
[_delegate request:self uploadPercentComplete:percentComplete];
}
}
Now put this in the FBRequest.h class to create a new FBRequest delegate:
/**
* Called a data packet is sent
*
* The result object is a float of the percent of data sent
*/
- (void)request:(FBRequest *)request uploadPercentComplete:(float)per;
This goes at the bottom of the FBRequest.h file after:
#protocol FBRequestDelegate <NSObject>
#optional
Now all you have to do is call this new delegate anywhere in your code like you would any other FBRequest delegate and it will give you a float from 0.0 to 1.0 (0% to 100%).
Strange that the Facebook API doesn't have this (along with upload cancel which I found out how to do here How to cancel a video upload in progress using the Facebook iOS SDK?) as it's not that tricky.
Enjoy!
Related
Does anyone know how to set ticket inside AFHTTPSessionOperation?
This is the previous call using AFNetworking framework 1.0
NSURLRequest* request = [self.myClient requestWithMethod:#"POST" path:[NSString stringWithFormat:#"%#/%#", controller, action] parameters:parameters];
AFHTTPRequestOperation* operation = [self.myClient HTTPRequestOperationWithRequest:request success:success failure:failure];
[self.mirrorClient enqueueHTTPRequestOperation:operation];
The ticket is stored inside the self.myClient. self.myClient.ticket
But I'm not sure how to implement that in the following call using AFHTTPSessionOperation with AFNetworking framework 3.1.
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer = manager.requestSerializer;
[requestSerializer setValue:[NSString stringWithFormat:#"%#", self.myClient.ticket] forHTTPHeaderField:#"Authorization"];
NSOperation *operation = [AFHTTPSessionOperation operationWithManager:manager HTTPMethod:#"POST"
URLString:urlString parameters:parameters
uploadProgress:nil downloadProgress: nil
success:success failure:failure];
Thank you
This code looks basically correct. You could simplify the requestSerializer configuration a tad, and I might not instantiate a new session for every request, but the following worked fine for me:
- (void)performRequest:(NSString *)urlString
parameters:(id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager.requestSerializer setValue:self.myClient.ticket forHTTPHeaderField:#"Authorization"];
NSOperation *operation = [AFHTTPSessionOperation operationWithManager:manager
HTTPMethod:#"POST"
URLString:urlString
parameters:parameters
uploadProgress:nil
downloadProgress:nil
success:success
failure:failure];
[self.queue addOperation:operation];
}
I watched it in Charles, and the ticket, 12345678 appeared in my request header, as expected:
I suspect your problem rests elsewhere. This code does set the Authorization header to ticket. Make sure this is the right place to set the ticket. Also, make sure the ticket is what you think it is.
I'm using AFHTTPRequestOperation to download a file. But on pausing and resuming the operation, the api gives incorrect progress count. I am downloading data using following code
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:str]];
_downloadOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
_downloadOperation.outputStream = [NSOutputStream outputStreamToFileAtPath:strFilePath append:YES];
[_downloadOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSLog(#"Progress %lld",totalBytesRead * 100 / totalBytesExpectedToRead);
}];
[_downloadOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"downloaded %#",operation.request.URL);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"failed");
}];
[_downloadOperation start];
I pause the operation when user goes in background,
- (void)applicationDidEnterBackground:(UIApplication *)application
{
if ([objAPI.downloadOperation isExecuting])
[objAPI.downloadOperation pause];
}
and resume operation when user comes in foreground
- (void)applicationWillEnterForeground:(UIApplication *)application
{
if ([objAPI.downloadOperation isPaused])
[objAPI.downloadOperation resume];
}
Example: If the operation is paused at progress 20% , on resuming it starts from 20% but ends at 120% . In other words the progress count goes incorrectly after pausing the operation.
Kindly help me to solve the problem
There is a related issue which would cause the percentages when using AFURLSessionManager to be incorrect.
The pull-request at https://github.com/AFNetworking/AFNetworking/pull/1786 may fix this issue.
I have an HTTPClient request as follows :
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:urlStringMain]];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
//parameters
nil];
[self beginBackgroundUpdateTask];
[httpClient postPath:postPath parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
//id results = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONWritingPrettyPrinted error:nil];
//completion code
[self endBackgroundUpdateTask];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//failure code
}];
[httpClient release];
The background task is executed in :
- (void) beginBackgroundUpdateTask{
[operationQueue addOperationWithBlock:^{
NSLog(#"started upload process as a background job");
self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
}];
}
And ended in :
- (void) endBackgroundUpdateTask{
NSLog(#"complete upload process as a background job");
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];
self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}
where self.backgroundUpdateTask is a UIBackgroundTaskIdentifier object, and operationQueue is an object of NSOperationQueue (public member), initialized in viewDidLoad:
operationQueue = [[NSOperationQueue alloc] init];
[operationQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount];
Now what I want to do is, perform these requests chronologically, in the background, such that pushing/popping from the viewController does not affect the request. It should also not be affected if the application goes into background. At times I post text, and at other times I post an image. Now, images take longer to upload, than text so if subsequent requests of text and images are made, texts are posted first, and images later. This breaks the chronology of the tasks, hence I wanted to use the NSOperationQueue. But being new to operation queues, I cannot seem to make it work. The chronology is still not being respected. How do I perform the task in the way I want to.
PS. Also, as you can see in the code, i have added [self endBackgroundUpdateTask] in both, the completion block of the httpClient request, and the beginBackgroundUpdateTask method. Now i understand this is not good. Where exactly should the endBackgroundUpdateTask method be called ?
Thank you.
The HTTPClient can handle the background task for you if you ask it to, simply set the appropriate flag (you need to create the operation and call setShouldExecuteAsBackgroundTaskWithExpirationHandler:).
Rather than change how the uploads operate, keep it simple. Send a date with the upload and use that to maintain your order information on the server.
If you really must execute your operations serially then you can get the operationQueue from the client and set it to only execute a single operation at a time.
In either case you shouldn't need to create your own operation queue.
I'm using AFNetworking and AFJSONRequestOperation to post an image along with some parameters to my REST server with this code.
NSData* uploadFile = nil;
if ([params objectForKey:#"file"]) {
uploadFile = (NSData*)[params objectForKey:#"file"];
[params removeObjectForKey:#"file"];
}
NSMutableURLRequest *apiRequest =
[self multipartFormRequestWithMethod:#"POST"
path:kAPIPath
parameters:params
constructingBodyWithBlock: ^(id <AFMultipartFormData>formData) {
if (uploadFile) {
[formData appendPartWithFileData:uploadFile
name:#"file"
fileName:#"photo.jpg"
mimeType:#"image/jpeg"];
[formData throttleBandwidthWithPacketSize:5000 delay:kAFUploadStream3GSuggestedDelay];
}
}];
AFHTTPRequestOperation *operation2 = [[AFJSONRequestOperation alloc] initWithRequest: apiRequest];
[operation2 setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation2, id responseObject) {
//success!
completionBlock(responseObject);
} failure:^(AFHTTPRequestOperation *operation2, NSError *error) {
//failure :(
completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription] forKey:#"error"]);
}];
[operation2 setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
if (self.delegate) {
NSLog(#"Sent %lld of %lld bytes", totalBytesWritten, totalBytesExpectedToWrite);
[self.delegate didReceiveData:(totalBytesWritten / (float)totalBytesExpectedToWrite)];
}
}];
[[API sharedInstance] enqueueHTTPRequestOperation:operation2];
This works randomly. I mean some times everything works perfectly and some times the progression get stucked in the middle of the sending process and finish with a Timeout error.
I tried with tons of parameters and different combinations but I have always the same behavior.
PS : I use the last version of the Framework.
PS2 : All my tests are made on both simulator and device, and also over a Wifi connection
This works randomly. I mean some times everything works perfectly and some times the progression get stucked in the middle of the sending process and finish with a Timeout error.
This is a server-side issue, then. Depending on what kind of endpoint your uploading to, you may need to throttle the data rate of the upload.
I've been looking around, saw similar posts, but nothing like this that could give me answers. This is my setup and flow of the app:
User has to login via Facebook, using Facebook Graph. LoginView is presented modally, non animated
When user logins I can retrieve FBID and I use this fbid to send it to my web service (REST)
Web service gets the FBID from the NSURL and matches it with database to retrieve other user info
Using JSONserialization i parse the JSON received from web service and display it in the view
PROBLEM: Everything returns NULL except FBID that I get from Facebook. BUT, if I logout from Facebook and then login, that's when it works.
Here is my code:
viewDidAppear method:
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:NO];
if (FBSession.activeSession.isOpen) {
[self populateUserDetails];
}
//Connect to WebService
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"http://atnightcorp.com/api/member/id/%#/format/json", fbid]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection connectionWithRequest:request delegate:self];
NSArray *pics = [member valueForKeyPath:#"photos"];
NSString *picCount = [NSString stringWithFormat:#"%d", [pics count]];
[photosCount setTitle:picCount forState:UIControlStateNormal];
NSLog(#"PHOTO: %#", picCount);
NSLog(#"FB: %#", fbid);
}
I tried putting NSURL request and connection code in viewDidLoad, but then I don't get anything back.
My NSURLConnection methods:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
data = [[NSMutableData alloc]init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)theData
{
[data appendData:theData];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
member = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
UIAlertView *errorView = [[UIAlertView alloc]initWithTitle:#"Error" message:#"The download could not complete. Please make sure you are connected to internet" delegate:nil cancelButtonTitle:#"Dismiss" otherButtonTitles:nil];
[errorView show];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
The populateUserDetails method that you have seen above:
- (void)populateUserDetails
{
if (FBSession.activeSession.isOpen) {
[[FBRequest requestForMe] startWithCompletionHandler:
^(FBRequestConnection *connection,
NSDictionary<FBGraphUser> *user,
NSError *error) {
if (!error) {
self.userProfileImage.profileID = user.id;
self.navigationItem.title = user.name;
self.fbid = user.id;
}
}];
}
}
This method basically sets the FBID once user is logged in. Other important things you should know that could help you understand my project:
FBID is set as NSString property in my .H file
All facebook connect thing goes on in AppDelegate
I need to dynamically set the NSURL after I find out who the user is.
if I manually input FBID in NSURL, then it works.
everything should be executed when user logins, I think that the timing of retrieving fbid and receiving data from web service is wrong but I can't get to fix it.
IF you need anything else, I will elaborate more and post more code if needed. -
PLEASE HELP as I've been looking for answers for last 3 days.
Your problem is that the populateUserDetails is called and it returns without waiting the code to be executed (because it's an async task with a completition handler, and when you call the NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"http://atnightcorp.com/api/member/id/%#/format/json", fbid]]; for the first time, the fbid is nuil or not set properly (also you should use self.fbid not fbid since fbid is a property).
So you should try to move the whole code that is handling the request from viewDidAppear into a separate method and you should call that method from startWithCompletionHandler after you set the line with self.fbid = user.id
Also call [super viewDidAppear:animated]; not with NO param (this won't solve your problem but this is the right way to do it)