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