AFNetworking Asynchronous Data Fetching - objective-c

I'm using the AFNetworking library to pull a JSON feed from a server to populate a UIPickerView, but I'm having a little trouble wrapping my head around the asynchronous way of doing things. The #property classChoices is an NSArray that's being used to populate the UIPickerView, so that the web call is only performed once. However, since the block isn't finished by the time the instance variable is returned, the getter returns nil, and it eventually causes my program to crash later on. Any help in fixing this would be greatly appreciated. Let me know if you need any additional information.
PickerViewController.m classChoices Getter
- (NSArray *)classChoices {
if (!_classChoices) {
// self.brain here refers to code for the SignUpPickerBrain below
[self.brain classChoicesForSignUpWithBlock:^(NSArray *classChoices) {
_classChoices = classChoices;
}];
}
return _classChoices;
}
SignUpPickerBrain.m
- (NSArray *)classChoicesForSignUpWithBlock:(void (^)(NSArray *classChoices))block {
[[UloopAPIClient sharedClient] getPath:#"mobClass.php" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseJSON) {
NSLog(responseJSON);
if (block) {
block(responseJSON);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
if (block) {
block(nil);
}
}];
}

You need a method like the following in your PickerViewController which returns the array once it has been downloaded. Once the callback has been returned, you can then continue on with your code:
- (void)classChoices:(void (^) (NSArray * classChoices)) _callback {
if (!self.classChoices) {
// self.brain here refers to code for the SignUpPickerBrain below
[self.brain classChoicesForSignUpWithBlock:^(NSArray *classChoices) {
_callback(classChoices);
}];
}
}
// call the method
- (void) viewDidLoad {
[super viewDidLoad];
[self classChoices:^(NSArray * updatedChoices) {
self.classChoices = updatedChoices;
[self.pickerView reloadAllComponents];
}];
}

Related

Snapchat's SnapKit addLoginStatusObserver not listening to events

