I'm very new to backend programming and iOS programming. I'm currently integrating DynamoDB into my iOS application. I'm doing a signup function that queries the database to check whether the username is taken.
Here's the main idea of my code:
__block BOOL isPlayerTaken = NO;
//Block of code that queries the database
//Mutates value of isPlayerTaken if player exists
[[dynamoDBObjectMapper query:[DDBRunnerTableRow class]
expression:queryExpression]
continueWithExecutor:[BFExecutor mainThreadExecutor] withSuccessBlock:^id(BFTask *task) {
AWSDynamoDBPaginatedOutput *paginatedOutput = task.result;
DDBRunnerTableRow *queriedRunner = paginatedOutput.items[0];
if (queriedRunner) {
isPlayerTaken = YES;
NSLog(#"%#", queriedRunner.runnerName);
}
return nil;
}];
return isPlayerTaken;
However, the function returns before the query result is completed and hence the output is always NO.
How can I ensure that the function doesn't return until the query block completes?
Thank you so much for your help. I believe I would need this in many functions of my code that requires querying and inserting of data and I didn't want to come up with a hackish solution for it.
Try something like this:
[[dynamoDBObjectMapper query:[DDBRunnerTableRow class]
expression:queryExpression]
continueWithExecutor:[BFExecutor mainThreadExecutor] withSuccessBlock:^id(BFTask *task) {
AWSDynamoDBPaginatedOutput *paginatedOutput = task.result;
DDBRunnerTableRow *queriedRunner = paginatedOutput.items[0];
if (queriedRunner)
{
NSLog(#"%#", queriedRunner.runnerName);
[self loginSuccessfull:queriedRunner];
}
else
{
[self loginFailed];
}
}];
then
- (void)loginSuccessfull:(DDBRunnerTableRow *)queriedRunner
{
//whatever
}
- (void)loginFailed
{
//whatever
}
Related
I am using the bolt framework for an Async task. How do I test the code, which is in continueWithBlock section?
BOOL wasFetchedFromCache;
[[store fetchFileAsync:manifestURL allowfetchingFromCache:YES fetchedFromCache:&wasFetchedFromCache]
continueWithBlock:^id(BFTask *task) {
NSData *fileContents = task.result;
NSError *localError;
// code to test
return nil
}];
In order to test async task, you should use XCTestExpectation which allows us to create an expectation that will be fulfilled in the future. That mean the future results returned is considered as an expectation in the test case, and the test will wait until receiving the results for asserted. Please take a look at the code below, which I write a simple async testing.
- (void)testFetchFileAsync {
XCTestExpectation *expectation = [self expectationWithDescription:#"FetchFileAsync"];
BOOL wasFetchedFromCache;
[[store fetchFileAsync:manifestURL allowfetchingFromCache:YES fetchedFromCache:&wasFetchedFromCache]
continueWithBlock:^id(BFTask *task) {
NSData *data = task.result;
NSError *localError;
XCTAssertNotNil(data, #"data should not be nil");
[expectation fulfill];
// code to test
return nil
}];
[self waitForExpectationsWithTimeout:15.0 handler:^(NSError * _Nullable error) {
if (error) {
NSLog(#"Timeout error");
}
}];
XCTAssertTrue(wasFetchedFromCache, #"should be true");
}
I am now working on an app that works with BLE, Backend server and location. I am facing a problem which I am not sure how to get out of which is what people call "Callback hell". The entire CoreBluetooth framework in iOS is based on a delegate pattern, which until you can use the CBPeripheral has to go to at least 3 callbacks:
DidConnectToPeripheral
DidDiscoverServices
DidDiscoverCharacteristics
But in fact there could be many more, and every action you take with the device will come back as a callback to one of those functions. Now when I want to "Rent" this ble product, I must connect to it, after connecting send a requests to the server and get the user's current location, after that all happens I have to write a value in the bluetooth device and get confirmation. This would not be so difficult, but unfortunately each and every one of those stages is failable, so error handling needs to be added. Not to mention implementing timeout.
I am sure I am not the only one to approach such issues so I looked around and I found 2 things that might help:
the Advanced NSOperations talk in the wwdc 2015, but after trying for 4 days to make it work, it seems like the code is too buggy.
Promisekit but I couldn't find a way to wrap CoreBluetooth.
How are people with even more complicated apps deal with this? in swift or objc.
Some sample problematic code:
-(void)startRentalSessionWithLock:(DORLock *)lock timeOut:(NSTimeInterval)timeout forSuccess:(void (^)(DORRentalSession * session))successBlock failure:(failureBlock_t)failureBlock{
//we set the block to determine what happens
NSAssert(lock.peripheral, #"lock has to have peripheral to connect to");
if (!self.rentalSession) {
self.rentalSession = [[DORRentalSession alloc] initWithLock:nil andSessionDict:#{} active:NO];
}
self.rentalSession.lock = lock;
[self connectToLock:self.rentalSession.lock.peripheral timeOut:timeout completionBlock:^(CBPeripheral *peripheral, NSError *error) {
self.BTConnectionCompleted = nil;
if (!error) {
[[INTULocationManager sharedInstance] requestLocationWithDesiredAccuracy:INTULocationAccuracyHouse timeout:1 delayUntilAuthorized:YES block:^(CLLocation *currentLocation, INTULocationAccuracy achievedAccuracy, INTULocationStatus status) {
if (status == INTULocationStatusSuccess || status == INTULocationStatusTimedOut) {
[self startServerRentalForSessionLockWithUserLocation:currentLocation.coordinate forSuccess:^(DORRentalSession *session) {
if (self.rentalSession.lock.peripheral && self.rentalSession.lock.peripheral.state == CBPeripheralStateConnected) {
[self.rentalSession.lock.peripheral setNotifyValue:YES forCharacteristic:self.rentalSession.lock.charectaristics.sensorCharacteristic];
}else{
//shouldnt come here
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.rentalSession.lock.peripheral.state == CBPeripheralStateConnected) {
!self.rentalSession.lock.open ? [self sendUnlockBLECommandToSessionLock] : nil;
if (successBlock) {
successBlock(session);
}
}else{
[self endCurrentRentalSessionWithLocation:self.rentalSession.lock.latLng andPositionAcc:#(1) Success:^(DORRentalSession *session) {
if (failureBlock) {
failureBlock([[NSError alloc] initWithDomain:DonkeyErrorDomain code:46 userInfo:#{NSLocalizedDescriptionKey:#"Could't connect to lock"}],200);
}
} failure:^(NSError *error, NSInteger httpCode) {
if (failureBlock) {
failureBlock([[NSError alloc] initWithDomain:DonkeyErrorDomain code:45 userInfo:#{NSLocalizedDescriptionKey:#"fatal error"}],200);
}
}];
}
});
} failure:^(NSError *error, NSInteger httpCode) {
if (failureBlock) {
failureBlock(error,httpCode);
}
}];
}else{
NSError *gpsError = [self donkeyGPSErrorWithINTULocationStatus:status];
if (failureBlock) {
failureBlock(gpsError,200);
}
}
}];
}else{
if (failureBlock) {
failureBlock(error,200);
}
}
}];
}
To get rid of this nested calls you can use GCD group + serial execution queue:
dispatch_queue_t queue = ddispatch_queue_create("com.example.queue", NULL);
dispatch_group_t group = dispatch_group_create();
// Add a task to the group
dispatch_group_async(group, queue, ^{
// Some asynchronous work
});
// Make dispatch_group_async and dispatch_group_sync calls here
// Callback to be executed when all scheduled tasks are completed.
dispatch_group_notify(serviceGroup,dispatch_get_main_queue(),^{
// Do smth when everything has finished
});
// wait for all tasks to complete
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
The other solution based on GCD groups is described here
Overview : I am using Amazon DynamoDB for my login service. And my login method looks like this in some UserAccount.m. And I am calling this class method in some LoginViewController.m on login button tap:
+ (BOOL)loginWithUsername:(NSString *)username password:(NSString *)password{
AWSDynamoDBObjectMapper *dynamoDBObjectMapper = [AWSDynamoDBObjectMapper defaultDynamoDBObjectMapper];
BOOL __block isLoginSuccessful = NO;
[[dynamoDBObjectMapper load:[User class] hashKey:username rangeKey:#"key"]
continueWithBlock:^id(AWSTask *task) {
if (task.error) {
NSLog(#"The request failed. Error: [%#]", task.error);
}
if (task.exception) {
NSLog(#"The request failed. Exception: [%#]", task.exception);
}
if (task.result) {
//Do something with the result.
User *user = task.result; // User is a model I'm using
NSLog(#"pass: %#", user.password);
// Check returned password from DynamoDB with user-supplied password.
if ([user.password isEqualToString:password]) {
isLoginSuccessful = YES;
}
}
return nil;
}];
return isLoginSuccessful; // <-- Issue: function returns before block executes and isLoginSuccessful value is changed.
}
The issue is function returns before block executes. What I tried:
i) I read up about using dispatch groups on this SO question
ii) Tried to execute method - (AWSTask *)load:(Class)resultClass hashKey:(id)hashKey rangeKey:(id)rangeKey on main thread like this but still function returns before block execution finishes.:
dispatch_async(dispatch_get_main_queue(), ^{
[[dynamoDBObjectMapper load:[User class]
hashKey:username
rangeKey:#"key"] continueWithExecutor:[AWSExecutor mainThreadExecutor] withBlock:^id(AWSTask *task) {
if (!task.error) {
User *user = task.result;
NSLog(#"pass: %#", user.password);
//Do something with the result.
if ([user.password isEqualToString:password]) {
isLoginSuccessful = YES;
}
} else {
NSLog(#"Error: [%#]", task.error);
}
return nil;
}];
});
Am I missing out something here/ doing wrong in my approach? Any suggestion towards right direction would be really helpful. Thank you!
Edit 1: Including function from where loginWithUsername class method is being called:
#IBAction func login(sender: AnyObject) {
if(UserAccount.loginWithUsername(txtEmail.text, password: txtPassword.text)){
println("SUCCESS")
lblIncorrect.hidden = true
}
else{
println("Incorrect username/password")
lblIncorrect.hidden = false
}
}
It is the basic idea of GCD that blocks are executed "in background", so the control flow can return to the sender of the message before the task is finished. This is, because the task is a potential long runner and you do not want to block the sending control flow, esp. if it is the main thread.
If you want to execute code after finishing the operation, simply add it to the block. This is the code inside login(): Add the if to the block and remove the block variable and the return value. Make the method void.
To have a general pattern:
-(IBAction)doSomething:(id)sender
{
[self executeOperation]; // Method becomes void
// Remove code processing on result
}
-(void)executeOperation // Method becomes void
{
[receiver longRunnerWithBlock:^(id result)
{
…
// Add code processing on result
}
}
Please take care, that the block is potentially run on a different thread than the main thread, so you have to dispatch it on the main thread, if it is UI related.
OR you could just take a page from Googles iOS framework code and do it the easy way like this:
- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel {
if (signInDoneSel) {
[self performSelector:signInDoneSel];
}
}
Problem
When I unsubscribe to my endpoints subscriptions, instruments memory graph goes from 32MB to 300MB and app is killed Jetsam. It was my understanding that AWS BFTasks clean up after themselves… If I remove the do...while and allow it to process the first 100 subscriptions, all works fine.
Process
I have about 8000 subscriptions in my development app that uses AWS SNS to push alerts to iOS devices and email. After the user selects the subscriptions they want, I remove their existing subscriptions, then subscribe them to the new ones. The [_awsSnsClient listSubscriptions:request] returns a max of 100 subscriptions, I loop through these, performing [_awsSnsClient unsubscribe:unsubscribeRequest] as needed. Then get the next 100...
Code
- (void) awsUsubscribeAllSubscriptions {
DLog(#"Removing existing subscriptions");
AWSSNSListSubscriptionsInput *request = [AWSSNSListSubscriptionsInput new];
__block Boolean sentLastUnsubscribe = NO; // YES when the last unsubscribe request was sent
__block int pendingUnsubscribe = 0; // Number of outsianding unsubscribes (async process not yet complete)
do {
[[[_awsSnsClient listSubscriptions:request] continueWithSuccessBlock:^id(BFTask *task) {
AWSSNSListSubscriptionsResponse *response = (AWSSNSListSubscriptionsResponse *)task.result;
NSString *nextToken = response.nextToken;
NSArray *subscriptions = response.subscriptions;
for (AWSSNSSubscription *subscription in subscriptions) {
if ([subscription.endpoint isEqualToString:_awsPlatformEndpoint.endpointArn]) {
AWSSNSUnsubscribeInput *unsubscribeRequest = [AWSSNSUnsubscribeInput new];
unsubscribeRequest.subscriptionArn = subscription.subscriptionArn;
pendingUnsubscribe++;
[[[_awsSnsClient unsubscribe:unsubscribeRequest] continueWithSuccessBlock:^id(BFTask *task) {
DLog(#"Unsubscribed from:%#",subscription.subscriptionArn);
return nil;
}] continueWithBlock:^id(BFTask *task) {
pendingUnsubscribe--;
if (task.error) {
// failed with error
ALog(#"Error unsubscribing to: %#, %#, %#", subscription, [task.error localizedDescription], [task.error localizedFailureReason]);
}
// If we have processed the last unsubscribe, then create topics and subscribe
if (sentLastUnsubscribe && (pendingUnsubscribe <= 0)) {
[self awsCreateTopicsAndSubscriptions];
}
return nil;
}];
}
}
request.nextToken = nextToken;
if(!nextToken) sentLastUnsubscribe = YES; // Reached the end of the SNS subscriptions
return nil;
}] continueWithBlock:^id(BFTask *task) {
if (task.error) {
// failed with error
ALog(#"Error listing subscriptions: %#, %#", [task.error localizedDescription], [task.error localizedFailureReason]);
}
return nil;
}];
} while (!sentLastUnsubscribe);
}
Is there a better way to remove all SNS subscriptions from an endpoint? Is there a way for force release any retained data during this loop?
Solution
Using both recursion and - waitUntilFinished. Memory load is now much lower.
-(void) awsUnsubscribeFromAllSubscriptionsWithNextToken:(NSString *)nextToken AndSubscribe:(BOOL)subscribe {
AWSSNSListSubscriptionsInput *request = [AWSSNSListSubscriptionsInput new];
request.nextToken = nextToken;
[[[_awsSnsClient listSubscriptions:request] continueWithSuccessBlock:^id(BFTask *task) {
AWSSNSListSubscriptionsResponse *response = (AWSSNSListSubscriptionsResponse *)task.result;
NSArray *subscriptions = response.subscriptions;
for (AWSSNSSubscription *subscription in subscriptions) {
if ([subscription.endpoint isEqualToString:_awsPlatformEndpoint.endpointArn]) {
DLog(#"Unsubscribe from %#", subscription.topicArn);
AWSSNSUnsubscribeInput *unsubscribeRequest = [AWSSNSUnsubscribeInput new];
unsubscribeRequest.subscriptionArn = subscription.subscriptionArn;
[[_awsSnsClient unsubscribe:unsubscribeRequest] waitUntilFinished];
}
}
if(response.nextToken) {
[self awsUnsubscribeFromAllSubscriptionsWithNextToken:response.nextToken AndSubscribe:subscribe];
} else if (subscribe) {
[self awsCreateTopicsAndSubscriptions];
}
return nil;
}] continueWithBlock:^id(BFTask *task) {
if (task.error) {
// failed with error
ALog(#"Error listing subscriptions: %#, %#", [task.error localizedDescription], [task.error localizedFailureReason]);
}
return nil;
}];
}
Please note that - listSubscriptions: is an asynchronous method and returns immediately. You are calling an async method in a for loop. It means you are potentially calling - listSubscriptions: hundreds or even thousands of times in a short period.
The easiest thing you can do to fix it is to call - waitUntilFinished at the end of the async block. It makes the entire method synchronous
However, in general, you should avoid calling - waitUntilFinished as much as possible. AWSKinesisRecorderTests.m has a function to call - getRecords: recursively. You can adopt a similar pattern.
I'm trying to test Diary class that has dependency to Network.
So Diary code:
- (PMKPromise *)saveAndUploadToServer:(DiaryItem *)item
{
return [self save:item].then(^{
return [self upload:item]; << See UPDATE //I put breakpoint here, it is never called
});
}
- (PMKPromise *)save:(DiaryItem *)item
{
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[self.entryCreationManagedContext performBlock:^{
BOOL success;
NSError *saveError = nil;
item.status = #(UploadingStatus);
success = [self.entryCreationManagedContext save:&saveError];
if (success) {
fulfill(item.objectID);
}
else {
reject(saveError);
}
}];
}];
}
- (PMKPromise*)upload:(DiaryItem*)item
{
return [self.network POST:self.diaryUrl parameters:[item dictionary]].then(^{
return [self reportUploadAnalytics];
});
}
And the test:
- (void)testFailedUploadingReportsAnalytics
{
XCTestExpectation *expectation = [self expectationWithDescription:#"Operations completed"];
[self uploadToServerAndReturnCallback].finally(^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
assertThat(error, is(nilValue()));
//check that mock called
}];
}
The Network is mock in this test. But what I see that chain of promises is not executed. It stuck. Maybe because then: block is called on main thread as well XCTest is pausing it. But at the same time it should probably continue after 5 sec. What can be the issue?
UPDATE
Looks like it is nothing with my original assumption. If I replace [self.entryCreationManagedContext save:&saveError] with YES then debug reaches breakpoint.
UPDATE 2
It looks like issue with this particular saving of managed context. It is triggering notification about synchronising another managed contexts. And we are discovering what else there.
It is ended up in different issue and nothing connected to PromiseKit.
We discovered growing amount of used memory. That NSManagedObjectContextDidSaveNotification was producing deadlock on different store coordinators. After fixing that tests started working as expected.