For a game I'm developing, I have several model classes that trigger notifications when their state changes. Then, the view subscribes to those notifications and can react on them.
I'm doing my unit tests for the model with OCUnit, and want to assert that the expected notifications were posted. For that, I'm doing something like this:
- (void)testSomething {
[[NSNotificationCenter defaultCenter] addObserver:notifications selector:#selector(addObject:) name:kNotificationMoved object:board];
Board *board = [[Board alloc] init];
Tile *tile = [Tile newTile];
[board addTile:tile];
[board move:tile];
STAssertEquals((NSUInteger)1, [notifications count], nil);
// Assert the contents of the userInfo as well here
[board release];
}
The idea is that the NSNotificationCenter will add the notifications to the NSMutableArray by calling its addObject: method.
When I run it, however, I see that addObject: is being sent to some other object (not my NSMutableArray) causing OCUnit to stop working. However, if I comment out some code (such as the release calls, or add a new unit test) everything starts working as expected.
I'm assuming this has to o with a timing issue, or NSNotificationCenter relying on the run loop in some way.
Is there any recommendation to test this? I know I could add a setter in Board and inject my own NSNotificationCenter, but I'm looking for a quicker way to do it (maybe some trick on how to replace the NSNotificationCenter dynamically).
Found the problem. When testing notifications you need to remove the observer after you have tested it. Working code:
- (void)testSomething {
[[NSNotificationCenter defaultCenter] addObserver:notifications selector:#selector(addObject:) name:kNotificationMoved object:board];
Board *board = [[Board alloc] init];
Tile *tile = [Tile newTile];
[board addTile:tile];
[board move:tile];
STAssertEquals((NSUInteger)1, [notifications count], nil);
// Assert the contents of the userInfo as well here
[board release];
[[NSNotificationCenter defaultCenter] removeObserver:notifications name:kNotificationMoved object:board];
}
If you fail to remove the observer, after a test runs and some local variables are released, the notification center will try to notify those old objects when running any subsequent test that triggers the same notification.
There are no timing issues or runloop related problems since everything in your code is non-concurrent and should be executed immediately. NSNotificationCenter only postpones notification delivery if you use an NSNotificationQueue.
I think everything is correct in the snippet you posted. Maybe there's an issue with the mutable array 'notifications'. Did you init and retain it correctly? Try to add some object manually instead of using the notification trick.
If you suspect your tests have timing issues - you may want to consider injecting your own notification mechanism into your board object (which is probably just a wrapper of the existing apple version).
That is:
Board *board = [[Board alloc] initWithNotifier: someOtherNotifierConformingToAProtocol];
Presumably your board object posts some notification - you would use your injected notifier in that code:
-(void) someBoardMethod {
// ....
// Send your notification indirectly through your object
[myNotifier pushUpdateNotification: myAttribute];
}
In your test - you now have a level of indirection that you can use for testing, so you can implement a test class the conforms to your AProtocol - and maybe counts up the pushUpdateNotification: calls. In your real code you encapsulate the code you probably already have in Board that does the notification.
This of course is a classic example of where MockObjects are useful - and there is OCMock which well let you do this without having to have a test class to do the counting (see: http://www.mulle-kybernetik.com/software/OCMock/)
your test would problably have a line something like:
[[myMockNotifer expect] pushUpdateNotification: someAttribute];
Alternatively you could consider using a delegate instead of notifications. There is a good pro/con set of slides here: http://www.slideshare.net/360conferences/nsnotificationcenter-vs-appdelegate.
Related
I'm working with XCode 4.6, and am trying to build a local notification feature on iOS that will execute a function upon reentering the app.Basically I would like to change the text in some of the labels and add some sound upon reentering the app. I thought I was on the right track, but only some parts of my code work when reentering the app via local notification.
First I added this function to my AppDelegate:
-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification{
NSLog(#"test1"); //this traces successfully
myAppViewController * controller = [myAppViewController alloc];
[controller doSomething]; //calling a function in my myAppViewController.m
}
I thought I had figured it out, but now only the NSLog works in my function in myAppViewController.m:
-(void)doSomething{
NSLog(#"do something"); //traces successfully
self.notificationTime.text=#"something else"; //nothing happens here, it works in other functions but not here
[self doSomethingElse]; //calling another function from this function for further testing
}
The next function is called....
-(void)doSomethingElse{
NSLog(#"do something else"); //this works
//this whole thing doesn't work -- no sound --
NSURL* url = [[NSBundle mainBundle] URLForResource:#"cash" withExtension:#"mp3"];
NSAssert(url, #"URL is valid.");
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
[self.player prepareToPlay];
[self.player play];
//this doesn't work again
self.notificationTime.text=#"something else";
}
I was hoping to get some general advice here and it would be much appreciated. If anyone knows a complete different way of solving the problem, that would be great as well!
The didReceiveLocalNotification method is only called when you application is running in the foreground. If you see a badge and click on the App to start it, then you need to process the local notification using application:willFinishLaunchingWithOptions: (or application:didFinishLaunchingWithOptions:) To get at your local notification in either of these two methods, use UIApplicationLaunchOptionsLocalNotificationKey as a key to the options dictionary.
Note, once you extract the local notification from the launch options, it is a viable approach to call your didReceiveLocalNotification method.
You shouldn't need to allocate a second instance of the app controller. You can just use self. If you do that, does the code work as expected?
I am using Core Data and was wondering if I am doing things correctly. I am opening my UIManagedDocument from a singleton object using the completion handler and block below.
[[self managedDocument] openWithCompletionHandler:^(BOOL success) {
if(success) {
NSLog(#"DOCUMENT: Success, Opened ...");
// TODO: Things to do when open.
// ...
// ...
}
}];
On my UIViewController I have setup an observer to watch for a UIDocumentStateChangedNotification to indicate that I can start working with the document.
- (void)awakeFromNib {
NSLog(#"%s", __PRETTY_FUNCTION__);
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(documentIsReady) name:UIDocumentStateChangedNotification object:nil];
}
This seams to work just fine, but I am conscious that I am not using the callback block. One solution might be to create my own notification and post that from the block, it does the same thing essentially but just makes the code more obvious to read. Any comments would be much appreciated.
I'd say that if you only need to notify one controller, once, and only when the document is opened (you have an app that uses a single UIManagedDocument that gets passed between controllers, like the CS193P demo), it would be better to leave only the code inside the completion block.
However, if your app is going to open and close the document many times, and multiple controllers have to be aware of that change, you should use notifications.
I am attempting to create an application that will initiate a call to a priority 1 contact on a call-center-like list.
Then, if that contact does not answer (let's forget the whole problem of answering machines here), I'd like to call the priority 2 contact, and so on, until one of them answers or I exhaust my list.
Is this possible?
I've tried the following:
Hook into the CTCallCenter.CallEventHandler event, and checking the call state for CTCallStateConnected and CTCallStateDisconnected, and I get it to respond to the fact that the call disconnected, without ever connecting, and then attempt to initiate another call like I did the first, but this second attempt just sits dead in the water.
Override the DidEnterBackground method, and periodically check the CTCall.CallState property, basically again trying to respond to a disconnect that was never connected, but this does not appear to work either
I also tried adding a short delay (1 second, 2.5 seconds and 10 seconds) after detecting the disconnected state before attempting the next dial, to allow for the phone application to "settle down" after aborting the call, this did not change anything.
I'm of the opinion that this is better solved at the destination of the phone call. I would either have the phone company configure a "follow me" service, use Twilio or some other 3rd party service (as already suggested), or configure my own PBX using something like Asterisk (Asterisk includes the ability to configure "follow me" type behavior). It provides you much more flexibility and control, even if you did find a way to do this natively in iOS.
Having said that, I did get this to work in iOS assuming the following:
Your app initiates the call.
The phone app is opened, dials the number, and disconnects.
The user explicitly returns to your app. If you managed to get the events while your app was backgrounded, I want to know more :-).
On return of control to your app, the phone events are sent and a new call is initiated.
I have the following snippet of code in my UIApplicationDelegate didFinishLaunchingWithOptions method:
// In appdelegate header, ct is declared as #property (strong, nonatomic) CTCallCenter *ct;
self.ct = [[CTCallCenter alloc] init];
self.ct.callEventHandler = ^(CTCall *call) {
if (call.callState == CTCallStateConnected) {
// do some state management to track the call
} else if (call.callState == CTCallStateDisconnected) {
// check that this is the expected call and setup the
// new phone number
NSURL *telURL = [NSURL URLWithString:myNewNumberURL];
[application openURL:telURL];
}
};
This will make the new call. I'm using the iOS 5 SDK; tested on an iPhone 4s.
EDIT:
Using Return to app behavior after phone call different in native code than UIWebView as a starting point, I've managed to get this to work. Note that I have punted on memory management for clarity. Assuming you use the web view technique for getting back to your app after the call is complete, try something like this in the call completed block:
else if (call.callState == CTCallStateDisconnected) {
// check that this is the expected call and setup the
// new phone number
NSURL *telURL = [NSURL URLWithString:myNewNumberURL];
dispatch_async(dispatch_get_main_queue(), ^{
UIWebView *callWebview = [[UIWebView alloc] init] ;
[self.window.rootViewController.view addSubview:callWebview];
[callWebview loadRequest:[NSURLRequest requestWithURL:telURL]];
// and now callWebView sits around until the app is killed....so don't follow this to the letter.
});
}
However, this may not quite give you what you want either. The user will get an alert on each call request, providing an opportunity to cancel the call.
You could use http://labs.twilio.com/twimlets/findme. You could have the app call a Twilio number and it could use findme to call all the numbers in order.
I didn't take a deeper look at it, but the Deutsche Telekom SDK might contain what you're looking after:
http://www.developergarden.com/fileadmin/microsites/ApiProject/Dokumente/Dokumentation/ObjectiveC-SDK-2.0/en/interface_voice_call_service.html
I really am not sure though (don't have time to really look at it at the moment) - I just remembered I'd read somewhere that they have an iOS SDK that is supposed to also handle call management, so I'm posting the link here for you to find out (and hopefully tell us if it works).
#pragma mark -
#pragma mark Call Handler Notification
-(void)notificationCallHandler {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(callReceived:) name:CTCallStateIncoming object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(callEnded:) name:CTCallStateDisconnected object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(callConnected:) name:CTCallStateConnected object:nil];
}
-(void)callEnded:(NSNotification*)notification {
NSLog(#"callEnded");
}
-(void)callReceived:(NSNotification*)notification {
NSLog(#"callReceived");
}
-(void)callConnected:(NSNotification*)notification {
NSLog(#"callConnected");
}
May this will help you
if you wanna setup a new call, while app is in background, i dont see any proper way for this, a lil hack could be, getting location update (because u can get location updates while app is in background), and location service automatically wakes up your application when new location data arrives, and small amount of time is given to application in which u can execute some code, in that time you may start a new call.
u can read further here:
search this ''Starting the Significant-Change Location Service'' in this link Location Aware programming guide
, and read the paragraph that is written after the code block.
I have an iPad app, and I want to do this
-(IBAction) clicked {
image=download(#"http://....."); // this is on the main thread
}
The download function is going to call a whole bunch of non blocking functions to download a file from the internet, but download itself shouldn't return until the image is downloaded.
While the program is waiting for the download at the image=download(...) line above, I want the UI to be able to still function, for example be able to scroll a UITableView, click another button etc.
So what I did was this inside the download function I used a RunLoop
-(void) download:(NSString *)url
{
BOOL stillDownloading=TRUE;
while(stillDownloading) {
stillDownloading=downloadAFwBytes(...);
CFRunLoopRunInMode(kCFRunLoopCommonModes, 0, YES);
}
}
I thought the CFRunLoopRunInMode function will keep pumping UI messages, touches, scrolls through the main UI thread so that the UI will keep working and not freeze until the download finished, but for some reason, it only works for a short time, and eventually the UI freezes.
Do you know why, or how to fix?
The download function is called everywhere in the program, that expects it to wait for the download, so I can't change it to non blocking at the moment.
The direct answer to your question is, no, this is not what CFRunLoopRunInMode does. What you are effectively trying to do is have the current run loop "yield" so execution can continue while the loading operation continues. This is not how iOS and run loops work. Your download function blocks the thread it is on until downloading is complete so the only solution to your issue is to change the implementation so that downloading occurs on a background thread and the objects that care are notified when it is complete. Here's a relatively small change that can get you on the right track. This overall topic (concurrency, managing background tasks) is a bigger discussion and there are different considerations/tradeoffs. I'll cut to the chase and hopefully get you on the right track.
Define a couple NSNotification's that your download method can post for interested objects to observe:
// in the .h file of the class performing the download
extern NSString * const MyClassLoadingDidStartNotification;
extern NSString * const MyClassLoadingDidFinishNotification;
// in the .m file of the class performing the download
NSString * const MyClassLoadingDidStartNotification = #"MyClassLoadingDidStart";
NSString * const MyClassLoadingDidFinishNotification = #"MyClassLoadingDidFinish";
In your download routine, do the download in the background and post the appropriate notifications:
-(void) download:(NSString *)url
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:MyClassLoadingDidStartNotification object:self];
BOOL stillDownloading=TRUE;
while(stillDownloading) {
stillDownloading=downloadAFwBytes(...);
}
[[NSNotificationCenter defaultCenter] postNotificationName:MyClassLoadingDidFinishNotification object:self];
});
}
In any object that initiates a download, observe and handle the notifications
// in any class that initiates a download
- (void)init...
{
self = [super init...];
if (self) {
// other initialization
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didStartLoading:) name:MyClassLoadingDidStartNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didFinishLoading:) name:MyClassLoadingDidFinishNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:MyClassLoadingDidStartNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:MyClassLoadingDidFinishNotification object:nil];
}
- (void)didStartLoading:(NSNotification *)notification
{
// update UI to show loading status (make sure you do UI changes on main thread)
// optionally check notification.object to ensure it's the loader class instance you care about
}
- (void)didFinishLoading:(NSNotification *)notification
{
// update UI to show loading status (make sure you do UI changes on main thread)
// optionally check notification.object to ensure it's the loader class instance you care about
}
Keep in mind that this is a very basic starting point. As you learn more and decide what you need you will definitely customize it. For example, you may want to add error handling, limit concurrent loading operations, provide other loading status, etc.
There are some architecture concerns with your question, but to properly download an image in the background and load it into a UIImageView or use it in any other way, I'd suggest taking a look at AFNetworking and read through their sample code for downloading resources.
Can you request to download the image on the background and then, return a static image asap in your method?
This way, the main thread returns fast and depending on your architecture, your background thread can update the image once it got it?
I think we need more details on your code (your download method is void so how is the downloaded data used?) to be able to really help.
Along with the 3 other guys telling you the same thing, I'll tell you to forget about your own run loop model, and just use the async download capabilities (assuming you're downloading over the net). You don't even have to build a thread, just start the async downloads and they will tell you when they're done. If you don't have the time to code it right, when will you have the time to fix your code?
Not sure if this is solved yet but can't you use?
[self.operationQueue addOperationWithBlock:^{
[self MethodName];
}];
and when you want something to happen in the UI like table updates then put this in the method code:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
////perform something here ////
}];
hope this helps.
I am writing an iPhone app in which I have three countdown timers, that each set a local notification when they are started by the user (IBAction).
My question is, how can I reference to an existing local notification elsewhere in the code? I have tried referencing it directly like this which crashes:
[[UIApplication sharedApplication] cancelLocalNotification:myNotification];
And like this after scheduling the notification and adding it to the user defaults:
In my scheduling method...
[myDefaults setObject:myNotification forKey:#"myNotification"];
...
And in my cancelling method...
NSUserDefaults *myDefaults = [NSUserDefaults standardUserDefaults];
[[UIApplication sharedApplication] cancelLocalNotification:[myDefaults objectForKey:#"myNotification"]];
[myDefaults synchronize];
My app crashes with a SIGABRT error on the cancelLocalNotification line above. Can anybody tell me where I am going wrong?
Thanks in advance!
Ok, I've found a working solution to my issue after thinking it through step by step. I found I was going about the whole issue entirely the wrong way!
Essentially, I found you don't need any local notifications when the app is active (i.e. in foreground). So instead of setting the local notification when the timer is started, I set the relevant notifications up when the app is about to resign.
I put these observers in my viewDidLoad;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(appWillResign) name:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(appIsActive) name:UIApplicationDidBecomeActiveNotification
object:nil];
and in my appWillResign method, I set up the notifications for any active timers. When the app is resumed, I simply cancel ALL the notifications.
[[UIApplication sharedApplication] cancelAllLocalNotifications];
In short, you shouldn't need to reference the notifications elsewhere in the code. You only set them up when they are absolutely needed: when the app is backgrounded! So you really don't need to 'store' them anywhere for later use.
Thanks for your contribution #Learner, you helped put me on the right track! :D
It is difficult to predict but generally SIGABRT comes when you access an nil or released object.
So in [[UIApplication sharedApplication] cancelLocalNotification:myNotification]; `
it can be "myNotification" which is nil.
OR if [myDefaults objectForKey:#"myNotification"] returns nil.
Possible resason for that can be that NsUserDefaults stores only few data type as mentioned in following paragraph of class reference.
A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
What i can predict is that myNotification which is being stored is of type UILocalNotification. so probably storing of data is failing.
Please debug the code further and post if above cases are not working.
The fix is to sync the NSUserDefaults. I'm assuming you just set the object and did not synchronize it. If so the object will not actually save to NSUserDefaults, rather just to the local NSUserDefaults object. What you need to do is call [defaults synchronize] after setting the object.