Hi I'm new to using blocks in Objective-C
What I think I want is the following:
- (void) iNeedAToken {
NSString *token = [self theMethodThatShouldReturnTheToken];
}
- (NSString) theMethodThatShouldReturnTheToken {
[myAwesomeAsyncMethod success:^(id JSON) {
NSString *token = [JSON objectForKey:#"FOO"];
return token;
}]
}
Is this possible? Or is this the wrong logic all together?
Thanks!
You're mixing async with synchronous code. You're theMethodThatShouldReturnTheToken already returned (you're missing a return value) before the block passed to success finishes.
Best bet would be to continue your process from the success block.
- (void) tokenRequestContext1
{
[self requestToken:^(NSString *token) {
// do something with token
}];
}
- (void) requestToken:(void(^)(NSString *))tokenBlock
{
[myAwesomeAsyncMethod success:^(id JSON) {
NSString *token = [JSON objectForKey:#"FOO"];
if (tokenBlock) {
tokenBlock(token);
}
}];
}
You start by calling requestToken. This will start the async request for your token. Some time might pass, but eventually doSomethingWithToken will be called where you can use the received token.
There is a description of way to wait until complition block will be finished: http://omegadelta.net/2011/05/10/how-to-wait-for-ios-methods-with-completion-blocks-to-finish/
Regular version of this code is:
- (void) iNeedAToken {
[self theMethodThatShouldReturnTheToken:^(id res){ token = res;}];
NSString *token = [self theMethodThatShouldReturnTheToken];
}
- (void) theMethodThatShouldReturnTheToken:(void (^)(id res)result) {
[myAwesomeAsyncMethod success:^(id JSON) {
NSString *token = [JSON objectForKey:#"FOO"];
result(token);
}]
}
Related
I have found plenty of information on passing data from flutter to android using method channel but none for objective c. I understand calling functions, but not passing data. Below is an example of what I have.
Future<void> _uploadImage(String filePath) async {
try {
await platform.invokeMethod('uploadImage', {"filePath": filePath});
} on PlatformException catch (e) {
print("Failed to get token: '${e.message}'.");
}
}
else if([#"uploadImage" isEqualToString:call.method]){
DBUserClient *client = [DBClientsManager authorizedClient];
NSString *filePath = call.arguments(#"filePath");
NSLog(filePath, result);
NSData *fileData = [filePath dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO];
[[[client.filesRoutes uploadData:filePath inputData:fileData]
setResponseBlock:^(DBFILESFileMetadata *result, DBFILESUploadError *routeError, DBRequestError *networkError) {
if (result)
{
NSLog(#"%#\n", result);
} else
{
NSLog(#"%#\n%#\n", routeError, networkError);
}
}] setProgressBlock:^(int64_t bytesUploaded, int64_t totalBytesUploaded, int64_t totalBytesExpectedToUploaded) {
NSLog(#"\n%lld\n%lld\n%lld\n", bytesUploaded, totalBytesUploaded, totalBytesExpectedToUploaded);
}];
}
else{
result(FlutterMethodNotImplemented);
}
}];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
To clarify, the bottom set of code written in objective-c is definitely not correct, but I've been trying to piece it out. I have the images path and I am trying to upload it to Dropbox. Main issue is that the filePath is not reaching the method channel. NSString *filePath = call.arguments(#"filePath") isn't an actual function call.
Change:
NSString *filePath = call.arguments(#"filePath");
to
NSString *filePath = call.arguments[#"filePath"];
I have this code:
- (NSString *) login {
datos=#"";
NSString __block *variable;
NSString *sqlQueryExisteUsuario;
sqlQueryExisteUsuario = [[NSString alloc] initWithFormat:#"SELECT COUNT(*) FROM tableName WHERE field='value' AND field='value'"];
SQLClient* client = [SQLClient sharedInstance];
client.delegate = self;
[client connect:#"serverName" username:#"username" password:#"password" database:#"database" completion:^(BOOL success) {
[client execute:sqlQueryExisteUsuario completion:^(NSArray* results) {
variable = [self processLogin:results];
NSLog(#"In: %#",variable);
[client disconnect];
}];
}];
NSLog(#"Out: %#",variable);
return nil;
}
- (NSString *)processLogin:(NSArray*)data
{
existeArray = [NSMutableArray array];
for (NSArray* table in data)
for (NSDictionary* row in table)
for (NSString* column in row)
[existeArray addObject:row[column]];
NSString *existe=existeArray[0];
if([existe isEqualToString:#"1"])
{
datos=#"yes";
}else{
datos=#"no";
}
return datos;
}
In the first call to NSLog, which begins with In, the value shows. In the second call, which begins with Out, the value doesn't show. Why?
Your connect is async method, so NSLog... line will be executed earlier than completion block. So, you have to use blocks also:
- (NSString *) loginWithCompletion:(void(^)(NSString *result))handler
{
datos=#"";
NSString *sqlQueryExisteUsuario;
sqlQueryExisteUsuario = [[NSString alloc] initWithFormat:#"SELECT COUNT(*) FROM tableName WHERE field='value' AND field='value'"];
SQLClient* client = [SQLClient sharedInstance];
client.delegate = self;
[client connect:#"serverName" username:#"username" password:#"password" database:#"database" completion:^(BOOL success) {
if (success) {
[client execute:sqlQueryExisteUsuario completion:^(NSArray* results) {
NSString *variable = [self processLogin:results];
NSLog(#"In: %#",variable);
[client disconnect];
if (handler) {
handler (variable);
}
}];
} else {
//TODO: handle this
if (handler) {
handler (nil);
}
}
}];
}
Usage:
- (void)ff
{
[self loginWithCompletion:^(NSString *variable) {
//Do something with variable
}];
}
The problem is that your variable is being set inside a completion block: the variable variable (not a great name, BTW!) is set inside not only one but two blocks - that's the "completion" part of your code – both of which are best thought of a bit(!) like a miniature anonymous function: in this case, they get run when the system is ready for it, not straight away.
iOS will start execution of your connect code, then jump ahead to NSLog(#"Out: %#",variable), then execute the completion block of connect, which in turn starts more code (execute), which has its own completion block, which finally gets executed. As #rmaddy says in a comment below, the technical name for this is asynchronous code: the bit inside your block does not get executed immediately, which allows the system to continue doing other things while waiting for your task to complete.
So the running order will be:
1) You declare variable.
2) Your connection code starts.
3) variable gets printed out.
4) The connection completes.
5) Your execute code starts.
6) Your execute code completes.
7) variable gets set to the final value.
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 use the following method to attempt to synchronously obtain an OAuth access token within 10 seconds, otherwise return nil. It works fine, however as an exercise I would like to convert my code to use a semaphore.
The Runloop version
- (NSString*)oAuthAccessToken
{
#synchronized (self)
{
NSString* token = nil;
_authenticationError = nil;
if (_authentication.accessToken)
{
token = [NSString stringWithFormat:#"Bearer %#", _authentication.accessToken];
}
else
{
[GTMOAuth2ViewControllerTouch authorizeFromKeychainForName:_keychainName authentication:_authentication];
[_authentication authorizeRequest:nil delegate:self didFinishSelector:#selector(authentication:request:finishedWithError:)];
for (int i = 0; i < 5; i++)
{
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
if (_authentication.accessToken)
{
token = [NSString stringWithFormat:#"Bearer %#", _authentication.accessToken];
break;
}
else if (_authenticationError)
{
break;
}
}
}
// LogDebug(#"Returning token: %#", token);
return token;
}
}
Semaphore Version
The semaphore version of the code goes a little something like this:
- (NSString*)oAuthAccessToken
{
#synchronized (self)
{
NSString* token = nil;
_authenticationError = nil;
if (_authentication.accessToken)
{
token = [NSString stringWithFormat:#"Bearer %#", _authentication.accessToken];
}
else
{
_authorizationSemaphore = dispatch_semaphore_create(0);
dispatch_async(_authorizationRequestQueue, ^(void)
{
[GTMOAuth2ViewControllerTouch authorizeFromKeychainForName:_keychainName authentication:_authentication];
[_authentication authorizeRequest:nil delegate:self didFinishSelector:#selector(authentication:request:finishedWithError:)];
});
dispatch_semaphore_wait(_authorizationSemaphore, DISPATCH_TIME_FOREVER);
if (_authentication.accessToken)
{
token = [NSString stringWithFormat:#"Bearer %#", _authentication.accessToken];
}
}
return token;
}
}
Gotcha!!! GTMOAuth2 sometimes returns immediately
When GTMOAuth2 needs to hit the network it calls back via a delegate method. In this method I signal my semaphore.
Sometimes GTMOAuth2 is able to return immediately. The problem is the method returns void.
How can I signal my semaphore in the latter case? If I add an observer to the authentication.assessToken will it be fired?
I'm not familiar with the GTMOAuth2 library but authentication.accessToken is a property, so and there doesn't seem to be anything that prevents it from being KVO compliant. Adding an observer should work for you in all cases, both for the async and the sync. Therefore, I'd consider only the async case.
If you want to make your solution even cleaner, then you should definitely try Reactive Cocoa.
Here is the function where it crashes. The exact line of code is the one with: removeObjectForKey. Even when the test function is completely empty it crashes on removeObjectForKey. Note: I'm just passing in an empty function callback. currently, I have ARC off, do i need to turn it on? If possible, i would like to do it with ARC off, because turning it on would mean dealing with alot of compile issues.
the function does say something about non-retained objects, hence could be a memory issue.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// So we got some receipt data. Now does it all check out?
BOOL isOk = [self doesTransactionInfoMatchReceipt:responseString];
VerifyCompletionHandler completionHandler = _completionHandlers[[NSValue valueWithNonretainedObject:connection]];
[_completionHandlers removeObjectForKey:[NSValue valueWithNonretainedObject:connection]];
if (isOk)
{
//Validation suceeded. Unlock content here.
NSLog(#"Validation successful");
completionHandler(TRUE);
} else {
NSLog(#"Validation failed");
completionHandler(FALSE);
}
}
Here is the verificationController usage:
[[VerificationController sharedInstance] verifyPurchase:transaction completionHandler:^(BOOL success) {
if (success) {
NSLog(#"Hi, its success.");
[self testMethod];
} else {
NSLog(#"payment not authorized.");
}
}];
}
- (void) testMethod {
}
I could use __weak but then I would have to turn on ARC, which i'm trying to avoid. Note: the verificaitionController works when I put it inside other Classes/Objects, but as soon as I put it in the InAppPurchaseManager it blows up anytime it tries to access self. Self points to an instance of InAppPurchaseManager as defined like so (its a phonegap plugin):
#interface InAppPurchaseManager : CDVPlugin <SKPaymentTransactionObserver> {
}
I also ran into this problem i fixed this issue this way
main issue is when you are setting value to completion handler in verifyPurchase method it is setting nil value so find this line in verifyPurchase method
_completionHandlers[[NSValue valueWithNonretainedObject:conn]] = completionHandler;
and replace it with
[_completionHandlers setObject:[completionHandler copy] forKey:[NSValue valueWithNonretainedObject:conn]];
and find the connectionDidReceivedata method and replace it with
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// So we got some receipt data. Now does it all check out?
BOOL isOk = [self doesTransactionInfoMatchReceipt:responseString];
if (_completionHandlers && [_completionHandlers respondsToSelector:#selector(removeObjectForKey:)])
{
VerifyCompletionHandler completionHandler = _completionHandlers[[NSValue valueWithNonretainedObject:connection]];
[_completionHandlers removeObjectForKey:[NSValue valueWithNonretainedObject:connection]];
if (isOk)
{
//Validation suceeded. Unlock content here.
NSLog(#"Validation successful");
completionHandler(TRUE);
} else {
NSLog(#"Validation failed");
completionHandler(FALSE);
}
}
//[_completionHandlers removeObjectForKey:[NSValue valueWithNonretainedObject:connection]];
}
Hope this may help you and saves alot of time.
Is _completionHandlers nil? You might do something like this -
if (_completionHandlers && [_completionHandlers respondsToSelector:#selector(removeObjectForKey:)]) {
[_completionHandlers removeObjectForKey:[NSValue valueWithNonretainedObject:connection]];
}
Good luck.
Find following string:
[_completionHandlers setObject:completionHandler forKey:[NSValue valueWithNonretainedObject:conn]];
And change to:
[_completionHandlers setObject:[completionHandler copy] forKey:[NSValue valueWithNonretainedObject:conn]];
I don't know if you found the answer yet, but I just realized that _completionHandlers is never allocated (and if you po _completionHandlers after setting a breakpoint you will notice it is nil). Hope this helps!
// in VerificationController.m
- (id)init
{
self = [super init];
if (self != nil)
{
transactionsReceiptStorageDictionary = [NSMutableDictionary dictionary];
_completionHandlers = [NSMutableDictionary dictionary];
}
return self;
}