2013-04-15 State machine in Objective-C

Here's my version of a state machine in Objective-C, useful for your iOS or Mac OS X projects. There are many like it, but this one is mine. I've modeled an alarm clock here.

What I like about it: it's pretty small and light on the objects. The state transitions are the only thing that's done with an object, and even these could be replaced with a struct, but that causes problems with ARC (which doesn't like function pointers in structs).

The code below is WTFPL-licensed. Just so you know.

The header file:

    /* Enums that we need throughout this class to maintain state */
    enum WSAlarmState {WSAlarmStateInactive, 
        WSAlarmStateActive, WSAlarmStatePlaying};
    enum WSAlarmAction {WSAlarmActionStart, WSAlarmActionPlay,
        WSAlarmActionSnooze, WSAlarmActionStop};
    /* Describes how one state moves to the other */
    @interface WSAlarmStateTransition : NSObject
    @property enum WSAlarmState srcState;
    @property enum WSAlarmAction result;
    @property enum WSAlarmState dstState;
    @end
    /* Singleton that maintains state for an alarm  */
    @interface WSAlarm : WSNotification
    @property enum WSAlarmState currentState;
    @end

The header file contains the enums for the states and the actions. Note that these actions are both used as a return value and as an input value.

The implementation file starts with the init method, which sets up the state transition table. Basically, this table says: given a state and a resulting action, what is the next state?

Furthermore, it contains a function that does all transitions, a function that looks up the next state, and the state methods.

    #import "WSAlarm.h"
    @implementation WSAlarmStateTransition
    - (id)init:(enum WSAlarmState)srcState :(enum WSAlarmAction)action :(enum WSAlarmState)dstState
    {
        if (self = [super init]) {
            // Do initialization here
            DLog(@"init");
            self.srcState = srcState;
            self.result = action;
            self.dstState = dstState;
        }
        return self;
    }
    @end
    #pragma mark -
    #pragma mark WSAlarm class
    @implementation WSAlarm {
        NSArray *stateMethods;
        NSArray *stateTransitions;
    }
    - (id)init
    {
        if (self = [super init]) {
            // Do initialization here
            DLog(@"init");
            self.currentState = WSAlarmStateInactive;
            
            /* This array and enum WSAlarmStates must be in sync! */
            stateMethods = @[
              [NSValue valueWithPointer:@selector(noAlarmActiveState)],
              [NSValue valueWithPointer:@selector(alarmActiveState)],
              [NSValue valueWithPointer:@selector(playingAlarm)]
            ];
            stateTransitions = @[
               [[WSAlarmStateTransition alloc] init:WSAlarmStateInactive
                                                   :WSAlarmActionStart
                                                   :WSAlarmStateActive],
               [[WSAlarmStateTransition alloc] init:WSAlarmStateActive
                                                   :WSAlarmActionPlay
                                                   :WSAlarmStatePlaying],
               [[WSAlarmStateTransition alloc] init:WSAlarmStateActive
                                                   :WSAlarmActionStop
                                                   :WSAlarmStateInactive],
               [[WSAlarmStateTransition alloc] init:WSAlarmStateActive
                                                   :WSAlarmActionForegrounded
                                                   :WSAlarmStatePlayedInBackground],
               [[WSAlarmStateTransition alloc] init:WSAlarmStatePlaying
                                                   :WSAlarmActionStart
                                                   :WSAlarmStateActive],
               [[WSAlarmStateTransition alloc] init:WSAlarmStatePlaying
                                                   :WSAlarmActionStop
                                                   :WSAlarmStateInactive],
               [[WSAlarmStateTransition alloc] init:WSAlarmStatePlayedInBackground
                                                   :WSAlarmActionStart
                                                   :WSAlarmStateActive],
               [[WSAlarmStateTransition alloc] init:WSAlarmStatePlayedInBackground
                                                   :WSAlarmActionStop
                                                   :WSAlarmStateInactive],
            
        }
        return self;
    }
    - (void)registerDefaults
    {
        DLog(@"entry");
    }
    #pragma mark -
    #pragma mark Convenience methods
    - (void)start:(NSDate*)notifDate
    {
        self.notificationDate = notifDate;
        [self transitionToState:WSAlarmStateActive withAction:WSAlarmActionStart];
    }
    - (void)stop
    {
        [self transitionToState:WSAlarmStateInactive withAction:WSAlarmActionStop];
    }
    - (void)play
    {
        [self transitionToState:WSAlarmStatePlaying withAction:WSAlarmActionPlay];
    }
    - (void)snooze
    {
        [self transitionToState:WSAlarmStateActive withAction:WSAlarmActionSnooze];
    }
    #pragma mark -
    #pragma mark State machine
    // Walk through table of transitions, and return new state
    - (enum WSAlarmState)lookupTransitionForState:(enum WSAlarmState)state
        withResult:(enum WSAlarmAction)action
    {
        enum WSAlarmState newState = -1;
        
        for(WSAlarmStateTransition *t in stateTransitions) {
            if(t.srcState == state && t.result == action) {
                // We found the new state.
                newState = t.dstState;
                break;
            }
        }
        
        if(newState == -1) {
            NSString *msg = [NSString stringWithFormat:
                @"Can't transition from state %@ with return code %@",
                alarmStateString[state], alarmActionString[action]];
            @throw [NSException exceptionWithName:@"TransitionException"
                                           reason:msg
                                         userInfo:nil];
        }
        
        return newState;
    }
    - (void)transitionToState:(enum WSAlarmState)newState withAction:(enum WSAlarmAction)result
    {
        NSValue *stateMethodValue = (NSValue*) stateMethods[newState];
        SEL stateMethod = [stateMethodValue pointerValue];
        
        // We need these because otherwise we get a warning
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        NSNumber *param = [NSNumber numberWithInt:result];
        enum WSAlarmAction nextAction = [self performSelector:stateMethod withObject:param];
    #pragma clang diagnostic pop
        
        self.currentState = [self lookupTransitionForState:self.currentState withResult:nextAction];
    }
    #pragma mark -
    #pragma mark States
    - (enum WSAlarmAction)noAlarmActiveState:(enum WSAlarmAction)action
    {
        // Some code to stop the alarm
        return WSAlarmActionStop;
    }
    - (enum WSAlarmAction)alarmActiveState:(enum WSAlarmAction)action
    {
        if(action == WSAlarmActionSnooze) {
            // User tapped "snooze", stop the sound
        } else if(action == WSAlarmActionStart) {
            // No alarm active, user starts alarm
        } else {
            // We reached state alarm active with a weird action
        }
        return WSAlarmActionStart;
    }
    - (enum WSAlarmAction)playingAlarmState:(enum WSAlarmAction)result
    {
        // Some code to play a sound
        return WSAlarmActionPlay;
    }
    @end