I'm working on a very simple iPhone game that involves choosing the right colored button as many times in a row based on a randomized voice prompt. I have it set up so that if the button is one color and gets clicked, it always goes to a hard-coded color every time (e.g. if you click red, it always turns blue). The color change method is set up in an IBOutlet. I have a timer set up in a while loop, and when the timer ends it checks if the player made the right selection. The problem is that the button color change does not occur until after the timer runs out, and this causes a problem with the method used to check the correct answer. Is there a way to make this color change happen instantly? From what I've searched I know it has something to do with storyboard actions not occurring until after code executes, but I haven't found anything with using a timer. Here is a section of the method that calls the timer if the answer is correct:
BOOL rightChoice = true;
int colorNum;
NSDate *startTime;
NSTimeInterval elapsed;
colorNum = [self randomizeNum:middle];
[self setTextLabel:colorNum];
while (rightChoice){
elapsed = 0.0;
startTime = [NSDate date];
while (elapsed < 2.0){
elapsed = [startTime timeIntervalSinceNow] * -1.0;
NSLog(#"elapsed time%f", elapsed);
}
rightChoice = [self correctChoice:middleStatus :colorNum];
colorNum = [self randomizeNum:middle];
}
One of two things stood out
You're using a while loop as a timer, don't do this - the operation is synchronous.
If this is run on the main thread, and you code doesn't return, your UI will update. The mantra goes: 'when you're not returning you're blocking.'
Cocoa has NSTimer which runs asynchronously - it is ideal here.
So let's get to grips with NSTimer (alternatively you can use GCD and save a queue to an ivar, but NSTimer seems the right way to go).
Make an ivar called timer_:
// Top of the .m file or in the .h
#interface ViewController () {
NSTimer *timer_;
}
#end
Make some start and stop functions. How you call these is up to you.
- (void)startTimer {
// If there's an existing timer, let's cancel it
if (timer_)
[timer_ invalidate];
// Start the timer
timer_ = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(onTimerFinish:)
userInfo:nil
repeats:NO];
}
- (void)onTimerFinish:(id)sender {
NSLog(#"Timer finished!");
// Clean up the timer
[timer_ invalidate];
timer_ = nil;
}
- (void)stopTimer {
if (!timer_)
return;
// Clean up the timer
[timer_ invalidate];
timer_ = nil;
}
And now
Put your timer test code in the onTimerFinish function.
Make an ivar that stores the current choice. Update this ivar when a choice is made and make the relevant changes to the UI. Call stopTimer if the stop condition is met.
In the onTimerFinished you can conditionally call and startTimer again if you desire.
Hope this helps!
I am using a UITableView in combination with a UISearchDisplayController to do search on the table data array.
for each row shown an image is fetched from the network and cached locally.
the problem is when the user types a search term, each key triggers a call to shouldReloadTableForSearchString which I use to start the online image fetch. If the user types fast it will create several network requests, by the time the network fetch is completed, the row that triggered it might not exist anymore since the search was changed.
I figured I need to wait until the user stops typing before making the network request, but could not yet find a way to do so with UISearchBar and UITableView..
any ideas?
You need to setup a timer upon every key stroke. Lets say you assume that a user has stopped typing if the last key stroke was received 1 second ago. Instead of returning YES from shouldReloadTableForSearchString:, you should schedule a timer. If another key stroke is received before 1 second elapses, invalidate the timer and reset it.
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
if(myTimer) //a timer is already scheduled. Invalidate it.
{
[myTimer invalidate]; //myTimer is an iVar
myTimer = nil;
}
myTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:0 target:self selector:#selector(search:) userInfo:searchString repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
return NO;
}
- (void)search:(NSTimer*)theTimer
{
//make network request here
}
Call [self.searchDisplayController.searchResultsTableView reloadData]; when the response from the network call is received.
I am using MBProgressHUD to display a progress indicator. The delegate that is called when the indicator is shown is:
- (void)myTask {
while (self.show_progress == NO){
}
}
basically when it goes out of the loop it dismisses the indicator. Now the issue is that I would like do something more in this method. I would like to check for how long has the indicator been spinning for, if it has been more than 5 seconds then I would like to re-load the request. The question is how do I check for this?
This is just to prevent the apps waiting for an infinite amount of time just in case the response never got back or got stuck somewhere.
I'm not familiar with MBProgressHUD , but on general terms you could do the following:
When you first make the request do:
NSDate *startTime = [NSDate date];
Then whenever you want to check how long has it been:
NSTimeInterval timePassed = -[startTime timeIntervalSinceNow];
timePassed will have the value, in seconds, of how long has it been since you made your request. May be you should consider using NSTimer for this: Schedule a timer that will fire 5 seconds after you performed your request, if it triggers cancel the request but if you receive a response before the timer triggers invalidate the timer.
If I understand correctly, your code just waits until the property show_progress becomes NO. I don't know why your code does this, it seems a little inelegant. If you want to keep it this way, at least use a condition lock to prevent the 100% CPU usage:
Prepare the condition lock like this:
NSConditionLock *progressLock = [[NSConditionLock alloc] initWithCondition:0];
In your second thread, once your loading stuff or whatever finishes, change the condition like this:
[progressLock lock];
[progressLock unlockWithCondition:1];
And in your method, do this:
- (void)myTask {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:5];
if ([progressLock lockWhenCondition: 1 beforeDate:timeoutDate) {
// we aquired the lock, processing has finished
[progressLock unlock];
} else {
// we didn't aquire the lock because the 5 seconds have passed
// reload the request or do whatever you want to do
}
}
This method waits 5 seconds, and then times out. It uses no CPU in those 5 seconds, because it waits for a signal at the lockWhenCondition:beforeDate: call.
The way I've gone about similar situations is to set up a timer. The basic concept would be to start the timer when the indicator starts spinning. Then invalidate it when the indicator stops. Else if it goes on for 5 seconds, execute your method.
So in your header, you'll want
NSTimer *myTimer;
then in the implementation, when you start the indicator spinning,
[indicator startAnimating];
if (myTimer != nil) {
[myTimer invalidate];
myTimer = nil;
}
myTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(reloadRequest) serInfo:nil repeats:NO];
when you stop the indicator from spinning, send [myTimer invalidate]; and myTimer = nil;. If the specified time is reached beforehand, reload the request in your reloadRequest method
I've heavily researched for a proper solution but it didn't work out functional:
My iPhone app updates data via NSURL requests on user demand. Each file loaded online is only 1.5k in size but one update can consist of 400 of such files. So I do the download stuff in a separate thread which is cancelable and during the update there is an UIAlertView with process indication and cancel button. The thing may run 1...3 minutes, so it can exceed the timeout the device is staying alive or other things happen and my app will go background.
When I do nothing when applicationDidEnterBackground is called, I realize that my app is suspended and also the worker thread. It wakes up and continues work when the app is in foreground again. So far so good. I get into trouble when I press the cancel button after being in foreground again - then the thread reacts as being cancelled (as it should) but immediately runs again and crashes at the end (with error codes somewhere deep in Apple frameworks). It works perfectly to cancel the thread as long as the app keeps staying in foreground - so my idea is to stop/cancel it when applicationDidEnterBackground is entered. I've tried some things to do so but each attempt ends in the fact that the worker thread is suspended at that moment applicationDidEnterBackground is called, so I can't cancel the thread and wait for that. One example I tried is this:
diagView* viewDiagCtrl = startDiag.diagViewControl;
if (viewDiagCtrl != nil && viewDiagCtrl.actionThread != nil)
{
// UIBackgroundTaskIdentifier bgTask is instance variable
NSAssert(self->bgTask == UIBackgroundTaskInvalid, nil);
bgTask = [application beginBackgroundTaskWithExpirationHandler:
^{
dispatch_async(dispatch_get_main_queue(),
^{
NSLog(#"stopping actions definitely");
[viewDiagCtrl stopBackGroundActivity:self];
[application endBackgroundTask:self->bgTask];
self->bgTask = UIBackgroundTaskInvalid;
});
}];
dispatch_async(dispatch_get_main_queue(),
^{
// Start with 10ms time boxes
NSTimeInterval ti = 0.01;
while ([application backgroundTimeRemaining] > 1.0)
{
if (![viewDiagCtrl.actionThread isExecuting])
break;
NSDate* date = [NSDate dateWithTimeIntervalSinceNow:ti];
// Let the current run-loop do it's magif for one time-box.
[[NSRunLoop currentRunLoop] runMode: NSRunLoopCommonModes
beforeDate: date];
// Double the time box, for next try, max out at 1000ms.
ti = MIN(1.0, ti * 2);
}
[viewDiagCtrl stopBackGroundActivity:self];
[application endBackgroundTask:self->bgTask];
self->bgTask = UIBackgroundTaskInvalid;
});
}
I'm not experienced with the whole queue stuff and found such a construct somewhere. I assume that all things dispatched work in the main thread and the worker thread remains suspended so that I don't have any chance to cancel it. Any ideas to come around with that?
I've also read about attempts to do everything without multithreading - but I don't really appreciate that.Is there maybe some useful link to handle the "go background" situation properly?
You can wrap your thread work in the background thread like:
...
[self performSelectorInBackground:#selector(backgroundThread)
withObject:nil];
...
-(void)backgroundThread
{
// do a download once a minute in a background thread - dont let the system suspend us
while(true)
{
BOOL expire = NO;
bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{expire = YES;}];
while(!downloadComplete && !expire)
{
//do your multiple file downloads here;
}
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
[NSThread sleepForTimeInterval:60];
}
}
What this will do is continue the task in progress which will continue when the app is backgrounded - I do this is several places in my heavily threaded/network intensive app.
AFAIK: You don't have to wait until the app is being backgrounded to call beginBackgroundTask - you should call it any time your app has a function it needs to complete without being interrupted/suspended.
I also use NSURLRequest with:
[request setNetworkServiceType:NSURLNetworkServiceTypeVoIP];
(and use the voip background mode in the supportedbackgroundmodes flag)
How can I detect lock/unlock events on the iPhone? Assuming it's only possible for jailbroken devices, can you point me to the correct API?
By lock events, I mean showing or hiding the Lock Screen (which might need a password to unlock, or not).
You can use Darwin notifications, to listen for the events. From my testing on a jailbroken iOS 5.0.1 iPhone 4, I think that one of these events might be what you need:
com.apple.springboard.lockstate
com.apple.springboard.lockcomplete
Note: according to the poster's comments to a similar question I answered here, this should work on a non-jailbroken phone, too.
To use this, register for the event like this (this registers for just the first event above, but you can add an observer for lockcomplete, too):
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), //center
(void*)self, // observer (can be NULL)
lockStateChanged, // callback
CFSTR("com.apple.springboard.lockstate"), // event name
NULL, // object
CFNotificationSuspensionBehaviorDeliverImmediately);
where lockStateChanged is your event callback:
static void lockStateChanged(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
NSLog(#"event received!");
if (observer != NULL) {
MyClass *this = (MyClass*)observer;
}
// you might try inspecting the `userInfo` dictionary, to see
// if it contains any useful info
if (userInfo != nil) {
CFShow(userInfo);
}
}
The lockstate event occurs when the device is locked and unlocked, but the lockcomplete event is only triggered when the device locks. Another way to determine whether the event is for a lock or unlock event is to use notify_get_state(). You'll get a different value for lock vs. unlock, as described here.
Round about answer:
Application will resign active gets called in all sorts of scenarios... and from all my testing, even if your application stays awake while backgrounded, there are no ways to determine that the screen is locked (CPU speed doesn't report, BUS speed remains the same, mach_time denom / numer doesn't change)...
However, it seems Apple does turn off the accelerometer when the device is locked... Enable iPhone accelerometer while screen is locked
(tested iOS4.2 on iPhone 4 has this behavior)
Thus...
In your application delegate:
- (void)applicationWillResignActive:(UIApplication *)application
{
NSLog(#"STATUS - Application will Resign Active");
// Start checking the accelerometer (while we are in the background)
[[UIAccelerometer sharedAccelerometer] setDelegate:self];
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:1]; // Ping every second
_notActiveTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(deviceDidLock) userInfo:nil repeats:NO]; // 2 seconds for wiggle
}
//Deprecated in iOS5
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
NSLog(#"STATUS - Update from accelerometer");
[_notActiveTimer invalidate];
_notActiveTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(deviceDidLock) userInfo:nil repeats:NO];
}
- (void)deviceDidLock
{
NSLog(#"STATUS - Device locked!");
[[UIAccelerometer sharedAccelerometer] setDelegate:nil];
_notActiveTimer = nil;
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
NSLog(#"STATUS - Application did become active");
[[UIAccelerometer sharedAccelerometer] setDelegate:nil];
[_notActiveTimer invalidate];
_notActiveTimer = nil;
}
I know... It's kind of a hack, but it has worked like a charm for me so far. Please update if you see any issues that prevent this from working.
There is a prettier way of telling apart task switching and screen locking-originated applicationWillResignActive: callbacks which doesn't even involve undocumented features such as the accelerometer state.
When the app is moving to the background, the app delegate is first sent an applicationWillResignActive:, then an applicationDidEnterBackground:. When the app is interrupted by pressing the Lock button or by an incoming phone call, the latter method is not called. We can use this information to distinguish between the two scenarios.
Say you want to be called back in the screenLockActivated method if the screen gets locked. Here's the magic:
- (void)applicationWillResignActive:(UIApplication*)aApplication
{
[self performSelector:#selector(screenLockActivated)
withObject:nil
afterDelay:0];
}
- (void)applicationDidEnterBackground:(UIApplication*)aApplication
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void)screenLockActivated
{
NSLog(#"yaay");
}
Explanation:
By default, we assume that every call to applicationWillResignActive: is because of an active->inactive state transition (as when locking the screen) but we generously let the system prove the contrary within a timeout (in this case, a single runloop cycle) by delaying the call to screenLockActivated. In case the screen gets locked, the system finishes the current runloop cycle without touching any other delegate methods. If, however, this is an active->background state transition, it also invokes applicationDidEnterBackground: before the end of the cycle, which allows us to simply cancel the previously scheduled request from there, thus preventing it from being called when it's not supposed to.
Enjoy!
As of the time of writing there are two fairly reliable ways to detect device locking:
Data Protection
By enabling the Data Protection entitlement your app can subscribe to the applicationProtectedDataWillBecomeUnavailable: and applicationProtectedDataDidBecomeAvailable: notifications to determine with high probability when a device that uses passcode/TouchID Authentication is locked/unlocked. To determine if a device uses a passcode/TouchID LAContext can be queried.
Caveats: This method relies on the "protected data becoming unavailable" coinciding with the phone being locked. When the phone is using TouchID and the sleep/lock button is pressed then the phone is locked, protected data becomes unavailable, and a passcode will immediately be required to unlock it again. This means that protected data becoming unavailable essentially indicates that the phone has been locked. This is not necessarily true when someone is using just a passcode since they can set the "requires passcode" time to anywhere from immediately to something like 4 hours. In this case the phone will report being able to handle protected data but locking the phone will not result in protected data becoming unavailable for quite some time.
Lifecycle Timing
If your app is in the foreground there will be a noticeable change in time difference between the two lifecycle events UIApplicationWillResignActiveNotification and UIApplicationDidEnterBackgroundNotification depending on what triggers them.
(This was tested in iOS 10 and may change in future releases)
Pressing the home button results in a significant delay between the two (even when the Reduced Motion setting is enabled):
15:23:42.517 willResignActive
15:23:43.182 didEnterBackground
15:23:43.184 difference: 0.666346
Locking the device while the app is open creates a more trivial (<~0.2s) delay between the two events:
15:22:59.236 willResignActive
15:22:59.267 didEnterBackground
15:22:59.267 difference: 0.031404
in iOS 8, you lock the screen or push the home button, all of those make app push in background, but you don't know which operator result in this. My solution same with Nits007ak,use notify_register_dispatch to get state.
#import <notify.h>
int notify_token
notify_register_dispatch("com.apple.springboard.lockstate",
¬ify_token,
dispatch_get_main_queue(),
^(int token)
{
uint64_t state = UINT64_MAX;
notify_get_state(token, &state);
if(state == 0) {
NSLog(#"unlock device");
} else {
NSLog(#"lock device");
}
}
);
As long as the app is running, in foreground or background. not suspend, you can get this event.
And you can use notify_token as parameter of notify_get_state to get current state anywhere, this is useful when you want know the state and the screen state don't change.
If passcode is set, you can use these event in AppDelegate
-(void)applicationProtectedDataWillBecomeUnavailable:(UIApplication *)application
{
}
- (void)applicationProtectedDataDidBecomeAvailable:(UIApplication *)application
{
}
Just import #import notify.h before using this code. enjoy!!
-(void)registerAppforDetectLockState {
int notify_token;
notify_register_dispatch("com.apple.springboard.lockstate", ¬ify_token,dispatch_get_main_queue(), ^(int token) {
uint64_t state = UINT64_MAX;
notify_get_state(token, &state);
if(state == 0) {
NSLog(#"unlock device");
} else {
NSLog(#"lock device");
}
NSLog(#"com.apple.springboard.lockstate = %llu", state);
UILocalNotification *notification = [[UILocalNotification alloc]init];
notification.repeatInterval = NSDayCalendarUnit;
[notification setAlertBody:#"Hello world!! I come becoz you lock/unlock your device :)"];
notification.alertAction = #"View";
notification.alertAction = #"Yes";
[notification setFireDate:[NSDate dateWithTimeIntervalSinceNow:1]];
notification.soundName = UILocalNotificationDefaultSoundName;
[notification setTimeZone:[NSTimeZone defaultTimeZone]];
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
});
}
From a lot of trial and error, discovered monitoring the blank screen, lock complete and lock state events gives a consistent lock screen indicator. You'll need to monitor a state transition.
// call back
void displayStatusChanged(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo)
{
// notification comes in order of
// "com.apple.springboard.hasBlankedScreen" notification
// "com.apple.springboard.lockcomplete" notification only if locked
// "com.apple.springboard.lockstate" notification
AppDelegate *appDelegate = CFBridgingRelease(observer);
NSString *eventName = (__bridge NSString*)name;
NSLog(#"Darwin notification NAME = %#",name);
if([eventName isEqualToString:#"com.apple.springboard.hasBlankedScreen"])
{
NSLog(#"SCREEN BLANK");
appDelegate.bDeviceLocked = false; // clear
}
else if([eventName isEqualToString:#"com.apple.springboard.lockcomplete"])
{
NSLog(#"DEVICE LOCK");
appDelegate.bDeviceLocked = true; // set
}
else if([eventName isEqualToString:#"com.apple.springboard.lockstate"])
{
NSLog(#"LOCK STATUS CHANGE");
if(appDelegate.bDeviceLocked) // if a lock, is set
{
NSLog(#"DEVICE IS LOCKED");
}
else
{
NSLog(#"DEVICE IS UNLOCKED");
}
}
}
-(void)registerforDeviceLockNotif
{
// screen and lock notifications
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), //center
CFBridgingRetain(self), // observer
displayStatusChanged, // callback
CFSTR("com.apple.springboard.hasBlankedScreen"), // event name
NULL, // object
CFNotificationSuspensionBehaviorDeliverImmediately);
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), //center
CFBridgingRetain(self), // observer
displayStatusChanged, // callback
CFSTR("com.apple.springboard.lockcomplete"), // event name
NULL, // object
CFNotificationSuspensionBehaviorDeliverImmediately);
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), //center
CFBridgingRetain(self), // observer
displayStatusChanged, // callback
CFSTR("com.apple.springboard.lockstate"), // event name
NULL, // object
CFNotificationSuspensionBehaviorDeliverImmediately);
}
To have the screen lock indicators run in the background, you need to implement background processing calling the following upon app launching.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.backgroundTaskIdentifier =
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
}];
[self registerforDeviceLockNotif];
}
If your app is running and the user locks the device your app delegate will receive a call to 'application Will Resign Active:'. If your app was running when locked, it will receive a call to 'application Did Become Active:' when the device is unlocked. But you get the same calls to your app if the user gets a phone call and then chooses to ignore it. You can't tell the difference as far as I know.
And if your app wasn't running at any of these times there is no way to be notified since your app isn't running.
The simplest way to get screen lock and unlock events are by adding event observers using NSNotificationCenter in your viewcontroller. I added the following observer in the viewdidload method. This is what i did:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationEnteredForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
Then I added the following selector to the viewcontroller. This selector will get called when the screen is unlocked.
- (void)applicationEnteredForeground:(NSNotification *)notification {
NSLog(#"Application Entered Foreground");
}
If you want to detect the event when screen gets locked, you can replace UIApplicationWillEnterForegroundNotification with UIApplicationDidEnterBackgroundNotification.