-(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;
}
Related
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
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.
My log window goes crazy, constantly reloading after a minute. Did I use NSTimer at the end correctly? Is performSelectorOnMainThread correct use? Thanks
-(void)URL
{
dispatch_async(myQueue, ^{
NSString* myURL= #"https://url.json";
NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString:myURL]];
NSLog(#"url: %#", myURL);
if ((!data) || (data==nil))//v1.2
{
NSLog(#"url: loading ERROR");
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:#selector(URL) userInfo:nil repeats:YES];
}else
{
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:data waitUntilDone:YES];
}
});
}
- (void)fetchedData:(NSData *)responseData {
NSLog=#"fetch";
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseData //1
options:kNilOptions
error:&error];
NSNumber* spot= [json objectForKey:#"amount"];
float spotfloat = [spot floatValue];
timer = [NSTimer scheduledTimerWithTimeInterval:15 target:self selector:#selector(URL) userInfo:nil repeats:YES];
}
set the repeats to NO or set the actual maximum timer. it's an infinite loop if you don't set the maximum time.
[NSTimer scheduledTimerWithTimeInterval:15 target:self selector:#selector(URL) userInfo:nil repeats:NO];
- (void)fetchedData:(NSData *)responseData {
NSLog=#"fetch";
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseData //1
options:kNilOptions
error:&error];
NSNumber* spot= [json objectForKey:#"amount"];
float spotfloat = [spot floatValue];
if (timer < 60)
{
timer = [NSTimer scheduledTimerWithTimeInterval:15 target:self selector:#selector(URL) userInfo:nil repeats:YES];
}
else
{
[timer invalidate];
}
}
A couple of observations:
You are repeatedly creating new repeating timers, but never calling invalidate on the old ones and as a result, you will undoubtedly end up with a cascade of timer events as the old ones will keep firing. (A repeating timer will continue firing until you explicitly call invalidate. Simply nil-ing or replacing the object in your class property/ivar is insufficient.) If you want to replace a repeating timer, make sure to invalidate the old one first (otherwise the old will keep firing).
Generally, though, you'd either create a repeating timer once and let it keep firing, or you'd create non-repeating timer, and at the end of the method, schedule another non-repeating timer. Given that you're dealing with network requests that may take an indeterminate amount of time (i.e. it could still be trying the previous request by the time the next repeating timer fires), I'd suggest using non-repeating timers that schedule the next one at the end of the method. Or, given the hassles in creating timers in background queues, just use dispatch_after.
BTW, timers need a run loop to function properly, so your attempt to create a timer in myQueue if the request failed is unlikely to succeed. If you wanted to schedule a timer from the background queue, the easiest way to do this would be to create the timer with timerWithTimeInterval and then manually add it to the main run loop. (The alternative, of creating a new background NSThread and scheduling your timer on that, is overkill for such a simple problem.)
As an aside, I'd be inclined to run fetchedData on the background queue, too. Just dispatch the UI and/or model update back to the main queue. That way you minimize how much you're doing on the main queue.
I'm unclear as to how you're determining when to stop this process. Maybe you've concluded you don't need that at this point, but I'd suggest you include some cancellation logic, even if you don't avail yourself of it at this point.
You're using a shorter delay if you encounter network problem. That might be a dangerous solution because if your server failed because it was overwhelmed with requests coming in every 15 seconds from too many users, having clients start sending requests every 5 seconds might only make the situation worse.
Frankly, in an ideal scenario, you'd look at the exact cause of the error, and decide the correct behavior at that point. For example, if a request failed because the device doesn't have Internet connectivity, use Reachability to determine when the network is restored, rather than blindly trying every five seconds.
You're sending requests ever 5-15 seconds. If you really need that sort of interactivity, you might consider a different architecture (e.g. sockets). That's beyond the scope of this question, but something for you to research/consider.
Anyway, you might consider something like:
- (void)viewDidLoad
{
[super viewDidLoad];
self.myQueue = ...
[self schedulePollWithDelay:0];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
self.stopPolling = YES;
}
- (void)schedulePollWithDelay:(CGFloat)delay
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), self.myQueue, ^{
[self retrieveData];
});
}
-(void)retrieveData
{
if (self.stopPolling)
return;
NSString* myURL= #"https://url.json";
NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString:myURL]];
if (!data) {
NSLog(#"url: loading ERROR");
} else {
[self processFetchedData:data];
}
[self schedulePollWithDelay:15];
}
- (void)processFetchedData:(NSData *)responseData {
NSError* error;
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:responseData //1
options:kNilOptions
error:&error];
NSNumber* spot = [json objectForKey:#"amount"];
float spotfloat = [spot floatValue];
dispatch_sync(dispatch_get_main_queue(), ^{
// update your UI or model here
});
}
I'm trying to implement a background fetch method to get new data, but it's giving me an error with an NSMutable Dictionary. Here's my code
In my appDelegate under performFetchWithCompletionHandler I have:
UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
id topViewController = navigationController.topViewController;
if ([topViewController isKindOfClass:[viewController class]])
{
[(viewController*)topViewController autologin];
}
else
{
completionHandler(UIBackgroundFetchResultNewData);
}
This calls auto login in my view controller
- (void) autologin
{
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSString* username = [defaults valueForKey:#"username"];
NSString* password = [defaults valueForKey:#"password"];
[self login:username password:password];
}
Which then calls login
- (void)login:(NSString*)username password:(NSString*) password
{
NSDictionary *login = [[NSDictionary alloc] initWithObjectsAndKeys:username, #"username", password, #"password", NO, #"showNotification", nil];
NSOperationQueue* backgroundQueue = [NSOperationQueue new];
ch = [[backgroundProcess alloc] init];
NSInvocationOperation* operation = [[NSInvocationOperation alloc] initWithTarget:ch selector:#selector(runEvents:) object:login];
[backgroundQueue addOperation:operation];
operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(checkStatus) object:nil];
[backgroundQueue addOperation:operation];
}
Everything works if my app is running in the foreground and I call the login function, but with the performFetchWithCompletionHandler as soon as it hits
NSDictionary *login = [[NSDictionary alloc] initWithObjectsAndKeys:username, #"username", password, #"password", NO, #"showNotification", nil];
I get EXC_BAD_ACCESS. Any help would be appreciated!
Not sure why you are only crashing from the background. One error I see is that you can't use "NO" (a scalar BOOLEAN value) in a dictionary directly. NSDictionary objects can only contain objects. You need to convert the NO value to an NSNumber. With recent versions of Objective C you can use the syntax #(NO) to convert a scalar to an NSNumber. This is the equivalent of
[NSNumber numberWithBool: NO];
I doubt if that is the source of your crash, but it is an error in your code.
I don't know the object ownership rules of NSInvocationOperation off the top of my head. Since the introduction of GCD I haven't used NSOperations or NSOperationQueues. You might want to think about using GCD based calls instead.
I'm new using dispatch_queue_t and I have two problems with my app which I think that could be solved using GCD. The first one is that in the main view (ViewController) I have a label which is actualized by another class (AudioViewController), and when I do any user interaction in ViewController the label stop to actualize, so I think if I use dispatch_queue_t this problem will be solved.
The second thing is that in the same main view (ViewController) when I press a contribution I call another class (ContributionViewController) and this class accesses just a instance variable of another class (AudioViewController) which is refreshed all the time. When I start contribution, I made a loop to get more than one value to make some calculus with them and those values are all the same though.
I'll put some code here trying to clear the things.
ViewController.m
- (IBAction)makeContribution:(id)sender
{
NSLog(#"A: Contribution button clicked");
NSLog(#"-= START CONTRIBUTION =-");
cvc = [[ContributionViewController alloc] init];
cvc.avc = self.avc;
// Get NUM_CONTRIBUTIONS contributions to make average.
int contContribution;
for (contContribution = 0; contContribution < NUM_CONTRIBUTIONS; contContribution++) {
[cvc getEachContribution];
}
// Make average
[cvc makeAverage:NUM_CONTRIBUTIONS];
[cvc release];
}
AudioViewController.m
- (void)audioInitializationWithTimeInterval:(float)time
{
NSDictionary* recorderSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kAudioFormatLinearPCM],AVFormatIDKey,
[NSNumber numberWithInt:44100],AVSampleRateKey,
[NSNumber numberWithInt:1],AVNumberOfChannelsKey,
[NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,
[NSNumber numberWithBool:NO],AVLinearPCMIsBigEndianKey,
[NSNumber numberWithBool:NO],AVLinearPCMIsFloatKey,
nil];
NSError* error;
NSURL *url = [NSURL fileURLWithPath:#"/dev/null"];
recorder = [[AVAudioRecorder alloc] initWithURL:url settings:recorderSettings error:&error];
//enable measuring
//tell the recorder to start recording:
[recorder record];
if (recorder) {
[recorder prepareToRecord];
recorder.meteringEnabled = YES;
[recorder record];
levelTimer = [NSTimer scheduledTimerWithTimeInterval:time target:self selector:#selector(levelTimerCallback:) userInfo:nil repeats:YES];
}
else
NSLog(#"%#",[error description]);
}
- (void)levelTimerCallback:(NSTimer *)timer
{
//NSLog(#"-= AVC =-");
[recorder updateMeters];
db = [recorder averagePowerForChannel:0] - DBOFFSET;
db += 120;
db = db < 0 ? 0 : db;
vc.lab_decibel.text = [NSString stringWithFormat:#"%0.0f", db];
}
ContributionViewController.m
- (void)getEachContribution
{
actualContribution = self.avc.db;
NSLog(#"Actual contribution: %f", actualContribution);
NSLog(#"Sum before: %0.2f", sumContribution);
sumContribution += actualContribution;
NSLog(#"Sum After: %0.2f", sumContribution);
}
- (void)makeAverage:(int)numOfContributions
{
self.average = self.sumContribution / numOfContributions;
NSLog(#"Average: %0.2f", self.average);
}
So, the main thing is dispatch_queue_t is going to solve my problems and how to do that? I've tried to put dispatch_queue_t on AudioViewController, ContributionViewController and ViewController, but the first didn't update the label, the second crashed and the third one the label still with 0 value.
Thanks for any tips to solve this problem.
EDIT 01:
The decibel label changes all the time.
Ok, this is starting to make sense. I didn't understand the mapView, contribution, etc. language. It was entirely cryptic, but I think I'm starting to understand your question. As I understand it, your question is why your user interface is not being updated. Ok, the big answer to your question is that you don't need GCD if you don't want to. The NSTimer does what you need.
Now, you say that your continuously updated db field is no longer updated. There's absolutely no reason why it shouldn't continue. I've tried it, and it works fine. There's something going on there. Personally, when I've got a NSTimer that is updating my UI, I make sure to turn it off in viewDidDisappear (no point in updating your UI if it's not visible) and always turn it on in viewDidAppear. When I did this, whenever I returned to my main view with the db numbers, it happily resumed. Or if I wanted another view controller to get the notifications, I just turned it back on using a delegate.
That might look like the following.
First, you probably want a delegate protocol:
// DecibelNotifierDelegate.h
#import <Foundation/Foundation.h>
#protocol DecibelNotifierDelegate <NSObject>
#required
- (void)decibelNotifierUpdate:(CGFloat)db;
#end
And then, you want to define your DecibelNotifier class:
// DecibelNotifier.h
#import <Foundation/Foundation.h>
#import "DecibelNotifierDelegate.h"
#interface DecibelNotifier : NSObject
#property (nonatomic, retain) id<DecibelNotifierDelegate> delegate;
#property CGFloat db;
- (void)startWithInterval:(float)time target:(id<DecibelNotifierDelegate>)delegate;
- (void)stop;
#end
and
// DecibelNotifier.m
#import "DecibelNotifier.h"
#import "AVFoundation/AVFoundation.h"
#interface DecibelNotifier ()
{
AVAudioRecorder *_recorder;
NSTimer *_levelTimer;
}
#end
// note, your code makes reference to DBOFFSET and I don't know what that is. Update this following line accordingly.
#define DBOFFSET 0
#implementation DecibelNotifier
#synthesize delegate = _delegate;
#synthesize db = _db;
- (id)init
{
self = [super init];
if (self)
{
NSDictionary* recorderSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kAudioFormatLinearPCM],AVFormatIDKey,
[NSNumber numberWithInt:44100],AVSampleRateKey,
[NSNumber numberWithInt:1],AVNumberOfChannelsKey,
[NSNumber numberWithInt:16],AVLinearPCMBitDepthKey,
[NSNumber numberWithBool:NO],AVLinearPCMIsBigEndianKey,
[NSNumber numberWithBool:NO],AVLinearPCMIsFloatKey,
nil];
NSError* error;
NSURL *url = [NSURL fileURLWithPath:#"/dev/null"];
_recorder = [[AVAudioRecorder alloc] initWithURL:url settings:recorderSettings error:&error];
if (!_recorder)
NSLog(#"%#",[error description]);
}
return self;
}
- (void)startWithInterval:(float)time target:(id<DecibelNotifierDelegate>)delegate
{
if (_recorder)
{
self.delegate = delegate;
[_recorder prepareToRecord];
_recorder.meteringEnabled = YES;
[_recorder record];
_levelTimer = [NSTimer scheduledTimerWithTimeInterval:time target:self selector:#selector(levelTimerCallback:) userInfo:nil repeats:YES];
}
}
- (void)stop
{
[_recorder stop];
[_levelTimer invalidate];
_levelTimer = nil;
self.delegate = nil;
}
- (void)levelTimerCallback:(NSTimer *)timer
{
[_recorder updateMeters];
CGFloat db = [_recorder averagePowerForChannel:0] - DBOFFSET;
db += 120;
db = db < 0 ? 0 : db;
_db = db;
[self.delegate decibelNotifierUpdate:db];
}
#end
So, the first view controller that wants the notifications might have a property:
#property (strong, nonatomic) DecibelNotifier *dBNotifier;
You can initialize it with:
self.dBNotifier = [[DecibelNotifier alloc] init];
You can then turn on notifications with:
[self.dBNotifier startWithInterval:0.25 target:self];
So the question is when you turn these notifications on and off. I do it in viewWillAppear and viewWillDisappear. You can also pass the dbNotifier to other view controllers and they can get notifications, too.
Like I said, I tried this out and it works fine, with any view controller that wants to get notifications can just turn it on and specify itself as the delegate, and you're off to the races.