I'm currently developing a React Native plugin for Snapchat's SnapKit SDK.
I can't seem to get the addLoginStatusObserver method to work (detailed here: https://snapkit.com/docs/api/ios/) and I suspect it's my lack of experience with Objective C's protocol/interface/implementation features.
Here's a trimmed down version of the code:
...
#interface RNSnapSDKListener : NSObject<SCSDKLoginStatusObserver> {
...
}
- (void)scsdkLoginLinkDidSucceed;
- (void)scsdkLoginLinkDidFail;
- (void)scsdkLoginDidUnlink;
...
#end
#implementation RNSnapSDKListener
- (void)scsdkLoginLinkDidSucceed{
NSLog(#"[RNSnapSDKListener] Snapchat Did Login!");
}
- (void)scsdkLoginLinkDidFail{
NSLog(#"[RNSnapSDKListener] Snapchat Did Fail!");
}
- (void)scsdkLoginDidUnlink{
NSLog(#"[RNSnapSDKListener] Snapchat Did Unlink!");
}
- (void)setDelegate: (RCTEventEmitter*) eventEmitter{
NSLog(#"[RNSnapSDKListener] Delegate Set!");
}
#end
#implementation RNSnapSDK
...
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(initialize){
RNSnapSDKListener *listener = [[RNSnapSDKListener alloc] init];
[listener setDelegate:self];
[SCSDKLoginClient addLoginStatusObserver:listener];
}
RCT_EXPORT_METHOD(login)
{
[SCSDKLoginClient loginFromViewController:[UIApplication sharedApplication].delegate.window.rootViewController completion:^(BOOL success, NSError * _Nullable error) {
}];
}
RCT_EXPORT_METHOD(logout: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
[SCSDKLoginClient unlinkAllSessionsWithCompletion:^(BOOL success) {
NSLog(#"Logout %s", success ? "true" : "false");
resolve(NULL);
}];
}
RCT_EXPORT_METHOD(getUserData: (RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
{
NSString *graphQLQuery = #"{me{externalId, displayName, bitmoji{avatar}}}";
NSDictionary *variables = #{#"page": #"bitmoji"};
[SCSDKLoginClient fetchUserDataWithQuery:graphQLQuery
variables:variables
success:^(NSDictionary *resources) {
NSDictionary *data = resources[#"data"];
resolve(data);
} failure:^(NSError * error, BOOL isUserLoggedOut) {
NSLog(#"%#",[error localizedDescription]);
NSLog(#" %s", isUserLoggedOut ? "true" : "false");
if(isUserLoggedOut){
[SCSDKLoginClient loginFromViewController:[UIApplication sharedApplication].delegate.window.rootViewController completion:^(BOOL success, NSError * _Nullable error) {
}];
}else{
reject(#"error", [error localizedDescription], error);
}
}];
}
NSURL *saved;
RCT_EXPORT_METHOD(authenticateDeepLink: (NSString *)url)
{
NSURL *finalUrl = [NSURL URLWithString:url];
saved = finalUrl;
[SCSDKLoginClient application:[UIApplication sharedApplication] openURL:finalUrl options:[NSMutableDictionary dictionary]];
}
...
#end
.initialize() is called inside the React Native module, and the setDelegate() method is called successfully (printing out "Delegate set" - this is for the react-native event bridge), but the other [RNSnapSDKListener]s dont print when they should (after logging in or logging out)
Is this something I'm doing wrong with objective-c or some other misuse of Snapchat's SDK?
Thanks!
The problem ended up being that the RNSnapSDKListener *listener needed to be declared as a global variable and initialized inside initialize() - not entirely sure why though - something with garbage collection maybe?

Prevent warning sheet after NSPersistentDocument rename

Whenever my document is renamed , autosaving is blocked and the first save after the rename presents the pictured message.
Technically its not an issue as either button takes the user back to a autosavable state but it is confusing for my users.
I have tried hooking the method
-(void)moveToURL:(NSURL *)url completionHandler:(void (^)(NSError *))completionHandler
{
void(^takeoverblock)(NSError *error) = ^(NSError *error){
if (completionHandler) {
completionHandler(error);
}
if (!error) {
[self updateChangeCountWithToken:[self changeCountTokenForSaveOperation:NSAutosaveInPlaceOperation] forSaveOperation:NSAutosaveInPlaceOperation];
}
};
[super moveToURL:url completionHandler:takeoverblock];
}
and using various flavours of updateChangeCount: and updateChangeCountWithToken: but the warning consistently appears.
How do I put the document in a state where it resumes standard autosave behaviour after a rename/move.?
The answer via a friendly Apple engineer is this appears when the modificationDate on the underlying sqlite file is different to the fileModificationDate property on the NSPersistentDocument instance so to resolve reset the fileModificationDate after the move
override moveToUrl: like this
-(NSDate *)modDateForURL:(NSURL *)url
{
NSDictionary *dict = [[NSFileManager defaultManager] attributesOfItemAtPath:[url path] error:NULL];
return dict[NSFileModificationDate];
}
-(void)moveToURL:(NSURL *)url completionHandler:(void (^)(NSError *))completionHandler
{
void(^takeoverblock)(NSError *error) = ^(NSError *error){
if (completionHandler) {
completionHandler(error);
}
if (!error) {
self.fileModificationDate = [self modDateForURL:self.fileURL];
}
};
[super moveToURL:url completionHandler:takeoverblock];
}

Assigning data from an anonymous handler ios

I'm trying to get a reference of an NSArray that gets passed when I call sendAsyncrhonousRequest. Once I have that NSArray, I'd like to assign it to a class attribute but it seems I can't do that.
#implementation BarTableViewController {
NSArray *_jsonArray;
}
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
if (statusCode == 200 && data.length > 0 && error == nil)
{
NSError *e = nil;
NSArray *jsonArray = [NSJSONSerialization JSONObjectWithData: data options: NSJSONReadingMutableContainers error: &e];
if (!jsonArray) {
NSLog(#"Error parsing JSON: %#", e);
} else {
_jsonArray = jsonArray; // this doesn't work? _jsonArray is at the class level
}
}
else if (error)
{
NSLog(#"HTTP Status: %ld", (long)statusCode);
}
else if (statusCode != 200)
{
NSLog(#"HTTP Status: %ld", (long)statusCode);
}
}];
If I traverse jsonArray it will correctly display the data. If I assign it to _jsonArray to use it later, it no longer returns any data. The count of the array is zero.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _jsonArray.count; // always returns zero
}
How can I assign jsonArray to a class attribute so that I can use that data later?
My theory was correct. The UI was loading before the request finished. What I did to fix this issue was to call
[self.tableView reloadData];
Inside the async request.
Forget simple assignment:
#implementation BarTableViewController {
// NSArray *_jsonArray; forget it
}
instead own the object.
#interface BarTableViewController()
#property(nonatomic, strong)NSArray *jsonArray;
#end
/* --------- */
#implementation BarTableViewController
#syntethise jsonArray = _jsonArray;
#end
then to make it own it
self.jsonArray = jsonArray; // will call synthesized setter
I'm assuming you are using ARC, because you get a nil value instead of a dangling pointer. Now you will get a valid jsonArray.

AFNetworking Synchronous Operation in NSOperationQueue on iPhone

My app is working this way :
- create an album and take pictures
- send them on my server
- get an answer / complementary information after picture analysis.
I have some issue with the sending part. Here is the code
#interface SyncAgent : NSObject <SyncTaskDelegate>
#property NSOperationQueue* _queue;
-(void)launchTasks;
#end
#implementation SyncAgent
#synthesize _queue;
- (id)init
{
self = [super init];
if (self) {
self._queue = [[NSOperationQueue alloc] init];
[self._queue setMaxConcurrentOperationCount:1];
}
return self;
}
-(void) launchTasks {
NSMutableArray *tasks = [DataBase getPendingTasks];
for(Task *t in tasks) {
[self._queue addOperation:[[SyncTask alloc] initWithTask:t]];
}
}
#end
and the SyncTask :
#interface SyncTask : NSOperation
#property (strong, atomic) Task *_task;
-(id)initWithTask:(Task *)task;
-(void)mainNewID;
-(void)mainUploadNextPhoto:(NSNumber*)photoSetID;
#end
#implementation SyncTask
#synthesize _task;
-(id)initWithTask:(Task *)task {
if(self = [super init]) {
self._task = task;
}
return self;
}
-(void)main {
NSLog(#"Starting task : %#", [self._task description]);
// checking if everything is ready, sending delegates a message etc
[self mainNewID];
}
-(void)mainNewID {
__block SyncTask *safeSelf = self;
[[WebAPI sharedClient] createNewPhotoSet withErrorBlock:^{
NSLog(#"PhotoSet creation : error")
} andSuccessBlock:^(NSNumber *photoSetID) {
NSLog(#"Photoset creation : id is %d", [photoSetID intValue]);
[safeSelf mainUploadNextPhoto:photoSetID];
}];
}
-(void)mainUploadNextPhoto:(NSNumber*) photoSetID {
//just admit we have it. won't explain here how it's done
NSString *photoPath;
__block SyncTask *safeSelf = self;
[[WebAPI sharedClient] uploadToPhotosetID:photoSetID withPhotoPath:photoPath andErrorBlock:^(NSString *photoPath) {
NSLog(#"Photo upload error : %#", photoPath);
} andSuccessBlock:^(NSString *photoPath) {
NSLog(#"Photo upload ok : %#", photoPath);
//then we delete the file
[safeSelf mainUploadNextPhoto:photoSetID];
}];
}
#end
Every network operations are done using AFNetworking this way :
-(void)myDummyDownload:(void (^)(NSData * data))successBlock
{
AFHTTPClient* _httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:#"http://www.google.com/"]];
[_httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
NSMutableURLRequest *request = [_httpClient requestWithMethod:#"GET" path:#"/" nil];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
AFHTTPRequestOperation *operation = [_httpClient HTTPRequestOperationWithRequest:(NSURLRequest *)request
success:^(AFHTTPRequestOperation *operation, id data) {
if(dataBlock)
dataBlock(data);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Cannot download : %#", error);
}];
[operation setShouldExecuteAsBackgroundTaskWithExpirationHandler:^{
NSLog(#"Request time out");
}];
[_httpClient enqueueHTTPRequestOperation:operation];
}
My problem is : my connections are made asynchronously, so every task are launched together without waiting fo the previous to finish even with [self._queue setMaxConcurrentOperationCount:1] in SyncAgent.
Do I need to perform every connection synchronously ? I don't think this is a good idea, because a connection never should be done this way and also because I might use these methods elsewhere and need them to be performed in background, but I cannot find a better way. Any idea ?
Oh and if there is any error/typo in my code, I can assure you it appeared when I tried to summarize it before pasting it, it is working without any problem as of now.
Thanks !
PS: Sorry for the long pastes I couldn't figure out a better way to explain the problem.
EDIT: I found that using a semaphore is easier to set up and to understand : How do I wait for an asynchronously dispatched block to finish?
If you have any control over the server at all, you should really consider creating an API that allows you to upload photos in an arbitrary order, so as to support multiple simultaneous uploads (which can be quite a bit faster for large batches).
But if you must do things synchronized, the easiest way is probably to enqueue new requests in the completion block of the requests. i.e.
// If [operations length] == 1, just enqueue it and skip all of this
NSEnumerator *enumerator = [operations reverseObjectEnumerator];
AFHTTPRequestOperation *currentOperation = nil;
AFHTTPRequestOperation *nextOperation = [enumerator nextObject];
while (nextOperation != nil && (currentOperation = [enumerator nextObject])) {
currentOperation.completionBlock = ^{
[client enqueueHTTPRequestOperation:nextOperation];
}
nextOperation = currentOperation;
}
[client enqueueHTTPRequestOperation:currentOperation];
The following code works for me, but I am not sure of the drawbacks. Take it with a pinch of salt.
- (void) main {
NSCondition* condition = [[NSCondition alloc] init];
__block bool hasData = false;
[condition lock];
[[WebAPI sharedClient] postPath:#"url"
parameters:queryParams
success:^(AFHTTPRequestOperation *operation, id JSON) {
//success code
[condition lock];
hasData = true;
[condition signal];
[condition unlock];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//failure code
[condition lock];
hasData = true;
[condition signal];
[condition unlock];
}];
while (!hasData) {
[condition wait];
}
[condition unlock];
}

How do I track NSError objects across threads?

I have a set of asynchronous calls being spawned using NSInvocationOperation:
- (void)listRequestQueue:(StoreDataListRequest *)request {
[openListRequests addObject:request];
NSInvocationOperation *requestOp = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(listRequestStart:)
object:request];
[opQueue addOperation:requestOp];
[requestOp release];
}
- (void)listRequestStart:(StoreDataListRequest *)request {
if(self.resourceData == nil) {
//TODO fail appropriately...
return;
}
StoreDataListResponse *response = [self newListResponseForProductID:request.productID];
[self performSelectorOnMainThread:#selector(listRequestFinish:)
withObject:response waitUntilDone:NO];
[response release];
[self performSelectorOnMainThread:#selector(cleanUpListRequest:)
withObject:request waitUntilDone:NO];
}
- (void)listRequestFinish:(StoreDataListResponse *)response {
[self.delegate storeData:self didReceiveListResponse:response];
}
- (StoreDataListResponse *)newListResponseForProductID:(NSString *)productID {
CollectionData *data = [self.resourceData dataForProduct:productID];
if(data == nil) {
//TODO do something
}
StoreDataListResponse *response = [[StoreDataListResponse alloc] init];
response.productID = productID;
if(productID != data.productID) {
//TODO fail; remove product from list
}
response.name = NSLocalizedString(#"Loading...", #"Loading message");
response.blurb = NSLocalizedString(#"Waiting for response from server", #"Waiting for website to respond");
return response;
}
For each of the TODOs in the above code, I should resolve the issue and let any handlers know that things have failed and why. Looking at the NSError class and documentation, it appears to be the appropriate answer but I am having trouble figuring out how to get to work with NSInvocationOperation and performSelectorOnMainThread:withObject:waitUntilDone:. I can certainly get the NSErrors out of the newListResponseForProductID: method by changing it to something like this:
- (StoreDataListResponse *)newListResponseForProductID:(NSString *)productID error:(NSError **)error;
How do I get the error generated back into the main thread so I can deal with the failed request?
The easiest way to run any code on the main thread is to use GCD and blocks on iOS 4 and above. Like this:
dispatch_async(dispatch_get_main_queue(), ^(void) {
// Your code to run on the main thread here,
// any variables in scope like NSError is captured.
});
Or use dispatch_sync() if you know you are on a background thread and want the block to complete on the main thread before you continue.