In a recent iOS project I needed to play some background music and have it fade smoothly in and out when it starts and stops.
The "play some music" part is easy. iOS give us
AVAudioPlayer in the AVFoundation framework. Super sweet and easy to use. However the "fade in and out" bit doesn't come for free.
The canonical solution suggested on the web is to hand-craft some nasty looping logic to get the job done. This is 2011 and we can do better than that. And, indeed,
here it is, in all it's Objective C glory.
1. Add a category
First, we open up our own category on the AVAudioPlayer class and define the methods we would ideally like the class to provide:
@interface AVAudioPlayer (PGFade)
- (void) stopWithFadeDuration:(NSTimeInterval)duration;
- (void) playWithFadeDuration:(NSTimeInterval)duration;
@end
That's the joys of categories - you can extend existent classes in your own application easily to make it look like methods were part of the original class interface. The common convention is to save this in a file called "AVAudioPlayer+PGFade.h".
Now client code can create a bog-standard AVAudioPlayer object, and call my new methods as if they were part of the base interface:
AVAudioPlayer *player = [AVAudioPlayer alloc] initWith...]; // however you want it set up
[player playWithFadeDuration:2.0];
2. Associative References
Now, our implementation of this is going to require some instance variables (ivars) to work with. The problem with categories is that they only allow you to add methods - you can't extend the set of instance variables defined in the class' @interface.
Or can you?
Associative references to the rescue! This is a handy Objective C runtime facility that allows you to associate another object with an existing object, with a lookup system very much like a dictionary - referenced by a void* key value.
The associated object is lifetime-managed with the original object, so when you release the parent, all associated objects are also released.
Using this facility we can "graft on" some of our own private ivars to the original class. Dirty, but effective.
One of the variables I need is a boolean variable tracking whether a fade is currently in progress. I actually expose this as a property called fading in the category interface. The implementation looks like this:
@implementation AVAudioPlayer (PGFade)
static char fadingKey;
- (BOOL) fading
{
NSNumber *number = (NSNumber *)objc_getAssociatedObject(self, &fadingKey);
return number && number.boolValue;
}
- (void) setFading:(BOOL)fading
{
objc_setAssociatedObject(self, &fadingKey, [NSNumber numberWithBool:fading], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
That's relatively simple. We just use two objective C runtime functions, objc_getAssociatedObject and objc_setAssociatedObject. For the association we use the address of a static variable - this will be an unambiguous value in the whole program. The object we store is an NSNumber object initialised with the value of our boolean.
It's a simple and effective trick.
3. Blocks
The final bit is for bonus points. You can see the implementation of my fade routine in the example project code itself. It's pretty simple - I just initialise the fade state in a few more "associative reference" variables and schedule a method call with
performSelector:withObject:afterDelay. Each time this is called, I adjust the AVAudioPlayer's volume, and schedule a new call if one is needed.
However, to truly perform a stop with fade, we need to ramp the audio volume down, and when it gets to zero stop the player. This could be achieved with some more state variables and clumsy logic, but iOS now provides us with blocks which are perfect for this kind of activity.
For sanity, I typedef a block (closure) type. The syntax practically identical to typedefing a pointer-to-function in C, but with ^ instead of *.
typedef void (^AVAudioPlayerFadeCompleteBlock)();
I make one more associated variable of this type, the block to call on completion:
- (AVAudioPlayerFadeCompleteBlock) fadeCompletion
{
return (AVAudioPlayerFadeCompleteBlock)objc_getAssociatedObject(self, &fadeCompletionKey);
}
- (void) setFadeCompletion:(AVAudioPlayerFadeCompleteBlock)completion
{
objc_setAssociatedObject(self, &fadeCompletionKey, completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
Note that blocks can be traded like objective C objects. You can retain, copy, and release them. In this case our code needs to take a copy (using the OBJC_ASSOCIATION_COPY_NONATOMIC flag). This ensures that any set block exists on the heap (persistent) rather than the default on-the-stack location.
Now, the internal "audio fade" routine need only check whether there is a completion block registered when the fade is complete, and if so call it:
if (fadeIsComplete)
{
self.fading = NO;
AVAudioPlayerFadeCompleteBlock completion = self.fadeCompletion;
if (completion) completion();
self.fadeCompletion = nil;
}
Invoking a block looks just like calling a function. Once called, we clear out the stored block in case it is holding other resources that should be released.
This provides an elegant way to perform any arbitrary action after the fade is complete.
That's all, folks!
The trio of Objective C facilities used here work together well to provide a simple and elegant solution to a problem that is often achieved with complex "strings-and-glue" logic. Check out the entire project on
Gitorious here.