I have an app that syncs data with the server on every launch with a Rails backend. The API fully works, but I'm having an issue with the interface becoming unresponsive when the interface needs to be refreshed.
I'm currently using GCD to start the sync:
Sync *sync = [[Sync alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[sync force_sync:#""];
[appModel updateSyncTime:current_time];
});
Once the sync is completed, I have the Sync object sending a NSNotification to the app so that the interface refreshes:
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"Sync-Completed" object:self];
});
When the notification is received, my other classes just simply reload the data in the UITableView, like so:
- (void)notificationReceived:(NSNotification *)notification {
[dataTable reloadData];
}
Up until the point the notification is received, the UI is completely responsive. Once the notification is received, you can't scroll UITableViews among other UI elements until the refresh is completed. I'm sure there's a better way to do this, but how?
Thanks in advance!
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"Sync-Completed" object:self];
});
Is not Async - you just dumped a huge amount of refresh work on your main queue (the processing of the sync-completed notification)- so your app is going to lock up unless you address the code that updates the UI.
Also, using the global queue for a heavy IO operation is not necessarily a good idea. I have seen cases where just doing that will lock up the UI - so make sure its the notification and not also the sync itself.
If you find it is the sync itself - create your own NSOperation queue and create an NSOperation out of that block.
Your mileage may vary but in general using the global queue has not worked for async IO for me.
Related
I have a model object and a window controller. I would like them to be able to communicate via notifications. I create both during the App Delegate's -applicationDidFinishLaunching: method. I add the observers after the window controller's window is loaded, like this:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
WordSetWindowController* windowController = [[WordSetWindowController alloc] initWithWindowNibName:#"WordSetWindowController"];
model = [[WordSetModel alloc] init];
NSWindow* window = windowController.window;
[[NSNotificationCenter defaultCenter] addObserver:windowController
selector:#selector(handleNotification:)
name:kNotification_GeneratingPairs
object:model];
[[NSNotificationCenter defaultCenter] addObserver:windowController
selector:#selector(handleNotification:)
name:kNotification_ProcessingPairs
object:model];
[[NSNotificationCenter defaultCenter] addObserver:windowController
selector:#selector(handleNotification:)
name:kNotification_UpdatePairs
object:model];
[[NSNotificationCenter defaultCenter] addObserver:windowController
selector:#selector(handleNotification:)
name:kNotification_Complete
object:model];
[model initiateSearch];
}
The -iniateSearch method kicks off some threads to do some processor-intensive calculations in the background. I'd like those threads to send notifications so I can update the UI while processing is occurring. It looks like this:
- (void)initiateSearch;
{
[[NSNotificationCenter defaultCenter] postNotificationName:kNotification_GeneratingPairs
object:self];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ... do the first part of the calculations ...
// Notify the UI to update
self->state = SearchState_ProcessingPairs;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kNotification_ProcessingPairs
object:self];
});
// ... Do some more calculations ...
// Notify the UI that we're done
self->state = SearchState_Idle;
dispatch_sync(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kNotification_Complete
object:self];
});
});
}
The first notification works properly, but none of the notifications that happen in the dispatch_async() call ever cause the notification handler to be called. I've tried calling -postNotificationName:: both on the background thread and the UI thread (both by using dispatch_async(dispatch_get_main_queue(),...) and by calling -performSelectorOnMainThread:::) and neither had any effect.
Curious, I added a call via an NSTimer that waits 5 seconds after the big dispatch_async() call at the end of -initiateSearch: and found that even though that all occurs on the main UI thread, it also does not fire the notification handler. If I simply call postNotification::: immediately after the dispatch_async() call returns, it works properly, though.
From this, I'm concluding that the observers are somehow getting removed from the notification center, despite the fact that my code never calls -removeObserver:. Why does this happen, and how can I either keep it from happening or where can I move my calls to -addObserver so that they aren't affected by this?
Is suspect your window controller is getting deallocated.
You assign it to WordSetWindowController* windowController, but that reference disappears at the end of -applicationDidFinishLaunching:, and likely your window controller with it.
Since the window itself is retained by AppKit while open, you end up with an on-screen window without a controller. (Neither NSWindow nor NSNotificationCenter maintain strong references to its controller/observers.)
The initial notification works because those are posted before -applicationDidFinishLaunching: ends and/or the autorelease pool for that event is drained.
Create a strong reference to your window controller in your application delegate, store the window controller's reference there, and I suspect everything will work as advertised.
Something very similar happened to me with a window controller that was also a table delegate; the initial setup and delegate messages would work perfectly, but then later selection events mysteriously disappeared.
I have a really light ViewController, it does nothing in viewDidLoad. I'm pushing this view on top of a navigationController. The method who does this action is called from inside a block. After the call to showView I added an NSLog, and that log prints in the console really fast, but the view takes a lot to load... I really don't understand what maybe happening... any idea???
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
[self showView];
NSLog(#"EXECUTED");
});
- (void) showView{
TestViewController *test = [[TestViewController alloc]init];
[self.navigationController pushViewController:test animated:NO];
}
From the docs for ABAddressBookRequestAccessWithCompletion:
The completion handler is called on an arbitrary queue. If your app uses an address book throughout the app, you are responsible for ensuring that all usage of that address book is dispatched to a single queue to ensure correct thread-safe operation.
You should make sure your UI code is called on the main thread.
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error {
dispatch_async(dispatch_get_main_queue(), ^{
[self showView];
NSLog(#"EXECUTED");
});
});
This might not be the only problem, but according to the docs, the completion handler passed to ABAddressBookRequestAccessWithCompletion is called on an arbitrary queue. -showView should only be called on the main queue since it is dealing with UIViewController objects.
The other thing to ask is what else is happening on the main queue? Are there other long-running tasks that could be blocking UI updates?
I’m building an application which supports both video playback and recording (not simultaneously, it’s just two separate features it provides). In order to make the videos play after the app enters background and gets back, I had to add an App plays audio item to Required background modes in the plist (I’m using MPMoviePlayerController for playback).
This, however, causes a problem with my video recording (I’m using UIImagePickerController for it). Basically, even after the picker is dismissed (either by the Cancel button or when it’s finished picking media), the app still keeps the audio recording session running.
If I remove the App plays audio item from the plist, the ImagePickerController’s audio session stops misbehaving, but then I can’t resume the playback of MPMoviePlayerViewController on switching to app from background mode.
Is there a way I can customise the handling of the audio session so that both the MPMoviePlayerController and UIImagePickerController can work properly?
yes, there is a way you can customize the handling of the audio session for what you desire: don't try to set the App plays audio setting.
instead, in your AppDelegate code (which is usually in AppDelegate.m from the simple wizard projects provided), supply code in the applicationWillResignActive: method that will stop the playback in your MPMoviePlayerController, and then use applicationDidBecomeActive: to resume the playback at the point at which you paused it if so desired.
this will not only allow the video to be resumed after a temporary pause, but it will allow you to save the state so that the video can be resumed in case the app is removed from memory or in case the user causes it to be quit.
You can scratch the background modes and instead use notifications to pause/resume your player. See UIApplicationDidBecomeActiveNotification and UIApplicationWillResignActiveNotification in the UIApplication class reference.
You can snag some code and see this implemented in this class. Here is some of the relevant code from that class:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(_didBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(_willResignActive:)
name:UIApplicationWillResignActiveNotification
object:nil];
- (void) _didBecomeActive:(NSNotification*)notification {
if (_wasPlaying) {
[_movieController play];
}
}
- (void) _willResignActive:(NSNotification*)notification {
_wasPlaying = _movieController.currentPlaybackRate != 0.0;
if (_wasPlaying) {
[_movieController pause];
}
}
I would like show my view immediately when I call it. I don't know how to make the view show.
-(IBAction) showProgress: (id) sender {
progressViewController *progress = [[progressViewController alloc] initWithNibName:#"progressViewController" bundle:NULL];
[self.view addSubview:progress.view];
[self someFunctionWhichTakesAgesToBeDone];
}
It's called from current UIViewController. And the view appears after the long function. How can I show it before the long funcion? Thanks for answer.
Use GCD (Grand Central Dispatch) which is the simplest way (and recommended by Apple), the code will then be:
-(IBAction) showProgress: (id) sender {
progressViewController *progress = [[progressViewController alloc] initWithNibName:#"progressViewController" bundle:NULL];
[self.view addSubview:progress.view];
// Heavy work dispatched to a separate thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(#"dispatched");
// Do heavy or time consuming work
[self someFunctionWhichTakesAgesToBeDone];
// When finished call back on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
// Return data and update on the main thread
});
});
}
It´s two blocks. The first one does the heavy work on a separate thread and then a second block is called when the heavy work is finished so that changes and UI updates are done on the main thread, if needed.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/nsobject_Class/Reference/Reference.html
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait`
use
[self.view performSelectorOnMainThread:#selector(addSubview:) withObject:progress.view waitUntilDone:YES]
or put your Sleep() function (i hope it's anything else, Sleep() func is really bad, as its been told) into another function MySleepFunc and call
[self performSelector:#selector(MySleepFunc) withObject:nil afterDelay:0.003]
instead of Sleep(3).
Read about multi-threading. In short, there's single UI thread that does drawing, accepting user events and so on. If you pause it with sleep() or any other blocking method, nothing will be shown/redrawn and no events will be processed. You have to make your HTTP request from background thread.
Regarding the Apple documentation there is no way to handle the phone state while the app is suspended:
https://developer.apple.com/documentation/coretelephony/ctcallcenter
"While it is suspended, your application does not receive call events"
Is this also true for the "background" state? (As the background state is not the same with the "suspended" app state regarding the states described in the Apple documentation)
https://web.archive.org/web/20140824215114/https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html
I'm handling the phone state using the following code:
CTCallCenter *callCenter = [[CTCallCenter alloc] init];
callCenter.callEventHandler=^(CTCall* call)
{
//call state
};
I have added a local notifications into the callEventHandler block in order to check if a call events will be received while my app is in background state but is seams that the block is not executed ( my app has a background support and all received events (via TCP) are handled correctly while the app is in background )
All tests that I've done I can't receive any using callEventHandler when the application is in background. But, when the application is in foreground, all nicely work.
The socket works, because iOS handles it for you app and deliver the packtes accordingly. But for that, you need to create a voip socket and add voip to UIBackgroundModes to your App-Info.plist.
You will not be able to monitor phone calls in the background using the callEventHandler...
However, according to this thread in apple dev forums, you can check the currentCalls periodically in the background to determine if calls are in progress.
Here is the code you should use (to check every seconds) :
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(phoneDetection) userInfo:nil repeats:YES];
}
- (void)phoneDetection {
_callCenter = [[CTCallCenter alloc] init]; // Here is the important part : instanciating a call center each time you want to check !
[_callCenter setCallEventHandler:^(CTCall *call) {
NSLog(#"Call detected");
}];
NSLog(#"%#", _callCenter.currentCalls);
}
I know some apps run soundless audio files in the background to prevent from being closed after 10 minutes of inactivity.
Bluetooth, location, and audio can prevent the app from being completely killed.
My app is able to detect incoming vs outgoing calls while in background because the app stays alive with location updates. We have used audio/bluetooth in the past.
Without some method of "keepAlive" the OS will suspend your app until some external stimulus reactivates it (push notification, user launch, etc...)