CCControl Class Hierarchy

Create a control object with Cocos2d for iPhone

Note: this post is outdated, you should take a look to this more recent article.

I work with Cocos2d for almost one year now and it’s really a great framework to work with. I like it because it includes a huge of utilities and components number ready to use, and it’s community is very active and nice.
However when you want create new custom controls (for example a drag&drop control, a slider, or whatever you want) you need each time to reinvent the wheel. There is no class that can help us ignore certain logics that are across all control objects. That’s why to make my life easier I wrote a class which allows us to automate some tasks such as the registration of target-action pairs, sending messages or even manage the control state. I named this class “CCControl” because its API is inspired from the UIControl class from the UIKit framework (CocoaTouch).

This article aims to explain briefly why and how I did this before to expose you what we’ll do with this code later.

Class hierarchy

CCControl Class Hierarchy

First of all I’m going to start by describe a bit the class hierarchy.

NSObject:

      It’s the root class of most Objective-C class hierarchies.

CCNode:

      It’s the main class used by Cocos2D to render objects using OpenGL ES. It also contains some logic like the add child, schedulers or actions methods.

CCLayer:

      It adds some functionalities such as the touch events and accelerometer management. It also manages the opacity and the RGB colors.

CCControl:

      It manages the control objects such as buttons. Its main role is to define an interface for preparing and dispatching action messages to their respective targets when certain events occur.

CCButton, CCSlider, CCSwitch:

      They are custom controls which not exist for the moment (except for CCSlider) but they extend the CCControl class to manage their state and the event dispatching.

As you can see the class hierarchy is cut so that each layer has its own logic. At the bottom they are the controllers that use the properties of CCControl to manage their internal state and sending messages.

CCControl

Let’s go into the implementation now.

Interface

To start we are going to dissect a bit the header (Most comments have been removed here to make the code more compact and readable).

/** Kinds of possible events for the control objects. */
enum 
{
    CCControlEventTouchDown           = 1 << 0,
    CCControlEventTouchDragInside     = 1 << 1,
    CCControlEventTouchDragOutside    = 1 << 2, 
    CCControlEventTouchDragEnter      = 1 << 3, 
    CCControlEventTouchDragExit       = 1 << 4,
    CCControlEventTouchUpInside       = 1 << 5,
    CCControlEventTouchUpOutside      = 1 << 6,
    CCControlEventTouchCancel         = 1 << 7,
    CCControlEventValueChanged        = 1 << 8 
};
typedef NSUInteger CCControlEvent;

/** The possible state for a control.  */
enum 
{
    CCControlStateNormal       = 1 << 0,
    CCControlStateHighlighted  = 1 << 1,
    CCControlStateDisabled     = 1 << 2,
    CCControlStateSelected     = 1 << 3 
};
typedef NSUInteger CCControlState;

This defines the control event types which are use to preparing and sending action messages. It also specify the control state constants.

@interface CCControl : CCLayer
{
@public
    CCControlState state_;

    BOOL enabled_;
    BOOL selected_;
    BOOL highlighted_;

@private
    NSMutableDictionary *dispatchTable_;
}
/** The current control state constant. */
@property (assign, readonly) CCControlState state;
/** Tells whether the control is enabled. */
@property (nonatomic, getter = isEnabled) BOOL enabled;
/** A Boolean value that determines the control’s selected state. */
@property(nonatomic, getter = isSelected) BOOL selected;
/** A Boolean value that determines whether the control is highlighted. */
@property(nonatomic, getter = isHighlighted) BOOL highlighted;

CCControl is based on a CCLayer because it’s the base view when you code with Cocos2d. It contains all informations we need to start a new control element.
It defines some variables (state, enabled, selected, highlighted) to be able to manage the control state. It’ll allow us to determine if the control is normal or highlighted for example.
At last, one of the most important is the dispatchTable. It keeps a relation between the CCControlEvents and the given target-action pairs. I’ll explain it later.

