I have a solution for a notification problem which works well, but I'm afraid might be a bad idea.
I have a notification that needs to be handled by each instance of a class and by the class itself. To handle this, I'm registering for a notification by both the class and instances of the class. Because it's the exact same notification, I've named the class and instance method the same. This follows the standard we've set for how notification handlers are named.
Is this a bad idea? Is there some hidden got'ca that I'm missing. Will I be confusing the heck out of future developers?
+ (void)initialize
{
if (self == [SICOHTTPClient class]) {
[[self notificationCenter] addObserver:self
selector:#selector(authorizationDidChangeNotification:)
name:SICOJSONRequestOperationAuthorizationDidChangeNotification
object:nil];
}
}
- (id)initWithBaseURL:(NSURL *)url
{
self = [super initWithBaseURL:url];
if (self) {
self.parameterEncoding = AFJSONParameterEncoding;
[self registerHTTPOperationClass:[SICOJSONRequestOperation class]];
[self setDefaultHeader:#"Accept" value:#"application/json"];
if ([[self class] defaultAuthorization])
[self setDefaultHeader:#"Authorization" value:[[self class] defaultAuthorization]];
[[[self class] notificationCenter] addObserver:self
selector:#selector(authorizationDidChangeNotification:)
name:SICOJSONRequestOperationAuthorizationDidChangeNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[[self class] notificationCenter] removeObserver:self
name:SICOJSONRequestOperationAuthorizationDidChangeNotification
object:nil];
}
#pragma mark Notifications
- (void)authorizationDidChangeNotification:(NSNotification *)notification
{
NSString *authorization = notification.userInfo[SICOJSONRequestOperationAuthorizationKey];
if ([authorization isKindOfClass:[NSString class]]) {
[self setDefaultHeader:#"Authorization" value:authorization];
} else {
[self clearAuthorizationHeader];
}
}
+ (void)authorizationDidChangeNotification:(NSNotification *)notification
{
NSString *authorization = notification.userInfo[SICOJSONRequestOperationAuthorizationKey];
if ([authorization isKindOfClass:[NSString class]]) {
[self setDefaultAuthorization:authorization];
} else {
[self setDefaultAuthorization:nil];
}
}
This is what code comments are for :)
There's no problem in Objective C with a class method and instance method having the same name.
I would suggest either:
amend your notification method name spec to handle this (and then handle the class notification with a different appropriately named method), or
add appropriate comment to explain what's happening for benefit of future potentially confused developers
The language itself and the runtime will see no ambiguity in what you're doing. So you're safe on that front.
In terms of confusing future maintainers I guess you needn't be too concerned with silly autocomplete mistakes because it's not a method you intend to make manual calls to.
That said, I'd be tempted to move the class stuff into an artificial category. That'll not only give separation on the page but make it explicit that the class intends to respond as a separate tranche of functionality from the instance responses.
Related
I'm writing a mac app that runs its own web server, using the GCDWebServer library (https://github.com/swisspol/GCDWebServer). My app delegate handles GET requests like so:
__weak typeof(self) weakSelf = self;
[webServer addDefaultHandlerForMethod:#"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return [weakSelf handleRequest:request];
}];
And then the handleRequest method returns the response data, something like:
return [GCDWebServerDataResponse responseWithHTML:#"<html><body><p>Hello World!</p></body></html>"];
So far so good. Except now I want the handleRequest method to use NSSpeechSynthesizer to create an audio file with some spoken text in it, and then wait for the speechSynthesizer:didFinishSpeaking method to be called before returning to the processBlock.
// NSSpeechSynthesizerDelegate method:
- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)success
{
NSLog(#"did finish speaking, success: %d", success);
// return to processBlock...
}
Problem is, I have no idea how to do this. Is there a way to return from the speechSynthesizer:didFinishSpeaking method into the processBlock defined above?
You need to run the speech synthesizer on a separate thread with its own run loop, and use a lock to allow your request thread to wait for the operation to complete on the speech thread.
Assuming the web server maintains its own thread(s) and runloop, you can use your app's main thread to run the speech synthesizer, and you can use NSCondition to signal completion to the web response thread.
A basic (untested) example (without error handling):
#interface SynchroSpeaker : NSObject<NSSpeechSynthesizerDelegate>
- (id)initWithText:(NSString*)text outputUrl:(NSURL*)url;
- (void)run;
#end
#implementation SynchroSpeaker
{
NSCondition* _lock;
NSString* _text;
NSURL* _url;
NSSpeechSynthesizer* _synth;
}
- (id)initWithText:(NSString*)text outputUrl:(NSURL*)url
{
if (self = [super init])
{
_text = text;
_url = url;
_lock = [NSCondition new];
}
return self;
}
- (void)run
{
NSAssert(![NSThread isMainThread], #"This method cannot execute on the main thread.");
[_lock lock];
[self performSelectorOnMainThread:#selector(startOnMainThread) withObject:nil waitUntilDone:NO];
[_lock wait];
[_lock unlock];
}
- (void)startOnMainThread
{
NSAssert([NSThread isMainThread], #"This method must execute on the main thread.");
[_lock lock];
//
// Set up your speech synethsizer and start speaking
//
}
- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)success
{
//
// Signal waiting thread that speaking has completed
//
[_lock signal];
[_lock unlock];
}
#end
It's used like so:
- (id)handleRequest:(id)request
{
SynchroSpeaker* speaker = [[SynchroSpeaker alloc] initWithText:#"Hello World" outputUrl:[NSURL fileURLWithPath:#"/tmp/foo.dat"]];
[speaker run];
////
return response;
}
GCDWebServer does run into its own threads (I guess 2 of them) - not in the main one. My solution needed to run code in Main Thread when calling the ProcessBlock.
I found this way that suits my needs:
First declare a weak storage for my AppDelegate: __weak AppDelegate *weakSelf = self;. Doing so I can access all my properties within the block.
Declare a strong reference to AppDelegate from within the block like so: __strong AppDelegate* strongSelf = weakSelf;
Use NSOperationQueue to align the operation on mainThread:
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
//Your code goes in here
NSLog(#"Main Thread Code");
[strongSelf myMethodOnMainThread];
}];
In this way myMethodOnMainThread surely will run where it's supposed to.
For sake of clarity I quote my relevant code section:
webServer = [[GCDWebServer alloc] init];
webServer.delegate = self;
__weak AppDelegate *weakSelf = self;
// Add a handler to respond to GET requests
[webServer addDefaultHandlerForMethod:#"GET"
requestClass:[GCDWebServerRequest class]
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
__strong AppDelegate* strongSelf = weakSelf;
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
//Your code goes in here
NSLog(#"Main Thread Code");
[strongSelf myMethodOnMainThread];
}];
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithJSONObject:packet];
completionBlock(response);
}];
GCWebServer supports fully asynchronous responses as of version 3.0 and later [1].
[webServer addDefaultHandlerForMethod:#"GET"
requestClass:[GCDWebServerRequest class]
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
// 1. Trigger speech synthesizer on main thread (or whatever thread it has to run on) and save "completionBlock"
// 2. Have the delegate from the speech synthesizer call "completionBlock" when done passing an appropriate response
}];
[1] https://github.com/swisspol/GCDWebServer#asynchronous-http-responses
I'm trying to write a plugin for phonegap/cordova that allows audio to resume when interrupted by a phone call. I'm using AVAudioSesionInterruptionNotification and it is working well. However, I need to be able to send a string to my event handler, but I can't figure out how.
I'm setting up an event listener here and calling it from the javascript layer:
- (void) startListeningForAudioSessionEvent:(CDVInvokedUrlCommand*)command{
NSString* myImportantString = [command.arguments objectAtIndex:0];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onAudioSessionEvent:) name:AVAudioSessionInterruptionNotification object:nil];
}
and I'm handling the event here:
- (void) onAudioSessionEvent:(NSNotification *)notification
{
if([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded]]){
//do stuff using myImportantString
}
}
can't figure out how to pass myImportantString over to onAudioSessionEvent. I know very little Objective-C (hence my use of cordova), so please respond as if you're talking to a child. Thanks!
By the way, I'm simply trying to add a couple of methods on top of cordova's media plugin found here: https://github.com/apache/cordova-plugin-media/tree/master/src/ios
so the code above is the entirety of my .m file minus this part
#implementation CDVSound (extendedCDVSound)
It's probably easier to use the block-based API for adding an observer:
- (void) startListeningForAudioSessionEvent:(CDVInvokedUrlCommand*)command{
NSString* myImportantString = [command.arguments objectAtIndex:0];
id observer = [[NSNotificationCenter defaultCenter] addObserverForName:AVAudioSessionInterruptionNotification
object:nil
queue:nil
usingBlock:^(NSNotification *notification){
if([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded]]){
//do stuff using myImportantString; you could even skip the use of that
//temporary variable and directly use [command.arguments objectAtIndex:0],
//assuming that command and command.arguments are immutable so that you
//can rely on them still being the same
}
}];
}
By using the block-based API, you can directly reference any variables that are in scope at the time the observer is added in the code which is invoked when the notification is posted.
When you're done observing, you need to remove observer as an observer of the notification.
Like this:
// near the top of MyController.m (above #implementation)
#interface MyController ()
#property NSString *myImportantString;
#end
// inside the implementation
- (void) startListeningForAudioSessionEvent:(CDVInvokedUrlCommand*)command{
self.myImportantString = [command.arguments objectAtIndex:0];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onAudioSessionEvent:) name:AVAudioSessionInterruptionNotification object:nil];
}
- (void) onAudioSessionEvent:(NSNotification *)notification
{
if([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded]]){
//do stuff using self.myImportantString
}
}
I'm having a random EXC_BAD_ACCESS KERN_INVALID_ADDRESS, but I can't point out the source. However, I'm wondering if this might be it:
I have an audio_queue created like this:
_audio_queue = dispatch_queue_create("AudioQueue", nil);
which I use to create and access an object called _audioPlayer:
dispatch_async(_audio_queue, ^{
_audioPlayer = [[AudioPlayer alloc] init];
});
The audio player is owned by a MovieView:
#implementation MovieView
{
AudioPlayer *_audioPlayer
}
Then, in the dealloc method of MovieView, I have:
- (void)dealloc
{
dispatch_async(_audio_queue, ^{
[_audioPlayer destroy];
});
}
Is this acceptable? I'm thinking that by the time the block is called, the MovieView would have already been deallocated, and when trying to access the _audioPlayer, it no longer exists. Is this the case?
My crash report only says:
MovieView.m line 0
__destroy_helper_block_
Your bug is in the ivar access. This is due to how ivars work in ObjC: the -dealloc above is equivalent to
- (void)dealloc
{
dispatch_async(self->_audio_queue, ^{
[self->_audioPlayer stopPlaying];
});
}
This can break because you end up using self after it is dealloced.
The fix is something like
- (void)dealloc
{
AVAudioPlayer * audioPlayer = _audioPlayer;
dispatch_async(audio_queue, ^{
[audioPlayer stopPlaying];
});
}
(It is frequently not thread-safe to explicitly or implicitly (via ivars) reference self in a block. Sadly, I don't think there is a warning for this.)
If this is the cause, the you could use dispatch_sync
- (void)dealloc
{
dispatch_sync(_audio_queue, ^{
[_audioPlayer stopPlaying];
});
}
I haven't tested this
I'm new to Obj-c. I've got a class which sets a var boolean to YES if it's successful (Game Center login = successful), what it would be great to do, is somehow have a listener to that var that listens to when it is YES and then executes some code. Do I use a block for that? I'm also using the Sparrow framework.
Here's my code in my GameCenter.m file
-(void) setup
{
gameCenterAuthenticationComplete = NO;
if (!isGameCenterAPIAvailable()) {
// Game Center is not available.
NSLog(#"Game Center is not available.");
} else {
NSLog(#"Game Center is available.");
__weak typeof(self) weakSelf = self; // removes retain cycle error
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer]; // localPlayer is the public GKLocalPlayer
__weak GKLocalPlayer *weakPlayer = localPlayer; // removes retain cycle error
weakPlayer.authenticateHandler = ^(UIViewController *viewController, NSError *error)
{
if (viewController != nil)
{
[weakSelf showAuthenticationDialogWhenReasonable:viewController];
}
else if (weakPlayer.isAuthenticated)
{
[weakSelf authenticatedPlayer:weakPlayer];
}
else
{
[weakSelf disableGameCenter];
}
};
}
}
-(void)showAuthenticationDialogWhenReasonable:(UIViewController *)controller
{
[[[[[UIApplication sharedApplication] delegate] window] rootViewController] presentViewController:controller animated:YES completion:nil];
}
-(void)authenticatedPlayer:(GKLocalPlayer *)player
{
NSLog(#"%#,%#,%#",player.playerID,player.displayName, player.alias);
gameCenterAuthenticationComplete = YES;
}
-(void)disableGameCenter
{
}
But I need to know from a different object if that gameCenterAuthenticationComplete equals YES.
You can use a delegate pattern. It's far easier to use than KVO or local notifications and it's used a lot in Obj-C.
Notifications should be used only in specific situations (e.g. when you don't know who wants to listen or when there are more than 1 listeners).
A block would work here but the delegate does exactly the same.
You could use KVO (Key-Value Observing) to watch a property of your object, but I'd rather post a NSNotification in your case.
You'll need to have the objects interested in knowing when Game Center login happened register themselves to NSNotificationCenter, then post the NSNotification in your Game Center handler. Read the Notification Programming Topics for more details !
If there is a single method to execute on a single delegate object, you can simply call it in the setter. Let me give a name to this property:
#property(nonatomic,assign, getter=isLogged) BOOL logged;
It's enough that you implement the setter:
- (void) setLogged: (BOOL) logged
{
_logged=logged;
if(logged)
[_delegate someMethod];
}
Another (suggested) way is to use NSNotificationCenter. With NSNotificationCenter you can notify multiple objects. All objects that want to execute a method when the property is changes to YES have to register:
NSNotificationCenter* center=[NSNotificationCenter defaultCenter];
[center addObserver: self selector: #selector(handleEvent:) name: #"Logged" object: nil];
The handleEvent: selector will be executed every time that logged changes to YES. So post a notification whenever the property changes:
- (void) setLogged: (BOOL) logged
{
_logged=logged;
if(logged)
{
NSNotificationCenter* center=[NSNotificationCenter defaultCenter];
[center postNotificationName: #"Logged" object: self];
}
}
I'm trying to create a Core Data, document based app but with the limitation that only one document can be viewed at a time (it's an audio app and wouldn't make sense for a lot of docs to be making noise at once).
My plan was to subclass NSDocumentController in a way that doesn't require linking it up to any of the menu's actions. This has been going reasonably but I've run into a problem that's making me question my approach a little.
The below code works for the most part except if a user does the following:
- Tries to open a doc with an existing 'dirty' doc open
- Clicks cancel on the save/dont save/cancel alert (this works ok)
- Then tries to open a doc again. For some reason now the openDocumentWithContentsOfURL method never gets called again, even though the open dialog appears.
Can anyone help me work out why? Or perhaps point me to an example of how to do this right? It feels like something that must have been implemented by a few people but I've not been able to find a 10.7+ example.
- (BOOL)presentError:(NSError *)error
{
if([error.domain isEqualToString:DOCS_ERROR_DOMAIN] && error.code == MULTIPLE_DOCS_ERROR_CODE)
return NO;
else
return [super presentError:error];
}
- (id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError
{
if(self.currentDocument) {
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openUntitledDocumentAndDisplayIfClosedAll: didCloseAll: contextInfo:)
contextInfo:nil];
NSMutableDictionary* details = [NSMutableDictionary dictionary];
[details setValue:#"Suppressed multiple documents" forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:DOCS_ERROR_DOMAIN code:MULTIPLE_DOCS_ERROR_CODE userInfo:details];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
- (void)openUntitledDocumentAndDisplayIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
if(self.currentDocument == nil)
[super openUntitledDocumentAndDisplay:YES error:nil];
}
- (void)openDocumentWithContentsOfURL:(NSURL *)url
display:(BOOL)displayDocument
completionHandler:(void (^)(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error))completionHandler NS_AVAILABLE_MAC(10_7)
{
NSLog(#"%s", __func__);
if(self.currentDocument) {
NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:[url copy], #"url",
[completionHandler copy], #"completionHandler",
nil];
[self closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocumentWithContentsOfURLIfClosedAll:didCloseAll:contextInfo:)
contextInfo:(__bridge_retained void *)(info)];
} else {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
}
}
- (void)openDocumentWithContentsOfURLIfClosedAll:(NSDocumentController *)docController
didCloseAll: (BOOL)didCloseAll
contextInfo:(void *)contextInfo
{
NSDictionary *info = (__bridge NSDictionary *)contextInfo;
if(self.currentDocument == nil)
[super openDocumentWithContentsOfURL:[info objectForKey:#"url"] display:YES completionHandler:[info objectForKey:#"completionHandler"]];
}
There's a very informative exchange on Apple's cocoa-dev mailing list that describes what you have to do in order to subclass NSDocumentController for your purposes. The result is that an existing document is closed when a new one is opened.
Something else you might consider is to mute or stop playing a document when its window resigns main (i.e., sends NSWindowDidResignMainNotification to the window's delegate), if only to avoid forcing what might seem to be an artificial restriction on the user.
I know it's been a while, but in case it helps others....
I had what I think is a similar problem, and the solution was to call the completion handler when my custom DocumentController did not open the document, e.g.:
- (void)openDocumentWithContentsOfURL:(NSURL *)url display:(BOOL)displayDocument completionHandler:(void (^)(NSDocument * _Nullable, BOOL, NSError * _Nullable))completionHandler {
if (doOpenDocument) {
[super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler];
} else {
completionHandler(NULL, NO, NULL);
}
}
When I added the completionHandler(NULL, NO, NULL); it started working for more than a single shot.