I'm interested in replacing some old AFNetworking 1.0 code with 2.0 using NSProgress. Here is a sketch of what I'm thinking of...
NSProgress *overallProgress = [NSProgress progressWithTotalUnitCount:[requests count]];
for (NSURLRequest *request in requests) {
[overallProgress becomeCurrentWithPendingUnitCount:1];
[self downloadTask:request];
[overallProgress resignCurrent];
}
- (void)downloadTaskWithRequest:(NSURLRequest *)request
{
NSProgress *progress = nil;
NSURLSessionDownloadTask *task = [self.sessionManager downloadTaskWithRequest:request progress:&progress destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
}];
}
I've read several posts on this and I'm having trouble composing the sub-tasks progress with the overallProgress. Getting progress back for a single file works, but trying to compose NSProgress tasks under and umbrella task eludes me.
How can I create an overall task with N pieces and then have each file as its download update the overall task?
Related
I handle some old code, it runs well, but now crash only on ios 14
here is the demo
static NSData *DownloadWithRange(NSURL *URL, NSError *__autoreleasing *error) {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:URL];
request.timeoutInterval = 10.0;
__block NSData *data = nil;
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
NSURLSession *URLSession = [NSURLSession sessionWithConfiguration:config];
NSURLSessionDataTask *task = [URLSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable taskData, NSURLResponse * _Nullable response, NSError * _Nullable taskError) {
data = taskData;
if (error)
*error = taskError;
dispatch_semaphore_signal(sema);
}];
[task resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return data;
}
- (IBAction)crashButton:(id)sender {
NSURL *url = [NSURL URLWithString:#"http://error"];
NSError * error = nil;
NSData *compressedData = DownloadWithRange(url, &error);
NSLog(#"error is %#",error);
}
before DownloadWithRange returned, the taskError memory(NSURLError) has released
on ios 13, it don't crash
it's really weird
The zombie diagnostics are letting you know that the autorelease object is getting deallocated by the time the data is returned. You should not be instantiating an autorelease object in one thread and trying to have a pool on a separate thread manage that. As the docs say:
Autorelease pools are tied to the current thread and scope by their nature.
While the problem might be manifesting itself differently in iOS 14, I do not believe that this pattern was ever acceptable/prudent.
If you're going to use this pattern (which I wouldn't advise; see below), you can solve this problem by copying the error object on the calling thread before returning:
static NSData *DownloadWithRange(NSURL *URL, NSError * __autoreleasing *error) {
...
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
if (error) {
*error = [*error copy];
}
return data;
}
FWIW, this technique of using semaphore to make asynchronous method behave synchronously is generally considered an anti-pattern. And you definitely should never use this pattern from the main thread.
I would suggest adopting asynchronous patterns:
- (NSURLSessionTask *)dataTaskWithURL:(NSURL *)url completion:(void (^ _Nonnull)(NSData * _Nullable data, NSError * _Nullable error))completion {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
request.timeoutInterval = 10.0;
NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(data, error);
});
}];
[task resume];
[session finishTasksAndInvalidate];
return task;
}
And
[self dataTaskWithURL:url completion:^(NSData * _Nullable data, NSError * _Nullable error) {
// use `data` and `error` here
}];
// but not here
Note, in addition to adopting asynchronous completion block pattern, a few other observations:
If you’re going to create a new NSURLSession for each request, make sure to invalidate it or else you will leak memory.
I’m returning the NSURLSessionTask, which some callers may want in case they might want to cancel the request (e.g. if the view in question is dismissed or a new request must be generated). But as shown above, you don’t need to use this NSURLSessionTask reference if you don’t want.
I'm dispatching the completion handler back to the main queue. That is not strictly necessary, but it is often a useful convenience.
I'm not sure if this is a question with a obvious answer but i haven't been able to find any.
I'm using AFNetworking to connect with my REST server.
I'm doing basic task like uploading and downloading images, posting and getting json etc etc.
What is the best practice to update UI when somethings changes. If for example have successfully downloadet the profile picture and need to change the image inside a tableview.
I only have 1 class that uses AFNetworking my APIConnector
APIConnector.h
#interface APIConnector : NSObject
-(void)downloadClientImageToSystem:(NSString *)imageURL;
#end
APIConnector.m
-(void)downloadClientImageToSystem:(NSString *)imageURL{
//setup
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
//Set url
NSURL *URL = [NSURL URLWithString:[NSString stringWithFormat:#"%#%#",backendURL,imageURL]];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
//Create a download task
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
NSString *filename = [NSString stringWithFormat:#"%#.jpeg",[[imageURL componentsSeparatedByString:#"&imgIndex="] lastObject]];
return [documentsDirectoryURL URLByAppendingPathComponent:filename];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error)
{
if (error) {
NSLog(#"there was an error downloading profile image");
[[NSNotificationCenter defaultCenter] postNotificationName:DLImageFail object:self];
}
else{
NSLog(#"File downloaded to: %#", filePath);
[[NSNotificationCenter defaultCenter] postNotificationName:DLImageSucces object:self];
}
}];
[downloadTask resume];
}
As you can see this currently is using NSNotificationCenter but is this the best solution? I've been reading about Delegates and blocks and it all just seems about loose. Should i implement AFNetworking inside the classes that needs it, like the class where i try to update my tableview?
Thanks :)
Extra code example
-(void)executePostForURL:(NSString *)url dictionary:(NSDictionary *)dict success:(SuccessBlock)success failure:(FailureBlock)failure{
[httpManager POST:url parameters:dict progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
//somehow i need to return [responseObject valueForKey:#"updateLabelString"];
}
failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
}
I'm trying to call this in viewdidload. This is of course just pseudo code and doesn't work, how do i parse the [responseObject valueForKey#"updateLabelString"] value into my labelToUpdate.text?
-(void)viewDidLoad{
NSDictionary *dicToSendToServer;
UILabel *labelToUpdate = #"temp text";
[apicon executePostForURL:#"serverurl" dictionary:dicToSendToServer success:^(NSString *test){
labelToUpdate.text = test;
}failure:nil];
}
I would declare it like this:
- (void)executePostForURL:(NSString *)url dictionary:(NSDictionary *)dict success:(void (^)(id objectYouRequested))success failure:(void (^)(NSError *error))failure;
I also like to use typedef to avoid some of the block syntax. I typically define the following:
typedef void (^SuccessBlock)(id result);
typedef void (^MySubclassedObjectSuccessBlock)(SubclassedObject *object);
typedef void (^FailureBlock)(NSError *error);
This then simplifies the method declaration above to:
- (void)executePostForURL:(NSString *)url dictionary:(NSDictionary *)dict success:(SuccessBlock)success failure:(FailureBlock)failure;
Idea
I'm building files download manager using AFNetworking and I'm using AFURLSessionManager class. the app is suppose to download mp3 files from the server.
I was concerned about memory consuming, so I'm trying to limit the number of simultaneous downloads to 1.
I know that there is a NSOperationQueue property in AFURLSessionManager called operationQueue and it's limited to 1 operation at a time by default.so I'm adding my NSURLSessionDownloadTask to operationQueue.
the problem
the code isn't working. files is being downloaded simultaneously instead of one after another.
the code
// 1. build sessionManager and prepare some vars
// note: by testing i found that it's better to init NSURLSessionConfiguration with backgroundSessionConfigurationWithIdentifier for memory issues
NSURLSessionConfiguration *conf = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"special_Identifier"];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:conf];
NSURL *urlDocs = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:NO
error:nil];
__block NSProgress *progress = Nil;
// 2. open sessionManager operation Queue and add this new download
[manager.operationQueue addOperationWithBlock:^{
// 2.1 init new download request
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:fileLink]];
// 2.2 creat a NSURLSessionDownloadTask
NSURLSessionDownloadTask *downloadTask = [self.downloadManager downloadTaskWithRequest:request progress:&progress
destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
return [urlDocs URLByAppendingPathComponent:fileName];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
if (!error) {
NSLog(#"done: %#", filePath);
}else{
NSLog(#"error %#",error);
}
}];
// 2.3 start downloading
[downloadTask resume];
// 2.4 track downloading progress using KVO
[progress addObserver:self
forKeyPath:NSStringFromSelector(#selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:(__bridge void *)(fileLink)];
}];
In AFNetworking 2 (and AFNetworking 3), you can init your AFHTTPSessionManager with an NSURLSessionConfiguration (use AFHTTPSessionManager initWithBaseURL:sessionConfiguration:). There you can specify the number of connections per host (HTTPMaximumConnectionsPerHost).
Sample:
NSURL *url = [NSURL URLWithString:#"myurl.net"];
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.defaultSessionConfiguration;
configuration.HTTPMaximumConnectionsPerHost = 1;
AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:url sessionConfiguration:sessionConfiguration];
Documentation:
AFHTTPSessionManager: http://cocoadocs.org/docsets/AFNetworking/3.0.4/Classes/AFHTTPSessionManager.html#//api/name/initWithBaseURL:sessionConfiguration:
NSURLSessionConfiguration: https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionConfiguration_class/#//apple_ref/occ/instp/NSURLSessionConfiguration/HTTPMaximumConnectionsPerHost
I'm facing a strong vs. autorelease problem :
I'm using an object which have a strong NSProgress to manage some file download.
For downloading, i'm using downloadtaskwithrequest from AFNetworking.
My problem is that this method take a NSProgress * __autoreleasing * which is not compatible with my strong NSProgress :
This is my object owning its NSProgress :
#interface MyDocument ()
#property(nonatomic, strong) NSProgress *progress;
#end
#implementation MyDocument ()
-(void)download
{
[myApiClient downloadFileWithUrl:_url progress:_progress]
}
#end
This is the SessionManager dealing with the download :
-(void)downloadFileFromUrl:(NSString*)url progress:(NSProgress * __strong *)progress
{
NSURLSessionDownloadTask *downloadTask = [self downloadTaskWithRequest:request
progress:progress
destination:^NSURL *(NSURL *targetPath, NSURLResponse *response)
{ ... }
completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error)
{ ... }];
}
This is the error concerning the line progress:progress :
Passing address of non-local object to __autoreleasing parameter for write-back
You need to pass the pointer to NSProgress object instead of passing object as parameter.
** means you have to pass the pointer to the pointer to an existing object.
[myApiClient downloadFileWithUrl:_url progress:&_progress];
You can find more details from this link
It's downloadTaskWithRequest who initialize the NSProgress object, so I cannot give it directly a NSProgress which is property of my object, i had to create another NSProgress object, and to update my property when needed :
-(void)downloadFileFromUrl:(NSString*)url progress:(NSProgress * __strong *)progress
{
NSProgress *localProgress = nil;
NSURLSessionDownloadTask *downloadTask = [self downloadTaskWithRequest:request
progress:localProgress
destination:^NSURL *(NSURL *targetPath, NSURLResponse *response)
{ ... }
completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error)
{ ... }];
// Update my property here :
*progress = localProgress;
}
While working with the AFNetworking library I am running into an issue where after downloading JSON data into a file using the AFURLSessionManager downloadTaskWithRequest's destination param code block asynchronously, I am wanting to perform the remaining operations asynchronously as well in its completionHandler block. The problem is the completionHandler block does not seem to run asynchronously.
Would there be a need to setup a new session manager and/or download task to accomplish this. Is there perhaps a better way to do this so the operations can be performed away from the main thread in the completionHandler block.
The reason for wanting to accomplish this is to avoid tying up the main thread in case there's a huge amount of data which needs to be assigned to the self.googleResults array or rather in a for loop using a custom class containing properties for specific key data which would eventually be added as elements to an array.
Here's the code so far...
- (void)viewDidLoad
{
[super viewDidLoad];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURL *url = [NSURL URLWithString:#"https://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response)
{
// NOTE: This code block runs asynchronously
NSURL *docPathURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
return [docPathURL URLByAppendingPathComponent:[response suggestedFilename]];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error)
{
// NOTE: This code block does not run asynchronously
// Would there be a need to create a new session and/or download task here to get the data from the filePath asynchronously?
// Or is there another way to this for the following code?
NSError *jsonSerializationErr;
NSData *jsonData = [NSData dataWithContentsOfURL:filePath];
NSDictionary *reponseDictionary = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonSerializationErr];
// self.googleResults is an instance of (NSArray *)
self.googleResults = [[reponseDictionary objectForKey:#"responseData"] objectForKey:#"results"];
NSLog(#"%#", self.googleResults);
}];
[downloadTask resume];
}