/** Sends action messages for the given control events.
 */
- (void)sendActionsForControlEvents:(CCControlEvent)controlEvents;

/** Adds a target and action for a particular event (or events) to an internal
 * dispatch table.
 * The action message may optionnaly include the sender and the event as 
 * parameters, in that order.
 * When you call this method, target is not retained.
 */
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(CCControlEvent)controlEvents;

/** Removes a target and action for a particular event (or events) from an 
 * internal dispatch table.
 */
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(CCControlEvent)controlEvents;

#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
/** Returns a boolean value that indicates whether a touch is inside the bounds
 * of the receiver. The given touch must be relative to the world.
 */
- (BOOL)isTouchInside:(UITouch *)touch;
#elif __MAC_OS_X_VERSION_MAX_ALLOWED
/** Returns a boolean value that indicates whether a mouse is inside the bounds
 * of the receiver. The given mouse event must be relative to the world.
 */
- (BOOL)isMouseInside:(NSEvent *)event;
#endif

Here are defined the main methods. They allow us to register target-action pairs with a vector of control events, remove them and send messages for particular events. There is also a convenient method to know whether the click/touch inside the component. The macro is used to make the difference between iOS and Mac OSX target to the compilation.
For example if you want stay tune when a button is pushed down you can do this on the receiver like that:

// Create a control button and listen to CCControlEventTouchDown
MyControlButton *button = [MyControlButton newControlButton];
[button addTarget:self action:@selector(mySelector) forControlEvents:CCControlEventTouchDown];

Note: I’m not convinced by the utility to register several delegates for the same event, but I decided to respect the UIControl API for more compatibility.

Base implementation

Take a look into the implementation file now. I’m going to show and explain you the most important parts of the code.

// 1
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(CCControlEvent)controlEvents
{
    // For each control events
    for (int i = 0; i < kControlEventTotalNumber; i++)
    {
        // If the given controlEvents bitmask contains the event
        if ((controlEvents & (1 << i)))
        {
            [self addTarget:target action:action forControlEvent:(1 << i)];
        }
    }
}

// 2
- (void)addTarget:(id)target action:(SEL)action forControlEvent:(CCControlEvent)controlEvent
{
    // Create the invocation object
    NSInvocation *invocation = [self invocationWithTarget:target action:action forControlEvent:controlEvent];

    // Add the invocation into the dispatch list for the given control event
    NSMutableArray *eventInvocationList = [self dispatchListforControlEvent:controlEvent];
    [eventInvocationList addObject:invocation];
}

// 3
- (NSInvocation *)invocationWithTarget:(id)target action:(SEL)action forControlEvent:(CCControlEvent)controlEvent
{
    NSAssert(target, @"The target cannot be nil");
    NSAssert(action != NULL, @"The action cannot be NULL");

    // Create the method signature for the invocation
    NSMethodSignature *sig = [target methodSignatureForSelector:action];
    NSAssert(sig, @"The given target does not implement the given action");

    // Create the invocation object
    // First and second corresponds respectively to target and action
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    [invocation setTarget:target];
    [invocation setSelector:action];

    // If the selector accept the sender as third argument
    if ([sig numberOfArguments] >= 3)
    {
        [invocation setArgument:&self atIndex:2];
    }

    // If the selector accept the CCControlEvent as fourth argument
    if ([sig numberOfArguments] >= 4)
    {
        [invocation setArgument:&controlEvent atIndex:3];
    }

    return invocation;
}

Let’s go over this section by section.

    1 – Decomposes the control events bitmask to add the given target-action pair into the dispatch table event by event.
    2 – Gets an NSInvocation object which contains the target-action pair and adds it into the corresponding dispatch list.
    3 – Creates an NSInvocation object with a given target-action pair and control event. It verifies that the given informations are valid before to create the NSInvocation object.

