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;
}
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 had iOS framework which it send JSON to server using NSURLSessionDataTask like this :
NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
int responseStatusCode = [httpResponse statusCode];
if (responseStatusCode == 200)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate onJsonHttpResult:data andStatusResponse:responseStatusCode];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate onJsonHttpResult:nil andStatusResponse:responseStatusCode];
});
}
}];
[postDataTask resume];
but whenever I run it, [self.delegate onJsonHttpResult:nil andStatusResponse:responseStatusCode]; not called.
is there any way to get value outside NSURLSessionDataTask when it run inside the framework ?
Thanks
My suggestion is to create and use APIHelperClass with completionBlock.
That will be more easy and affective then use of custom Delegate as per my view.
To create it you can do as follow:
In APIHelperClass.h
#import <Foundation/Foundation.h>
#interface APIHelperClass : NSObject
+(void)apiCallSharedSessionPOST:(NSURLRequest *)request withCompletionHandlar:(void (^) (NSDictionary *dicResult,NSError *error, int status))completionBlock;
#end
And
APIHelperClass.m
#import "APIHelperClass.h"
#implementation APIHelperClass
+(void)apiCallSharedSessionPOST:(NSURLRequest *)request withCompletionHandlar:(void (^) (NSDictionary *dicResult,NSError *error, int status))completionBlock;
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
int responseStatusCode = (int)[httpResponse statusCode];
if (error!=nil)
{
completionBlock(nil,error,responseStatusCode);
[task suspend];
}
else
{
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
completionBlock(dic,error,responseStatusCode);
[task suspend];
}
}];
[task resume];
}
Then you can use that class for whole App and you don't need to create custom Delegates always.
Then Use that where you want Like :
NSURLRequest *request;
/*
Configure your Request Here
*/
[APIHelperClass apiCallSharedSessionPOST:request withCompletionHandlar:^(NSDictionary *dicResult, NSError *error, int status) {
}];
Thanks for help but I still need delegate to return my value outside framework.
I got issued that whenever I done with NSURLSessionDataTask, delegate is became null, I think it cause that delegate already released after I got response from NSURLSessionDataTask, So I tried to change #property delegate to strong and it work. I can return my value using delegate again. Thanks
I'm using blocks to get header fields from response in one class and I have to get that in another class.
I implemented code like this
In first class:
- (void)viewDidLoad {
[super viewDidLoad];
UserAuthentication *auth = [[UserAuthentication alloc]init];
NSDictionary *dict = [auth getUserConfiguration];
NSLog(#"%#",dict);
}
In userAuthentication class:
-(NSDictionary *)getUserConfiguration;
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
NSLog(#"%#",resultDictionary);
}
}] resume];
NSLog(#"%#",resultDictionary);
return resultDictionary;
}
Here my problem is in first class I'm getting dict as null.
Even in userAuthentication class also I'm getting null.
But after some time call back method is calling and then I can see the response correctly in completionHandler.
So how I can get response in firstClass?
You are misunderstanding the basic principle of async operation that runs in background thread and when the operation is completed it gives you data in completion block.
To get response in viewDidLoad Method of second class you need to use blocks. like below
-(void)getUserConfigurationOnCompletion:(void (^)(NSDictionary *))completion
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
// Call completion with parameter
completion(resultDictionary);
}
}] resume];
}
and use it like this in viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
UserAuthentication *auth = [[UserAuthentication alloc]init];
[auth getUserConfigurationOnCompletion:^(NSDictionary *dict){
// do necessary work with response dictionary here
NSLog(#"%#",dict);
}];
}
That's something you'll have to get used to: Anything that is related to internet access (and some things not related to it) cannot be returned immediately - unless you are willing to wait for it, block your user interface, and make your users very, very unhappy.
You have to write your application in such a way that it can be in four states: Never asked for the user configuration, asking for the user configuration, having asked for and received the user configuration, or having asked for the user configuration and failed. In this case your view must handle all four possibilities and must handle when the situation changes.
You are using NSURLSession! It performs tasks on a background thread!
Completion block is called only when you get the response from the server. Naturally it will take time to complete the request. You should use blocks to complete the request and return the result on completion.
-(void)getUserConfigurationAndOnCompletion:(void(ˆ)(NSDictionary *dict, NSError *error))completion;
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
NSLog(#"%#",resultDictionary);
//This will call the block in the first class with the result dictionary
dispatch_async(dispatch_get_main_queue(), ^{
if(!error){
completion(resultDictionary,nil);
}else{
completion(nil,error);
}
});
}] resume];
}
When you call the above code from your first class, it will create a block there and you will get the required dictionary over there in the block parameter!
Your method should be like,
-(void)getUserConfigurationwithCompletionHandler : (void (^)(NSDictionary* resultDictionary))completionHandler
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
NSLog(#"%#",resultDictionary);
completionHandler(resultDictionary);
}
}] resume];
NSLog(#"%#",resultDictionary);
}
and you can access it like,
- (void)viewDidLoad {
[super viewDidLoad];
[self getUserConfigurationwithCompletionHandler:^(NSDictionary *resultDictionary) {
// you can acess result dictionary here
NSLog(#"%#",resultDictionary);
}];
}
because you will getting data in response of webservice(from server) so it takes some time to complete so you have to return data from completion handler of webservice call and you can't return data from completion handler so you have to create own completion handler and call as i have mentioned above. you can access resultDictionary in completionHandler and you can show new VC from this completionHandler.
You have to call a method in your first class in your completionHandler.
Create a property of type YOURFIRSTCLASS *myfirstclass in your UserAuthentication Class.
Pass your firstclass with "self" to the UserAuthentication object.
create visible method in your firstclass "-(void)responseCaller:(NSDictionary)dict"
call the method in your response method
YOURFIRSTCLASS .h:
-(void)responseCaller:(NSDictionary)dict;
YOURFIRSTCLASS .m
-(void)responseCaller:(NSDictionary)dict
{NSLog(#"%#",dict);}
- (void)viewDidLoad {
[super viewDidLoad];
UserAuthentication *auth = [[UserAuthentication alloc]init];
auth.myfirstclass = self;
NSDictionary *dict = [auth getUserConfiguration];
NSLog(#"%#",dict);
}
UserAuthentication .h
#import "YOURFIRSTCLASS.h"
#property (nonatomic) *myfirstclass;
UserAuthentication .m
-(NSDictionary *)getUserConfiguration;
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
__weak myfirstclassSave = myfirstclass;
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
[myfirstclassSave responseCaller:resultDictionary ];
}
}] resume];
return resultDictionary;
}
Something like that
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;
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?