Tuesday, November 20, 2012

Objective-C Swizzling - Quick & Dirty Way of Adding Sounds to CCMenuItem

If you need to add sounds to your GUI or gameplay buttons and you're using CCMenuItem to do all the touch handling for you, than creating a class that contains or inherits from CCMenuItem that adds your own functionality is what you probably have in mind. In this post I present an alternative way of doing that, using a technique called Swizzling, which I explain.

Sometimes the quick and dirty approach, for you, is the right one. Maybe you're prototyping. Maybe you inherited a large project with CCMenuItems everywhere, including some instances serialized in CCB files which would make them even messier to replace, and you have a couple of hours to make small UX improvements such as adding a click sound to to get a more responsive UI.
Maybe in your eyes the quick and dirty approach isn't even that bad.

Each class has a "dispatch table" which associates selectors with their implementation code. Swizzling methods in Objective-C is a nick name given to the dark art of swapping between the implementations of two functions, or in Obj-C terms, exchanging between the implementations associated with two message selectors - during runtime.

Normally you have selectorA which invokes implementationA, and selectorB which invokes implementationB. Swizzling allows you to make selectorA invoke implementationB, and selectorB invoke implementationA.

The technique is often used to do things that Apple doesn't let us, intentionally or not. I've seen it used to give the built in map UI components a different look, and personally used it for stuff such as getting notified when a certain thing happens that you don't have a delegate or an NSNotification for. Swizzling should normally be your choice when there is no other way to do what you want.

When using Swizzling you should make sure that you test your solution with each update of the API whose methods are being Swizzled - for example cocos2d or the iOS SDK (You should also give it a very good test to begin with).

The reason for this, and why you'd normally want to avoid swizzling, is because it relies on private methods or classes - implementation details of the APIs you are using. You should be working with interfaces and not implementation details. API programmers rarely modify their interfaces. Changing the interface means forcing users to adapt the code that consumes the API. The concept of deprecating code (as opposed to just deleting code) exists to allow API programmers modify interfaces without breaking existing code that works with them. The same obligations just don't exist for implementation details, which can change as radically as the writers see fit.
Swizzling methods in cocos2d is safer than in UIKit because of two reasons.
First, no one is ever forcing you to update cocos2d (or most 3rd party libraries) - if an update is causing problems with your Swizzling solution, you can keep using the non problematic version until you adjust the new one. With UIKit your application is running against whatever version is installed on the user's device, so your application is better be ready by the time each iOS update is out.
The second reason is that cocos2d is open-source. When relying on implementation details, being able to see the source code can very helpful.

Adding Click Sounds to CCMenuItems

CCMenuItem has a private method named 'activate' which we are going to intercept. We'll then play our click sound every time it's being called.

  • You can apply this to an existing project, or if you just want to see this in action create a new project from the cocos2d template, and put a couple of CCMenuItems in a CCMenu on HelloWorldLayer.
  • Download HGSwizzler. It's a super simple Swizzling implementation taken from here wrapped in an Objective-C class method. Add the two code files and the included 'click.wav' into your project. The click sound is taken from here.
  • Create a new category on CCMenuItem, and add the following function to it:

  • Go to your UIApplicationDelegate class (typically AppDelegate.m) and import both HGSwizzler.h and the new CCMenuItem category you created
  • Add the following line to applicationDidFinishLaunching:
 
 
You've added a new method to CCMenuItem and exchanged the implementations of the 'activate' and 'activateSwizzle' methods. That means that when you do [myMenuItem activate], 'activate' will not be called and 'activateSwizzle' will be called instead. Calling 'activateSwizzle' will call the original implementation of 'activate', this is why the call to [self activateSwizzle] at the end of the function doesn't create an infinite loop resulting in a stack overflow. It is the equivalent of calling [super activate] if we'd subclass instead of swizzling.

That's it.  If you've found a problem in the code, typos the post, inaccuracies in the information, or just want a clarification feel absolutely free to leave a comment or mail me.

1 comment:

  1. Struggling to get your car paid off? Are you mad about the last deal you got?
    Perhaps you're looking for an automobile now, and you're not sure what to do differently.
    You're in the same boat with many other people. Keep reading to find out information regarding what to do next time you enter a dealership.

    Feel free to visit my site cars

    ReplyDelete