Grand Central Dispatch-backed threadsafe state machine library for iOS.
This library was inspired by the Ruby gem state_machine and the letter 5.
- 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
andafter
transition hooks. - Less boilerplate to write. Dynamically generates all state machine methods directly onto your classes using some Objective-C runtime voodoo jah.
- CocoaPods is the way and the light. Just add to your
Podfile
:
pod 'StateMachine-GCDThreadsafe', '>= 2.0.0'
- 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. - The indirect approach. If you are using git, consider using a
git submodule
.
Seriously though, get with CocoaPods already, y'know?
Let's model a Subscription
class.
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
@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. ...
In the implementation of the class, we use the StateMachine
DSL to define our valid states and events.
- "The DSL is a work in progress and will change." - @luisobo
- "I'onno mate I think i'ss quite nice, really" - @brynbellomy
- 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];
}];
});
- Use the
@gcd_threadsafe_init(...)
macro in your designated initializer (-init
, etc.). Place it before anything else in theif (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
- Call
[self initializeStateMachine]
right after that. - 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.
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).
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"
duh, son, c'mon now.
- 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. - If you're implementing a GCD-threadsafe
StateMachine
on one of yourUIViewController
s, keep in mind all ofUIViewController
'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 inUIViewController
state machines (and inGCDThreadsafe
code inside theseUIViewController
methods more generally).
- Brush up on your ReactiveCocoa. That's the direction that this fork of the code is overwhelmingly likely to head.
- Fork this project.
- Create a new feature branch.
- Commit your changes.
- Push to the branch.
- Create new pull request.
- Luis Solano Bonet < [email protected] >, the original fork's author
- bryn austin bellomy < [email protected] >, a rookie, a failure