Edit to Question: 21/10/2013 20:10 GMT
Added how the method is called below and also the object that is leaking is "allDBObjects" If i remove this and change the dictionary below to "NSMutableDictionary *objectsById" there is no leak.
Edit to Question: 21/10/2013
After a few days not on this issue I have come back to it. I believe this is all down to "The Deallocation Problem" (https://developer.apple.com/library/ios/technotes/tn2109/_index.html#//apple_ref/doc/uid/DTS40010274-CH1-SUBSECTION11). I have tested with both MKNetworkKit and AFNetworking 1.3.3 (Changing 1 method to use AFNetworking instead of MKNetwork Kit) and am still getting these objects leaking in my completion block. I have no references to self within my block and using AFNetworking I can see the completionBlock is set to nil and I have tried to manually break the retain cycle by setting the [weakOp setCompletionBlock:nil].
EDIT: The code sample below I tried to use properties and reference them as weakSelf. I have now changed these to local variables and they still leak.
Any ideas?
Original Question
I have taken over a project using MKNetworkKit and Core Data, after running the project through Leaks in instruments I can see a lot of leaked objects in various places in the app.
After debugging the code I can see the objects that are leaking are 2 fetch requests that are happening in the callback of a MKNetworkKit request (setCompletionBlock:). The fetch requests need to be done to check whether the data needs to be inserted or updated.
Some further information. Inside the completion block I am getting an instance of the ManagedObjectContext and creating it with concurrency type of "NSPrivateQueueConcurrencyType" and to perform the insert I am correctly calling "performBlock:" on the moc.
Please advise.
James
Sample Code of Block:
Please note: I have commented out the 2 fetch requests are there are no leaks and putting them back in causes the leaks of hundreds of objects, also the weakSelf properties I am setting the NSDictionary and NSArray are (nonatomic, strong).
- (void) updateDbObjects: (int) page withCallback: (CompletionResultsNumberBlock) callback {
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
dispatch_queue_t callerQueue = dispatch_get_current_queue();
#pragma GCC diagnostic warning "-Wdeprecated-declarations"
__weak typeof(self) weakSelf = self;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[self createFullPath:urlStr]]];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSManagedObjectContext *moc = [weakSelf managedObjectContextForCurrentThread];
DataRoot *dataRoot = [DataRoot sharedInstanceInMoc:moc];
NSArray *returnJSON = JSON[#"object"];
__block int count = returnJSON.count;
if (!count)
{
dispatch_async(callerQueue, ^{
callback(0);
});
return;
}
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"DBObjects"];
NSError *error;
NSArray *allDBObjects = [moc executeFetchRequest:fetchRequest error:&error];
NSMutableDictionary *objectsById = [NSMutableDictionary dictionaryWithObjects:allTeamsArray forKeys:[allTeamsArray valueForKey: GoalTeamObjectAttributes.teamId]];
for (NSDictionary *rootDict in returnJSON)
{
GoalTeamObject *dbObject = objectsById[rootDict[#"id"]];
if (dbObject == nil)
{
dbObject = [DBObjects insertInManagedObjectContext:dataRoot.managedObjectContext];
}
[weakSelf importStandardParametersFrom:rootDict into:dbObject withPrefix:#""];
}
returnJSON = nil;
objectsById = nil;
[dataRoot saveContext];
NSError *childError = nil;
if ([moc save:&childError]) {
NSError *parentError = nil;
if (![moc.parentContext save:&parentError]) {
NSLog(#"Error saving parent");
}
dispatch_async(callerQueue, ^{
callback(count);
});
} else {
NSLog(#"Error saving child");
}
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
}];
[operation start];
}
This is how this code is called: It is called recursively in a loop as there are many pages of data.
__block int page = 1;
__weak typeof(self) weakSelf = self;
CompletionResultsNumberBlock loadData;
__block CompletionResultsNumberBlock block_loadData = loadData = ^(int results)
{
if (results < 100)
{
dispatch_async(callerQueue, callback);
} else {
[weakSelf updateDbObjects:++page withCallback:block_loadData];
}
};
[self updateDbObjects:page withCallback: loadData];
This doesn't look right:
__block CompletionResultsNumberBlock block_loadData = loadData = ^...
Under ARC, the block would hold a strong reference to itself. Under ARC, you should do:
__block __weak CompletionResultsNumberBlock block_loadData = loadData = ^...
You use weakSelf in some places within the block, and self in others. That means that the block will still capture self in a strong fashion.
I'd try replacing all references to self with weakSelf within your block first.
Related
I want try to understand block capture logic and now I have question about it. I have MeRequest and NSNumber properties.
#property (nonatomic) MeRequest *request;
#property (nonatomic) NSNumber *number;
Then, in viewDidLoad i call request method
self.request = [[MeRequest alloc] init];
[self.request meInfoSuccessBlock:^(NSDictionary *response) {
} failureBlock:^(Error *error) {
self.number = #5;
}];
- (void)meInfoSuccessBlock:(RequestSuccessBlock)success failureBlock:(RequestFailureBlock)failure {
self.method = #"GET";
self.parameters = #{};
[self performWithCompletion:^(id responseObject) {
NSDictionary *response = (NSDictionary *)responseObject;
if (success) {
success(response);
}
} onFailure:^(Error *error) {
if (failure) {
failure(error);
}
}];
}
- (AFHTTPRequestOperation *)performWithCompletion:(void(^)(id responseObject))completion
onFailure:(void(^)(Error *error))failure {
NSURLRequest *request = [[NetworkManager sharedManager] requestWithMethod:self.method path:self.path parameters:self.parameters];
if (_operation) {
[_operation cancel];
}
_operation = [[NetworkManager sharedManager] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
_operation = nil;
dispatch_semaphore_signal(_semaphore);
if (completion) {
completion(responseObject);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
_operation = nil;
dispatch_semaphore_signal(_semaphore);
if (failure) {
failure(_error);
}
}];
[_operation start];
return _operation;
}
And in failureBlock I set number to property. When I leave this controller I see dealloc message in console, that controller has been dealloced.
- (void)dealloc {
NSLog(#"%s", __PRETTY_FUNCTION__);
}
Why controller deallocs? I don't use weak reference to self
To know definitively, you'd have to post the implementation of the MeRequest class.
Without knowing that, this is an educated guess.
The blocks passed into self.request via meInfoSuccessBlock:failureBlock: may be nil'd out when the transaction is complete. That is, it may be something like:
- (void)meInfoSuccessBlock:... sB failureBlock:... fB {
_sB = sB; // put ref in ivar
_fB = fB; // because this is probably broken up across methods
dispatch_async(_queue, ^{
.... think hard ...
if (success) _sB(...);
else _fB(...);
_sB = nil;
_fB = nil;
};
}
So, first, you aren't creating a direct cyclic reference, but -- maybe -- a cyclic reference of self -> request -> _sB -> self. And, secondly, by assigning _sB = nil after computation is done and the callback is made, the cycle is broken.
Or, in your case, you have strong references to the blocks that only survive the scope. I.e. kinda like this:
- (void)meInfoSuccessBlock:... sB failureBlock:... fB {
dispatch_async(_queue, ^{
.... think hard ...
if (success) sB(...);
else fB(...);
// when the block finishes execution, fB and sB will be released
};
// when execution gets to here, the block above is the only strong references to sB and fB
}
That is, while you have a retain cycle, one reference in that cycle is explicitly tied to the lifespan of the callback blocks and since those only survive until the callback is complete, they get destroyed and that destroys the cycle.
I am quite new to Objective-C & have to dynamically change the value of #property (strong, nonatomic) NSMutableArray *allCategories from inside of AFHTTPRequestOperationManager in success block.
[self.allCategories addObject:tempObject]; doesn't change the value of allCategories while iterating in a loop.
The variable has been initialized as self.allCategories = [[NSMutableArray alloc]init]; in viewDidLoad.
I have also tried creating a temporary variable as __block NSMutableArray *tempCategories = [[NSMutableArray alloc]init]; before initiating AFHTTPRequestOperationManager object. tempCategories doesn't even retain its value.Can't figure out what's happening.EditSorry for inconvenienceviewDidLoad has the following code self.allCategories = [[NSMutableArray alloc]init];[self loadData];Here's the code
-(NSMutableArray *)loadData
{
__block NSMutableArray *tempCategories = [[NSMutableArray alloc]init];
manager = [AFHTTPRequestOperationManager manager];
[manager GET:kAPICategoryList
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// downcast id to NSMutableDictionary
NSMutableDictionary *json = (NSMutableDictionary *)responseObject;
// check if dictionary is non nil has at least 1 element
if (json != nil && [json count] >= 1) {
// NSLog(#"json:\t%#", json);
// check json is non nil & has success message
if ([json objectForKey:kAPIKeyCategoryRoot] != nil) {
NSArray *arrCategoriesRoot = [json objectForKey:kAPIKeyCategoryRoot];
// check categories has some data
if (arrCategoriesRoot.count >= 1) {
for (int i = 0; i < arrCategoriesRoot.count; i++) {
SomeModel *pCategory;
NSDictionary *dctCategorySingle = [arrCategoriesRoot objectAtIndex:i];
// check category has sub category
if ([dctCategorySingle objectForKey:kAPIKeyCategorySubCategory] != nil) {
// create category with sub category
pCategory = [[SomeModel alloc]initWithSubCategorisedCategoryID:[dctCategorySingle objectForKey:kAPIKeyCategoryID]
name:[dctCategorySingle objectForKey:kAPIKeyCategoryName]
image:kIMGCategoryDefault
subCategory:[dctCategorySingle objectForKey:kAPIKeyCategorySubCategory]];
} else{
// create just a category
pCategory = [[SomeModel alloc]initWithCategoryID:[dctCategorySingle objectForKey:kAPIKeyCategoryID]
name:[dctCategorySingle objectForKey:kAPIKeyCategoryName]
image:kIMGCategoryDefault];
} // else just
[tempCategories addObject:pCategory];
[_allCategories addObject:pCategory];
} // for
NSLog(#"categories count %lu", [self.allCategories count]);
} // if count >= 1
}
else if ([json objectForKey:kAPIRespMsgCategoryFetchErrKey] != nil) {
[Utility showAlertWithTitle:kAPIRespMsgCategoryFetchErrKey
message:[json objectForKey:kAPIRespMsgCategoryFetchErrVal]
button:kMsgButtonOkayTtl];
}
} else {
// error in login => enable login
NSLog(#"%#", kMsgNetworkEmptyJSON);
}
}
// network error
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"error %#", [error localizedDescription]);
}];
NSLog(#"tempCategories count %lu", [tempCategories count]);
return tempCategories;
}
Here's the output form NSLog:2015-03-19 18:27:17.845 MyProject[4011:121268] viewDidLoad
2015-03-19 18:27:18.133 MyProject[4011:121268] tempCategories count 0
2015-03-19 18:27:18.136 MyProject[4011:121268] numberOfRowsInSection count 0
2015-03-19 18:27:18.137 MyProject[4011:121268] numberOfRowsInSection count 0
2015-03-19 18:27:19.019 MyProject[4011:121268] categories count 20when loadData finishes allCategories has not data in it (nil).
As far as I know it should work that way.. are you sure your success block is being called before you check the content of allCategories?
A success block work asynchronously, which means it will be executed only when the RequestOperationis completed (which can take a long time if you're downloading something big)
If you are trying to get the value of allCategories before the success block is executed you won't get what you're expecting. I would recommend using breakpoints or NSLog on your success block to see if it's been executed when you think it's doing it.
e.g
...
successBlock:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"Success");
[self.allCategories addObject:tempObject]
}]; //End of request
[operation start]; //Begin executing the AFHTTPOperation
NSLog("%#",self.allCategories.description); //probably nil or empty
//since the success block hasn't been called yet
EDIT:
As I though, you are returning a value before is been set by the async operation, to return a value from an async operation I would suggest take a look to this answer and this one . Also you should read a bit of how async task work.
Basically what you want to do with async operations/tasks is make sure the value will be available when you want to use it. The main issue with that is that you don't know when the value will be set, but you can make sure what you want to do whenever it's set.
To do that you can create a simple method with a custom completion block
- (void)myCustomMethodWithCompletionBlock: (void (^)(NSArray *))completion {
//Do your request
//...
successBlock:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"Success");
completionBlock(allCategories);
}]; //End of request
}
Meanwhile in your main method you call
[self myCustomMethodWithCompletionBlock:^(NSArray *allCategories) {
self.allCategories = allCategories;
//Do other stuff you need to with that variable since now you are
//sure the value will be set unless the operation failed
}];
I had the same problem a few days ago. My problem was my array seems nil, array allocations in viewdidload method may be your request run before viewDidLoad. Check it with debug if you see the array is nill then alloc array different place.
P.S: I m not expert but may be it's the same problem with me.
Try this:
dispatch_async(dispatch_get_main_queue(), ^{
[self.allCategories addObject:tempObject];
});
Define NSMutableArray with following line.
#property (nonatomic, strong) NSMutableArray * arrData;
initializein viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
self.arrData = [NSMutableArray array];
}
call following method with any UIButton action for see output OR working behavior
- (void) TestMethod {
dispatch_queue_t queue = dispatch_queue_create("myQueue", 0);
dispatch_async(queue, ^{
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL urlWithEncoding:#"https://www.google.co.in/?gws_rd=ssl"]];
[httpClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[httpClient setDefaultHeader:#"Accept" value:#"application/json"];
[httpClient setParameterEncoding:AFJSONParameterEncoding];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"" parameters:nil];
[request setTimeoutInterval:180];
[AFJSONRequestOperation addAcceptableContentTypes:[NSSet setWithObject:#"text/html"]];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
[self.arrData addObject:[NSDictionary dictionaryWithObjectsAndKeys:#"test",#"t3da",#"adsf",#"afds", nil]];
dispatch_semaphore_signal(sema);
} failure:^ (NSURLRequest *request, NSURLResponse *response, NSError *error, id json){
[self.arrData addObject:[NSDictionary dictionaryWithObjectsAndKeys:#"test",#"t3da",#"adsf",#"afds", nil]];
dispatch_semaphore_signal(sema);
}];
[operation start];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
DLog(#"arrData = %#",self.arrData);
});
}
Creating first app with webservices, I am using AFNetworking for webservices. Everything is working fine but i have no idea , that how to fetch data out from block which i am getting in response. This is what i have done so far
+(WebServices *)sharedManager{
static WebServices *managerServices = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
managerServices = [[self alloc] init];
});
return managerServices;
}
-(NSArray *)firstPostService{
//1
NSURL *url = [NSURL URLWithString:BaseURLString];
//2
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSDictionary *param = #{#"request" : #"get_pull_down_menu" , #"data" : #"0,0,3,1"};
[manager POST:#"person.php" parameters:param success:^(NSURLSessionDataTask *task, id responseObject) {
[self methodUsingJsonFromSuccessBlock:responseObject];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
UIAlertView *av = [[UIAlertView alloc] initWithTitle:#"Error retrieving data" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil];
[av show];
}];
if (list.count == 0) {
NSLog(#"Nothing in array yet!!");
}
else{
NSLog(#"Object 1 is : %#", [list objectAtIndex:1]);
}
return list;
}
- (void)methodUsingJsonFromSuccessBlock:(id)json {
// use the json
NSString *string = [NSString stringWithUTF8String:[json bytes]];
NSLog(#"This is data : %#", string);
list = [string componentsSeparatedByString:#"\n"];
NSLog(#"After sepration first object: %#", [list objectAtIndex:1]);
//NSLog(#"json from the block : %#", json);
}
What i understand reading from different blogs and tuts, that block is a separate thread and what every i do finishes with it. I read some where that this is normally use for it
dispatch_async(dispatch_get_main_queue(), ^{
data = [string componentsSeparatedByString:#"\n"];
//WHERE DATA IS __block NSArray * data = [[NSArray alloc] init];
});
and i was returning it in the of the function(firstPostService) but nothing happen. i still get an empty array outside the block. Kindly help me , suggest me some good reading stuff. Thanking you all in advance.
You say:
I need this data to my view controller i am trying to return in dispatch part but it is not allowing. Is it possible to get data into my viewcontroller class ?
Yes, it's possible. But, no, firstPostService should not return the results. It can't because it returns immediately, but the POST completion blocks won't be called until much later. There's nothing to return by the time firstPostService returns.
At the end of your original question, you said:
What i understand reading from different blogs and tuts, that block is a separate thread and what every i do finishes with it. I read some where that this is normally use for it
dispatch_async(dispatch_get_main_queue(), ^{
data = [string componentsSeparatedByString:#"\n"];
//WHERE DATA IS __block NSArray * data = [[NSArray alloc] init];
});
This is not the appropriate pattern of __block local variable. You generally use that __block pattern when dealing with some block that runs synchronously (for example the block of an enumeration method). But while you can use __block variable with asynchronous block, you almost never do (and it doesn't quite make sense to even try to do it). When you use appropriate completion block patterns, there's no need for any __block variable.
So, let's go back to your original code sample: So, you should take a page from AFNetworking and employ completion blocks yourself. When the AFNetworking POST method wanted to return data to your code asynchonously, it used a completion block pattern, instead. Thus, if your own firstPostService wants to pass back data asynchronously, it should do the same.
For example:
#interface WebServices ()
#property (nonatomic, strong) AFHTTPSessionManager *manager;
#end
#implementation WebServices
// note, use `instancetype` rather than actually referring to WebServices
// in the `sharedManager` method
+ (instancetype)sharedManager
{
static id sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
// I'd also suggest that you init the `AFHTTPSessionManager` only once when this
// object is first instantiated, rather than doing it when `firstPostService` is
// called
- (instancetype)init
{
self = [super init];
if (self) {
NSURL *url = [NSURL URLWithString:BaseURLString];
self.manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
}
return self;
}
// Notice:
//
// 1. This now has a return type of `void`, because when it instantly returns,
// there is no data to return.
//
// 2. In order to pass the data back, we use the "completion handler" pattern.
- (void)firstPostServiceWithCompletionHandler:(void (^)(NSArray *list, NSError *error))completionHandler {
NSDictionary *param = #{#"request" : #"get_pull_down_menu" , #"data" : #"0,0,3,1"};
[self.manager POST:#"person.php" parameters:param success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *list = [self methodUsingJsonFromSuccessBlock:responseObject];
if (completionHandler) {
completionHandler(list, nil);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[[[UIAlertView alloc] initWithTitle:#"Error retrieving data" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil] show];
if (completionHandler) {
completionHandler(nil, error);
}
}];
// // none of this code belongs here!!! You are dealing with asynchronous methods.
// // the `list` has not been returned by the time you get here!!! You shouldn't even
// // be using instance variable anyway!
//
// if (list.count == 0) {
//
// NSLog(#"Nothing in array yet!!");
// }
// else{
// NSLog(#"Object 1 is : %#", [list objectAtIndex:1]);
//
// }
// return list;
}
- (NSArray *)methodUsingJsonFromSuccessBlock:(NSData *)data {
// note, do not use `stringWithUTF8String` with the `bytes` of the `NSData`
// this is the right way to convert `NSData` to `NSString`:
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"This is string representation of the data : %#", string);
// Note, retire the `list` instance variable, and instead use a local variable
NSArray *list = [string componentsSeparatedByString:#"\n"];
NSLog(#"After sepration first object: %#", [list objectAtIndex:1]);
return list;
}
#end
Then, you could invoke that like so:
[[WebServices sharedManager] firstPostServiceWithCompletionHandler:^(NSArray *list, NSError *error) {
if (error) {
// handle the error here
} else {
// use the `list` results here
}
}];
// NOTE, DO NOT USE `list` HERE. By the time you get here, `list` has not been
// returned. Only use it in the above block.
//
// In fact, you can see that if you put a `NSLog` or breakpoint here, and again, above
// where it says "use the `list` results` here", you'll see that it's running the code
// inside that block _after_ this code down here!
I'd suggest you tackle the above first, to first make sure you completely understand the proper asynchronous technique of the completion block pattern. We don't want to complicate things quite yet. Make sure you're getting the sort of data you wanted before you proceed to what I will describe below.
But, once you've grokked the above, it's time to look at your JSON parsing. You make several reference to JSON, but if that's what it really is, then using componentsSeparatedByString is not the right way to parse it. You should use NSJSONSerialization. Or even better, you can let AFNetworking do that for you (right now, you're making it more complicated than it needs to be and your results will not be formatted correctly).
Above, I kept your methodUsingJsonFromSuccessBlock in the process, but if you're really dealing with JSON, you should eliminate that method entirely. Let AFNetworking do this for you.
You should eliminate the line that says:
responseSerializer = [AFHTTPResponseSerializer serializer];
The default serializer is AFJSONResponseSerializer which is what you want to use if handling JSON requests.
The methodUsingJsonFromSuccessBlock is then no longer needed because AFNetworking will do the JSON conversion for you. So firstPostServiceWithCompletionHandler should look like:
- (void)firstPostServiceWithCompletionHandler:(void (^)(NSArray *list, NSError *error))completionHandler {
NSDictionary *param = #{#"request" : #"get_pull_down_menu" , #"data" : #"0,0,3,1"};
[self.manager POST:#"person.php" parameters:param success:^(NSURLSessionDataTask *task, id responseObject) {
if (completionHandler) {
completionHandler(responseObject, nil);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[[[UIAlertView alloc] initWithTitle:#"Error retrieving data" message:[error localizedDescription] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil] show];
if (completionHandler) {
completionHandler(nil, error);
}
}];
}
i am new to iOS programming, still learning.
EDIT: !!!!!! Everything in my code works. My question is about the delegation pattern i use,
if i am generating problems in the background that i have no idea of, or if there is a better way to handle my situation in AFNetworking...
I have created an API for my app by subclassing AFHTTPSessionManager.
My API creates a singleton and returns it and supplies public functions for various requests. And those functions create parameter lists, and make GET requests on the server like this:
- (void)getCharacterListForKeyID:(NSString *)keyID vCode:(NSString *)vCode sender:(id)delegate
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[#"keyID"] = keyID;
parameters[#"vCode"] = vCode;
[self GET:#"account/Characters.xml.aspx" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
self.xmlWholeData = [NSMutableDictionary dictionary];
self.errorDictionary = [NSMutableDictionary dictionary];
NSXMLParser *XMLParser = (NSXMLParser *)responseObject;
[XMLParser setShouldProcessNamespaces:YES];
XMLParser.delegate = self;
[XMLParser parse];
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didHTTPRequestWithResult:)]) {
[delegate EVEAPIHTTPClient:self didHTTPRequestWithResult:self.xmlWholeData];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didFailWithError:)]) {
[delegate EVEAPIHTTPClient:self didFailWithError:error];
}
}];
}
I was using a normal protocol/delegate method earlier. But once i make calls this API more than once like this: (IT WAS LIKE THIS:)
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
client.delegate = self;
[client getCharacterListForKeyID:self.keyID vCode:self.vCode];
Previous call's delegate was being overwritten by next. So i changed to above style. Passing sender as an argument in the function:
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
[client getCharacterListForKeyID:self.keyID vCode:self.vCode sender:self];
And i pass this sender to GET request's success and failure blocks.
What i wonder is : "Is this a good programming practice ?". Passing objects to blocks like this should be avoided if possible ? Is there any other more elegant way in AFHTTPSessionManager to handle this type of work (making same GET request over and over with different parameters and returning results to the respective request owners) more elegantly ?
Delegation pattern falters when it comes to simplicity and asynchronous request processing. You should be using blocks, here's an example
Your server class:
static NSString *const kNews = #"user_news/"; // somewhere above the #implementation
- (NSURLSessionDataTask *)newsWithPage:(NSNumber *)page
lastNewsID:(NSNumber *)lastNewsID
completion:(void (^)(NSString *errMsg, NSArray *news, NSNumber *nextPage))completionBlock {
return [self GET:kNews
parameters:#{#"page" : page,
#"news_id" : lastNewsID
}
success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *news = nil;
NSNumber *nextPage = nil;
NSString *errors = [self errors:responseObject[#"errors"]]; // process errors
if ([responseObject[#"status"] boolValue]) {
news = responseObject[#"news"];
nextPage = responseObject[#"next_page"];
[self assignToken];
}
completionBlock(errors, news, nextPage);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSString *errors = [self errors:error];
completionBlock(errors, nil, nil);
}];
}
The caller
- (void)dealloc {
[_task cancel]; // you don't want this task to execute if user suddenly removes your controller from the navigation controller's stack
}
- (void)requestNews {
typeof(self) __weak wself = self; // to avoid the retain cycle
self.task = [[GSGServer sharedInstance] newsWithPage:self.page
lastNewsID:self.lastNewsID
completion:^(NSString *errMsg, NSArray *news, NSNumber *nextPage) {
if (errMsg) {
[GSGAppDelegate alertQuick:errMsg]; // shortcut for posting UIAlertView, uses errMsg for message and "Error" as a title
return;
}
[wself.news addObjectsFromArray:news];
wself.lastNewsID = [wself.news firstObject][#"id"];
wself.page = nextPage;
[wself.tableView reloadData];
}];
}
I have an application that retrieves json (employees workschedules) from a web service using AFNetworking and displays them in a table view.
I have my webservice class that takes care of doing the request and once it is done, it stores these data into coredata (I have an another issue here, being that I use magicalRecord and the data does not persist, and I don't understand why) and then calls back its delegate (my tableViewController) telling it it's done, so this can load the workschedules into the cells.
WebServiceClient.m
NSURL *url = [NSURL URLWithString:stringUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
NSArray *workSchedules = [[[NSSet alloc] initWithArray:JSON] allObjects];
NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
Workschedule *workscheduleEntity = nil;
NSError *error = nil;
for (NSDictionary *web_workschedule in workSchedules)
{//Inside this method I create other entities that will hydrate my workschedule entity, and it is done using the MR_CreateInContext
workscheduleEntity = [Workschedule workScheduleFromJSONDictionary:web_workschedule withError:&error];
[context MR_save];
}
if([self.delegate respondsToSelector:#selector(workSchedules)]){
[self.delegate workSchedules];
}
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
LOG_ERROR(2,#"Received an HTTTP %d:", response.statusCode);
LOG_ERROR(2,#"The error was: %#", error);
if([self.delegate respondsToSelector:#selector(workSchedules:)]){
[self.delegate workSchedules:nil];//return error
}}];
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperation:operation];
}
PendingWorkscheduleViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
[self.webServiceClient getMockedWorkSchedulesForEmployee:[NSNumber numberWithInt:1]];
[self workSchedules];
}
-(void)workSchedules
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"pending == YES"];
NSArray *pendingWorkSchedules = [Workschedule MR_findAllWithPredicate:predicate];
self.pendingWorkSchedules = pendingWorkSchedules;
[self.tableView reloadData];
}
My problem is that when i run this while the request is processed the UI is unresponsive (it's a very brief time, but if the request were to increase...) so that if i load the table view and right away try to scroll or click the back button, it just ignores it as it is "frozen". This behavior is on my iphone 4s. On the simulator this works fine and I can't wrap my head around why is that. I tried to call the "[self.webServiceClient getMockedWorkSchedulesForEmployee:[NSNumber numberWithInt:1]];" in a queue using GCD, I tried using performSelectorInBackground: WithObject: etc but still the same (even though with this last method it seemed a little more efficient, but it's an impression and only on the simulator, no changes on the device).
As far as magicalRecord goes I will make separate question.
I would appreciate your help.
Fixed it. The problem is that the success block run on the main thread! (which I did not understand). I just used GCD in the success block with a background queue for processing the data and the main queue to store this data in core data.
As far as magical record issue, i needed to save "nestedContext".
Cheers everyone.