Awesome
motion-state-machine
Hey, this is motion-state-machine
— a state machine gem designed for
RubyMotion for iOS.
It features:
- A simple, nice-looking definition syntax
- Reaction to sent events, defined timeouts and global NSNotifications
- Transition guards and actions
- State entry / exit actions
- Internal transitions that don't leave the machine's current state
- Optional verbose log output for easy debugging
- Grand Central Dispatch-awareness — uses GCD queues for synchronization
Defining a state machine looks like this:
fsm = StateMachine::Base.new start_state: :awake
fsm.when :awake do |state|
state.on_entry { puts "I'm awake, started and alive!" }
state.transition_to :sleeping, on: :finished_hard_work,
state.die on: :too_hard_work
end
See below for more examples and usage instructions or consult the RubyDoc page on RubyGems.
Motivation
Undefined states and visual glitches in applications with complex UIs can be a hassle, especially when the UI is animated and the app has to handle asynchronous data retrieved in the background.
Well-defined UI state machines avoid these problems while ensuring that asynchronous event handling does not lead to undefined results (a.k.a. bugs).
MacRuby and Cocoa don't provide a simple library to address this — motion-state-machine should fill the gap for RubyMotion developers.
Installation
-
If not done yet, add
bundler
gem management to your RubyMotion app. See http://thunderboltlabs.com/posts/using-bundler-with-rubymotion for an explanation how. -
Add this line to your application's Gemfile:
gem 'motion-state-machine'
-
Execute:
$ bundle
Usage
The following example shows how to initialize and define a state machine:
fsm = StateMachine::Base.new start_state: :working, verbose: true
This initializes a state machine. Calling fsm.start!
would start the
machine in the defined start state :working
. Using :verbose
activates
debug output on the console.
Defining states and transitions
After initialization, you can define states and transitions:
fsm.when :working do |state|
state.on_entry { puts "I'm awake, started and alive!" }
state.on_exit { puts "Phew. That was enough work." }
state.transition_to :sleeping,
on: :finished_hard_work,
if: proc { really_worked_enough_for_now? },
action: proc { puts "Will go to sleep now." }
state.die on: :too_hard_work
end
This defines…
-
An entry and an exit action block, called when entering/exiting the state :working.
-
a transition from state
:working
to:sleeping
, happening when callingfsm.event(:finished_hard_work)
.Before the transition is executed, the state machine asks the
:if
guard block if the transition is allowed. Returningfalse
in this block would prevent the transition from happening.If the transition is executed, the machine calls the given
:action
block. -
another transition that terminates the state machine when calling
fsm.event(:too_hard_work)
. When terminated, the state machine stops responding to events.
Note that a transition from a state to itself can be internal: Entry/exit actions are not called on execution in this case.
Handling events, timeouts and NSNotifications
Transitions can be triggered…
-
by calling the state machine's
#event
method (see above). -
automatically after a given timeout:
fsm.when :sleeping do |state| state.transition_to :working, after: 20 end
(goes back to
:working
after 20 seconds in state:sleeping
) -
when a
NSNotification
is posted:fsm.when :awake do |state| state.transition_to :in_background, on_notification: UIApplicationDidEnterBackgroundNotification end
How fast is it?
The implementation is designed for general non-performance-intensive purposes like managing UI state behavior. It might be too slow for parsing XML, realtime signal processing with high sample rates and similar tasks.
Anyway, it should be able to handle several thousand events per second on an iOS device.
Contributing
Feel free to fork the project and send me a pull request if you would like me to integrate your bugfix, enhancement, or feature.
You can easily add new triggering mechanisms — they can be
implemented in few lines by subclassing the Transition
class (see
the implementation of NotificationTransition
for an example).
I'm also open for suggestions regarding the interface design.
To contribute,
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Added some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
If the feature has specs, I will probably merge it :)