As you can see the given selector can have 0, 1 or 2 arguments. It means that the receiver’s selector may look like these: “aSelector”, “aSelector:(id)sender” or “aSelector:(id)sender controlEvent:(CCControlEvent)controlEvent” according to your needs.

// 4
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(CCControlEvent)controlEvents
{
    // For each control events
    for (int i = 0; i < kControlEventTotalNumber; i++)
    {
        // If the given controlEvents bitmask contains the event
        if ((controlEvents & (1 << i)))
        {
            [self removeTarget:target action:action forControlEvent:(1 << i)];
        }
    }
}

// 5
- (void)removeTarget:(id)target action:(SEL)action forControlEvent:(CCControlEvent)controlEvent
{
    // Retrieve all invocations for the given control event
    NSMutableArray *eventInvocationList = [self dispatchListforControlEvent:controlEvent];

#if NS_BLOCKS_AVAILABLE
    NSPredicate *predicate = 
    [NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings)
     {
         NSInvocation *evaluatedObject = object;

         if ((target == nil && action == NULL)
             || (target == nil && [evaluatedObject selector] == action)
             || (action == NULL && [evaluatedObject target] == target)
             || ([evaluatedObject target] == target && [evaluatedObject selector] == action))
         {
             return YES;
         } 

         return NO;
     }];

    // Define the invocation to remove for the given control event
    NSArray *removeObjectList = [eventInvocationList filteredArrayUsingPredicate:predicate];
#else
    NSMutableArray *removeObjectList = [NSMutableArray array];
    if (target == nil && action == NULL)
    {
        removeObjectList = eventInvocationList;
    } else
    {
        if (target)
        {
            NSPredicate *predicate = [NSPredicate predicateWithFormat:@"target == %@",target];
            removeObjectList = [NSMutableArray arrayWithArray:[eventInvocationList filteredArrayUsingPredicate:predicate]];
        } else
        {
            removeObjectList = [NSMutableArray arrayWithArray:eventInvocationList];
        }

        if (action != NULL)
        {
            NSMutableArray *tempObjectToKeep = [NSMutableArray array];
            for (NSInvocation *invocation in removeObjectList)
            {
                if ([invocation selector] != action)
                {
                    [tempObjectToKeep addObject:invocation];
                }
            }
            [removeObjectList removeObjectsInArray:tempObjectToKeep];
        }
    }
#endif

    // Remove the corresponding invocation objects
    [eventInvocationList removeObjectsInArray:removeObjectList];
}

Continue section by section:

    4 – Decomposes the control events bitmask to remove the given target-action pair into the dispatch table event by event.
    5 – Removes the given target-action pair for the given event. It searches the corresponding target and action for the given control and removes them from the dispatch list. If the target is nil or the action is NULL it considers that it has to delete all invocation object for the given control event.

Attention: There is a macro which verifies whether the blocks are available to the target compilation. I’ve update the code to avoid any trouble with the LLVM GCC 4.2 compiler, however, you should use the LLVM compiler 2.0 instead.

// 6
- (void)sendActionsForControlEvents:(CCControlEvent)controlEvents
{
    // For each control events
    for (int i = 0; i < kControlEventTotalNumber; i++)
    {
        // If the given controlEvents bitmask contains the current event
        if ((controlEvents & (1 << i)))
        {
            NSMutableArray *invocationList = [self dispatchListforControlEvent:(1 << i)];

            for (NSInvocation *invocation in invocationList)
            {
                [invocation invoke];
            }
        }
    }
}

To finish with the implementation file:

    6 -This is code to send messages for particular events. It cut the given bitmask and for each NSInvocation available for the current event and it invokes the method on the receiver.

What’s next

Now you know a little better what CCControl does and how it works. We can certainly improve this class by adding the management of blocks in addition to the target-action pattern.
The next times, I will show you how to implement your one control with the example of a slider control or a button.
You can find the files to my github.

1 Star2 Stars3 Stars4 Stars5 Stars (6 votes, average: 3.50 out of 5)
Loading...

3 comments

Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.