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