Skip to content
Thread-safe state machine library for Objective-C backed by Grand Central Dispatch.
Objective-C Ruby C
Pull request Compare This branch is 26 commits ahead, 4 commits behind luisobo:master.
Latest commit ae7e64d May 30, 2013 @brynbellomy using new gcdthreadsafe idioms required by iOS 6 and the deprecation …
…of dispatch_get_current_queue()

README.md

// StateMachine-GCDThreadsafe

Grand Central Dispatch-backed threadsafe state machine library for iOS.

This library was inspired by the Ruby gem state_machine and the letter 5.

features

  • Threadsafe and FAST. All transition code executes within barrier blocks on a critical-section-only async dispatch queue.
  • Open architecture. You can submit your own blocks to this queue as well. Everything will be threadsafed for you under the hood.
  • Easy, block-based DSL for defining your classes' state machines. Block-based before and after transition hooks.
  • Less boilerplate to write. Dynamically generates all state machine methods directly onto your classes using some Objective-C runtime voodoo jah.

installation

  1. CocoaPods is the way and the light. Just add to your Podfile:
pod 'StateMachine-GCDThreadsafe', '>= 2.0.0'
  1. The direct approach. You should be able to add StateMachine to your source tree. Create an Xcode workspace containing your project and then import the StateMachine-GCDThreadsafe project into it.
  2. The indirect approach. If you are using git, consider using a git submodule.

Seriously though, get with CocoaPods already, y'know?

basic usage for humans with an concrete objective

Let's model a Subscription class.

1. @interface

Declare your class to conform to the SEThreadsafeStateMachine protocol (which is defined in LSStative.h, if you're curious).

@interface Subscription : NSObject <SEThreadsafeStateMachine>
@property (nonatomic, strong, readwrite) NSDate *terminatedAt;
- (void) stopBilling;
@end

2. the @gcd_threadsafe macro.

@gcd_threadsafe is defined in <BrynKit/GCDThreadsafe.h>. Import that. The macro itself should be placed within your @implementation block. Preferably at the very top, so it's more self-documenting.

This macro defines a couple of methods on your class for dispatching critical section code onto self.queueCritical -- the show-stopper, the main attraction -- the private dispatch_queue_t on which shit be GITTAN RIAL.

#import <BrynKit/GCDThreadsafe.h>

@implementation Subscription
@gcd_threadsafe

// methods, etc. ...

3. the state machine dsl

In the implementation of the class, we use the StateMachine DSL to define our valid states and events.

  1. "The DSL is a work in progress and will change." - @luisobo
  2. "I'onno mate I think i'ss quite nice, really" - @brynbellomy
  3. Conclusion: *shrug*
STATE_MACHINE(^(LSStateMachine *sm) {
    sm.initialState = @"pending";

    [sm addState:@"pending"];
    [sm addState:@"active"];
    [sm addState:@"waitingOnSomething"];
    [sm addState:@"suspended"];
    [sm addState:@"terminated"];

    [sm when:@"startWaiting" transitionFrom:@"active" to:@"waitingOnSomething"];
    [sm when:@"activate"  transitionFrom:@"pending" to:@"active"];
    [sm when:@"suspend"   transitionFrom:@"active" to:@"suspended"];
    [sm when:@"unsuspend" transitionFrom:@"suspended" to:@"active"];
    [sm when:@"terminate" transitionFrom:@"active" to:@"terminated"];
    [sm when:@"terminate" transitionFrom:@"suspended" to:@"terminated"];

    [sm before:@"terminate" do:^(Subscription *self){
        self.terminatedAt = [NSDate dateWithTimeIntervalSince1970:123123123];
    }];

    [sm after:@"suspend" do:^(Subscription *self) {
        [self stopBilling];
    }];
});

4. designated initializer

  1. Use the @gcd_threadsafe_init(...) macro in your designated initializer (-init, etc.). Place it before anything else in the if (self) block. It takes two parameters, both of which concern the critical section dispatch queue:
    • its concurrency mode: SERIAL or CONCURRENT
    • and its label: a regular C string
  2. Call [self initializeStateMachine] right after that.
  3. C'mon just do it
- (id) init
{
    self = [super init];
    if (self) {
        @gcd_threadsafe_init(CONCURRENT, "com.pton.KnowsHowToParty.queueCritical");
        [self initializeStateMachine];
    }
    return self;
}

@gcd_threadsafe_init(...) initializes the self.queueCritical property.

the metamorphosis

Once given a particular configuration of transitions and states, StateMachine-GCDThreadsafe will dynamically add the appropriate methods to your class to reflect that configuration. You'll find yourself facing a few compiler warnings regarding these methods. Wanna shut the compiler up? Easy enough: define a class category and don't implement it. The category can live hidden inside your implementation file (if the methods need to be private), in your header file (if the methods ought to be publicly callable), or split between the two.

@interface Subscription (StateMachine)
- (void)initializeStateMachine;
- (BOOL)activate;
- (BOOL)suspend;
- (BOOL)unsuspend;
- (BOOL)terminate;

- (BOOL)isPending;
- (BOOL)isActive;
- (BOOL)isSuspended;
- (BOOL)isTerminated;

- (BOOL)canActivate;
- (BOOL)canSuspend;
- (BOOL)canUnsuspend;
- (BOOL)canTerminate;
@end

In addition to your class's main state property being KVO-observable, StateMachine will define query methods to check if the object is in a given state (isPending, isActive, etc.) and to check whether an event will trigger a valid transition (canActivate, canSuspend, etc).

triggering events

Subscription *subscription = [[Subscription alloc] init];
subscription.state;                 // will be set to @"pending", the value of the initialState property

Start triggering events...

[subscription activate];            // retuns YES because it's a valid transition
subscription.state;                 // @"active"

[subscription suspend];             // also, `-stopBilling` is called by `-suspend`
                                    // retuns YES because it's a valid transition

subscription.state;                 // @"suspended"

[subscription terminate];           // retuns YES because it's a valid transition
subscription.state;                 // @"terminated"

subscription.terminatedAt;           // [NSDate dateWithTimeIntervalSince1970:123123123];

But! If we trigger an invalid event...

// the subscription is now suspended

[subscription activate];            // returns NO because it's not a valid transition
subscription.state;                 // @"suspended"

is it re-entrant?

duh, son, c'mon now.

tips n' tricks

  1. It's almost always a BAD, BAD idea to dispatch_sync(...) a synchronous block to the main queue from inside one of your transition or critical section blocks. Why? If the main thread happens to be waiting on your critical section code before moving forward, you'll deadlock. You should generally be sending things to the main queue that don't need to be synchronous (UI updates, certain kinds of NSNotifications, KVO messages, etc.). If it seems impossible to rewrite your main thread code in an asynchronous way, you may have an architectural problem.
  2. If you're implementing a GCD-threadsafe StateMachine on one of your UIViewControllers, keep in mind all of UIViewController's -viewDidX... and -viewWillY... methods are called from the main thread. Given tip #1 just above, this means that you have to be especially careful of deadlocks in UIViewController state machines (and in GCDThreadsafe code inside these UIViewController methods more generally).

contributing

  1. Brush up on your ReactiveCocoa. That's the direction that this fork of the code is overwhelmingly likely to head.
  2. Fork this project.
  3. Create a new feature branch.
  4. Commit your changes.
  5. Push to the branch.
  6. Create new pull request.

top scores

Something went wrong with that request. Please try again.