I have an NSTimer in an iOS that is polling a database server at every 10th seconds for a data row in a table based upon a certain data ID, that has been sent as an argument via a PHP-script. If the data ID matches the data ID of the row that have been inserted by an external source then the app will show an alert box containing the information from the data row and the NSTimer will stop to tick.
But this only works while the app is running in the foreground and I want to show the information message as a local notification so that even though the user has exited from the app, it will still poll the server when the app is running in the background as well.
I have read on many sites that the Local Notification service and background fetch is the right kind of solution but I don't know how to set it up really, it is very confusing.
Because I have seen many examples where Local Notification is used to send reminders at certain dates on the calendar and trigger alarms at certain times and not so much about polling to a server.
How do you set up a Local Notification that will poll to a server at the interval of 10 seconds and then cancel it as soon as it receives right kind of information that it will display at last?
Here is how I have done so far:
...
NSTimer *confirmedTimer;
int orderId = 1;
...
-(IBAction) sendButton: (id) sender {
confirmedTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:#selector(confirmedTick) userInfo:nil repeats:YES];
}
-(void)confirmedTick {
NSString *paramsConfirmed = [NSString stringWithFormat:#"order_id=%d", orderId];
NSData *postDataConfirmed = [paramsConfirmed dataUsingEncoding:NSUTF8StringEncoding];
NSURL *urlConfirmed = [NSURL URLWithString:#"http://www.serverexample.com/confirmed.php"];
NSMutableURLRequest *requestConfirmed = [NSMutableURLRequest requestWithURL:urlConfirmed];
[requestConfirmed setHTTPMethod:#"POST"];
[requestConfirmed addValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
[requestConfirmed setHTTPBody:postDataConfirmed];
[requestConfirmed setValue:[NSString stringWithFormat:#"%i", postDataConfirmed.length] forHTTPHeaderField:#"Content-Length"];
NSURLResponse *responseConfirmed;
NSError *errorConfirmed = nil;
NSData *receivedDataConfirmed = [NSURLConnection sendSynchronousRequest:requestConfirmed
returningResponse:&responseConfirmed
error:&errorConfirmed];
if(errorConfirmed) {
if([responseConfirmed isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponseConfirmed = (NSHTTPURLResponse *)responseConfirmed;
return;
}
return;
}
NSString *responseStringConfirmed = [[NSString alloc] initWithData:receivedDataConfirmed
encoding:NSUTF8StringEncoding];
if ([responseStringConfirmed isEqualToString:#"true"]) {
return;
}
NSDictionary *jsonObjectConfirmed = [responseStringConfirmed objectFromJSONString];
NSDictionary *jsonDictionary = [NSJSONSerialization JSONObjectWithData:receivedDataConfirmed options:0 error:nil];
NSArray *confirmedArray = [jsonDictionary objectForKey:#"confirmed_table"];
if([confirmedArray count] > 0)
{
[confirmedTimer invalidate];
NSString *confirmedMessage = #"";
for(NSDictionary *confirmed in confirmedArray)
{
confirmedMessage = [confirmedMessage stringByAppendingString:[NSString stringWithFormat:#"confirmed_id: %#\n", [NSNumber numberWithInt:[[confirmed objectForKey:#"confirmed_id"] intValue]]]];
confirmedMessage = [confirmedMessage stringByAppendingString:[NSString stringWithFormat:#"order_id: %#\n", [NSNumber numberWithInt:[[confirmed objectForKey:#"order_id"] intValue]]]];
confirmedMessage = [confirmedMessage stringByAppendingString:[NSString stringWithFormat:#"Information: %#", [confirmed objectForKey:#"information"]]];
}
UIAlertView *confirmedAlert = [[UIAlertView alloc]
initWithTitle:#"Confirmation"
message:confirmedMessage
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[confirmedAlert show];
[confirmedAlert release];
}
}
You have it slightly backwards. The local notification doesn't check the server. Rather you implement background fetch and then post a local notification if the background fetch detects the relevant data. There is a good tutorial on background fetch here.
Note that background fetch won't execute every 10 seconds
Related
I have an issue that my NSTimer doesn't work when the app is working in backgroud.
on the simulator it is work, on the device it is not.
my code on the AppDelegate.m:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
NSLog(#"enter background");
notiTimer = [NSTimer scheduledTimerWithTimeInterval:4 target:self selector:#selector(checkNotification) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:notiTimer forMode:NSDefaultRunLoopMode];
}
and the method I am trying to run in loop:
-(void)checkNotification {
NSLog(#"running loop in bg");
userdetails =[NSUserDefaults standardUserDefaults];
userid = [userdetails objectForKey:#"userid"];
username = [userdetails objectForKey:#"username"];
if (userid != NULL && username != NULL) {
notifString = [NSString stringWithFormat:#"userid=%#", userid];
notifData = [NSData dataWithBytes: [notifString UTF8String] length: [notifString length]];
urlreq = [[NSMutableURLRequest alloc] initWithURL:notifUrl];
[urlreq setHTTPMethod: #"POST"];
[urlreq setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"content-type"];
[urlreq setHTTPBody: notifData];
sendreq = [NSURLConnection sendSynchronousRequest:urlreq returningResponse:nil error:nil];
response =[[NSString alloc] initWithBytes:[sendreq bytes] length:[sendreq length] encoding:NSUTF8StringEncoding];
responsTrimmed = [response stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
splitResponse = [responsTrimmed componentsSeparatedByString: #","];
if ([splitResponse[0] isEqualToString:#"1"]) {
NSLog(#"%#",splitResponse[1]);
}
else {
NSLog(#"err");
}
}
}
now once at the app enter to background I get at the debugger the NSLog:
NSLog(#"enter background");
but the run loop doesn't work and the method doesn't call in loop and I don't get NSLog
NSLog(#"running loop in bg");
on the debugger, any idea ?
When your application goes in background, it is "frozen" by the OS, so no task can be executed in the normal way.
The application itself should declare how it handles background task, but you are limited to one of this situation:
you need a little amount of time (usually less then 10 minute): you can simply ask for it
your app is downloading something: you can ask the system to continue the download for you
a little set of task can be executed in background, but you have to specify it in your project
So in order to execute some code, you have to start a background task at quit time.
Here is the Apple documentation for Background Execution, hope it can help you.
I have an app that can send information to a server. This information is stacked up during the day (while the client uses the app), and when he so desires, he can hit the "update" button to send everything on the server.
This always worked fine until he recently had a flow increase and went from updating 10 objects to more than 100.
Obviously, the update takes more time, taht's not the issue.
The issue is, at some point, i'm getting
Error: Error Domain=NSURLErrorDomain Code=-1001 "La requête a expiré."
UserInfo=0x189874b0 {NSErrorFailingURLStringKey=http://www.*********.be/upload,
NSErrorFailingURLKey=http://www.************.be/upload,
NSLocalizedDescription=La requête a expiré.,
NSUnderlyingError=0x189abd70 "La requête a expiré."}
For the frenchophobes, " The request has expired " is what i get back, and i've hidden the url with ****, as you noticed.
Now, i've tried locally, it works fine with a small update, but when i loop 150 times on my update (i send 150 times the same thing), at some point i just get the above error X times. This error does not specificall occur with all the last items, it can be 20 in the middle, or 30, etc.
Is there a way i can change that?
Here is a piece of code that must be related to the issue.
// Set the max number of concurrent operations (threads)
//[operationQueue setMaxConcurrentOperationCount:3]; // Todo: try increasing max thread count
[operationQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount]; //dynamic thread count
self.queueCount = persons.count;
self.currentQueue = 1;
for (Person *person in persons) {
for (int i = 0 ; i<130 ; i++){ //this is where i try to break the app
[self createSendPersonOperation:person];
}}
Now what would probably work is put the last line in a "thing" that would slow down the process every 20 or so occurences, so the server or the app doesn't go crazy.
Is this possible? if so, how?
Note : I am a junior dev trying to get into a senior's code, and that guy is not available, so i'm open to all the help i can have.
Edit : also, do you think my error comes from a server-sided issue or is definitly an app-sided issue?
Edit : Complete HTTP request.
So for every person that is saved into the app, when the user decides to update, it does that for every Person in the array of persons.
- (void)createSendPersonOperation:(Person *)person
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:#"text/html", #"application/json", nil];
NSDictionary *params = #{
#"email": person.email,
#"gender": person.gender,
#"language": person.language,
#"hasFacebook": person.hasFacebook,
#"sendPostalCard": person.sendPostalCard
};
NSLog(#"params: %#", params);
[manager POST:kURLUpdate parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
// Add picture to the form
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *pictureFilePath = [documentsDirectory stringByAppendingPathComponent:person.picture];
NSURL *pictureURL = [NSURL fileURLWithPath:pictureFilePath];
[formData appendPartWithFileURL:pictureURL name:#"picture" error:nil];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success: %#", responseObject);
if ([responseObject isKindOfClass:[NSDictionary class]]) {
if ([responseObject objectForKey:#"error"]) {
NSLog(#"Error 1");
NSDictionary *error = [responseObject objectForKey:#"error"];
NSLog(#"Error message: %#", [error objectForKey:#"message"]);
} else {
// Set Person's sended attribute
person.sended = #YES;
[Person saveObject:[[PersistentStack sharedInstance] managedObjectContext] error:nil];
}
} else {
NSLog(#"Error 2");
}
[self decreaseQueueCount];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
NSLog(#"Parameter that failed : %#", [params objectForKey:#"email"]);
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Erreur"
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:#"Fermer"
otherButtonTitles:nil];
[alertView show];
self.updateHud.mode = MBProgressHUDModeText;
self.updateHud.labelText = AMLocalizedString(#"update.failure.message", #"");
[self.updateHud hide:YES afterDelay:3];
}];
}
I don't really know the source of your problem, but if you think slowing the app will at least help you understand your problem you could do it with something like this:
NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:15];
while ([loopUntil timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:loopUntil];
}
It will wait for 15 seconds before continue, so you can put this one after 20~30 requests as you suggested.
I really believe you should consider grouping your requests or something like that so you won't overload your server (if that is really your problem).
-(void) parseXML
{
[self performSelector:#selector(parseXML) withObject:self afterDelay:55.0 ];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://apikeygoeshere.com/data.xml"]];
NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
NSString *xmlString = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
NSDictionary *xml = [NSDictionary dictionaryWithXMLString:xmlString];
NSMutableArray *items = [xml objectForKey:#"TeamLeagueStanding"];
NSMutableArray *newTeamObjectArray = [[NSMutableArray alloc] init];
for (NSDictionary *dict in items) {
TeamObject *myTeams = [TeamObject teamFromXMLDictionary:dict];
[newTeamObjectArray addObject:myTeams];
}
NSNull *nullValue = [NSNull null];
NSNull *nullValue2 = [NSNull null];
[newTeamObjectArray insertObject:nullValue atIndex:0];
[newTeamObjectArray insertObject:nullValue2 atIndex:1];
NSLog(#"standingsdataaaaa %#", newTeamObjectArray);
}
I want to add a unbutton to my storyboard so the user can refresh the data whenever he wants, but i don't him to be able to do this more than once per hour,
Can anyone help me? Thank you.
Just in the action method or wherever you call to get the XML
setEnabled: NO and set an NSTimer to fire nod a date that is 3600 seconds from now.
When it fires, setEnabled:YES
It might be nice to create a visual indicator to the user like a counter.
EDIT: In order to account for the fact that you still want to run the parseXML method every 55 seconds with or without the button press, I'm changing my answer by putting the conditional in the IBAction method triggered by the button press instead of putting the conditional in parseXML:
Declare an NSTimer as a class variable. For example, at the top of your .m directly after your #synthesizes, declare an NSTimer:
NSTimer *parseTimer;
Then in the IBAction method triggered by the button press, only call parseXML if the timer is nil; and if it is in fact nil and the parseXML method is going to run, initiate the timer so it doesn't run again for another hour:
- (IBAction)buttonPressed:(sender)id {
// If the parseTimer is active, do call parseXML.
// (And perhaps fire an alert here)
if (parseTimer != nil) return;
// Otherwise initialize the timer so that it calls the the method which
// will deactivate it in 60*60 seconds, i.e. one hour
parseTimer = [NSTimer scheduledTimerWithTimeInterval:60*60 target:self selector:#selector(reactivateButton) userInfo:nil repeats:YES];
[self parseXML];
}
The deactivateParseTimer method should deactivate the timer and set it to nil so that parseXML may run again:
- (void)deactivateParseTimer {
[parseTimer invalidate];
parseTimer = nil;
}
I'm using Braintree for Payment process in my application
[BTPaymentViewController paymentViewControllerWithVenmoTouchEnabled:NO];and use this method for encryption
`
(void)paymentViewController:(BTPaymentViewController *)paymentViewController
didSubmitCardWithInfo:(NSDictionary *)cardInfo
andCardInfoEncrypted:(NSDictionary *)cardInfoEncrypted {
NSDictionary *dict=[self encryptFormData:cardInfo];
[self savePaymentInfoToServer:dict];
}
-(NSDictionary *) encryptFormData:(NSDictionary *) formData {
BTEncryption *braintree = [[BTEncryption alloc] initWithPublicKey: PUBLIC_KEY];
NSMutableDictionary *encryptedParams = [[NSMutableDictionary alloc] init];
[formData enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop) {
[encryptedParams setObject: [braintree encryptString: object] forKey: key];
}];
return encryptedParams;
}
call to this method to post the data to localhost server for testing
- (void) savePaymentInfoToServer:(NSDictionary *)paymentInfo {
NSURL *url = [NSURL URLWithString: [NSString stringWithFormat:#"%#/card", SAMPLE_CHECKOUT_BASE_URL]];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
// You need a customer id in order to save a card to the Braintree vault.
// Here, for the sake of example, we set customer_id to device id.
// In practice, this is probably whatever user_id your app has assigned to this user.
// NSString *customerId = [[UIDevice currentDevice] identifierForVendor].UUIDString;
AppDelegate *appdelegate=(AppDelegate *) [[UIApplication sharedApplication]delegate];
[paymentInfo setValue:appdelegate.referenceId forKey:#"bookingRefId"];
[paymentInfo setValue:appdelegate.passengerId forKey:#"passengerId"];
request.HTTPBody = [self postDataFromDictionary:paymentInfo];
request.HTTPMethod = #"POST";
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *body, NSError *requestError)
{
NSError *err = nil;
if (!response && requestError) {
NSLog(#"requestError: %#", requestError);
[self.paymentViewController showErrorWithTitle:#"Error" message:#"Unable to reach the network."];
return;
}
NSDictionary *<b>responseDictionary</b> = [NSJSONSerialization JSONObjectWithData:body options:kNilOptions error:&err];
NSLog(#"saveCardToServer: paymentInfo: %# response: %#, error: %#", paymentInfo, responseDictionary, requestError);
if ([[responseDictionary valueForKey:#"success"] isEqualToNumber:#1]) { // Success!
// Don't forget to call the cleanup method,
// `prepareForDismissal`, on your `BTPaymentViewController`
[self.paymentViewController prepareForDismissal];
// Now you can dismiss and tell the user everything worked.
[self dismissViewControllerAnimated:YES completion:^(void) {
[[[UIAlertView alloc] initWithTitle:#"Success" message:#"Saved your card!" delegate:nil
cancelButtonTitle:#"OK" otherButtonTitles:nil] show];
}];
} else { // The card did not save correctly, so show the error from server with convenenience method `showErrorWithTitle`
[self.paymentViewController showErrorWithTitle:#"Error saving your card" message:[self messageStringFromResponse:responseDictionary]];
}
}];
}`
contain responseDictionary is null and error is null how to fix the issue can any one help me
where are you sending the paymentInfo dictionary to (i.e. what is SAMPLE_CHECKOUT_BASE_URL)? The example project built by Braintree simulates a backend as if you had one yourself. You will want to replace that URL with your backend's URL.
The BTPaymentViewController provides a client-side credit card checkout page, but your backend still has to execute the transaction. For your backend to execute that transaction, you'll have to send that paymentInfo dictionary to your servers.
If you haven't yet built a backend for your iOS app, you can quickly get set up and approved in minutes with Braintree to process your payments.
I am using the great RestKit Framework for an iPhone Application.
I have got a method, where I send requests to a webservice. Sometimes four or more requests per 30 seconds.
My sendMethod looks like:
- (void) sendLocation {
NSString *username = [userDefaults objectForKey:kUsernameKey];
NSString *password = [userDefaults objectForKey:kPasswordKey];
NSString *instance = [userDefaults objectForKey:kInstanceKey];
NSString *locationname = self.location.locationname;
NSString *url = [[NSString alloc] initWithFormat:#"http://www.someadress.com/%#", instance];
RKClient *client = [RKClient clientWithBaseURL:url username:username password:password];
// Building my JsonObject
NSDictionary *locationDictionary = [NSDictionary dictionaryWithObjectsAndKeys: username, #"username", locationname, #"locationname", nil];
NSDictionary *jsonDictionary = [NSDictionary dictionaryWithObjectsAndKeys:locationDictionary, #"location", nil];
NSString *JSON = [jsonDictionary JSONRepresentation];
RKParams *params = [RKRequestSerialization serializationWithData:[JSON dataUsingEncoding:NSUTF8StringEncoding]MIMEType:RKMIMETypeJSON];
[client post:#"/locations" params:params delegate:self];
}
Sometimes (especially when sending more requests after another) the value of the count property of the RKRequestQueue Object is > 1.
When my application enters background and then enters foreground the requests in the queue (when foreground is entered) are sent to my Webservice and the delegate
- (void)request:(RKRequest*)request didLoadResponse:(RKResponse*)response {)
for all requests will be called.
So the question is:
Why doesnt RestKit send some requests immediately (my Webservice doesnt receive anything while a request is stored in the queue)???
Does anyone know a solution or had/has the same problem?
I noticed this line where you create the RKClient:
RKClient *client = [RKClient clientWithBaseURL:url username:username password:password];
This basically creates new instance each time sendLocation method is called - are you sure this is your desired behavior? If the url, username and password does not change, you can access previously created client by calling [RKClient sharedClient]. In your current approach, new request queue is created for each new client.
Now back to the point. Take a look on this property of the RKRequestQueue:
/**
* The number of concurrent requests supported by this queue
* Defaults to 5
*/
#property (nonatomic) NSUInteger concurrentRequestsLimit;
As you can see this defaults to 5, so if you have more than that in any of your queues they will wait until the ongoing requests are processed. You also mentioned that the requests stack up when the application is moved to background, and then when it enters the foreground all of them are dispatched. You can control how your requests behave like this:
- (void)backgroundUpload {
RKRequest* request = [[RKClient sharedClient] post:#"somewhere" delegate:self];
request.backgroundPolicy = RKRequestBackgroundPolicyNone; // Take no action with regard to backgrounding
request.backgroundPolicy = RKRequestBackgroundPolicyCancel; // If the app switches to the background, cancel the request
request.backgroundPolicy = RKRequestBackgroundPolicyContinue; // Continue the request in the background
request.backgroundPolicy = RKRequestBackgroundPolicyRequeue; // Cancel the request and place it back on the queue for next activation
}
I have found this solution here. Scroll down to Background Upload/Download section.