Skip to content

Commit

Permalink
Final and background states.
Browse files Browse the repository at this point in the history
  • Loading branch information
drekka committed Mar 24, 2019
1 parent 437f217 commit 0d59445
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 184 deletions.
8 changes: 4 additions & 4 deletions Machinus.playground/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ enum UserState: StateIdentifier {
case loggedOut
}

let initialising = State<UserState>(withIdentifier: .initialising, allowedTransitions: .registering, .loggedOut)
let initialising = StateConfig<UserState>(identifier: .initialising, allowedTransitions: .registering, .loggedOut)

let registering = State<UserState>(withIdentifier: .registering, allowedTransitions: .loggedIn)
let registering = StateConfig<UserState>(identifier: .registering, allowedTransitions: .loggedIn)
.afterEntering { _ in
registerUser()
}

let loggedIn = State<UserState>(withIdentifier: .loggedIn, allowedTransitions: .loggedOut)
let loggedIn = StateConfig<UserState>(identifier: .loggedIn, allowedTransitions: .loggedOut)
.afterEntering { _ in
displayUserHome()
}

let loggedOut = State<UserState>(withIdentifier: .loggedOut, allowedTransitions: .loggedIn)
let loggedOut = StateConfig<UserState>(identifier: .loggedOut, allowedTransitions: .loggedIn)
.afterEntering { _ in
displayEnterPassword()
}
Expand Down
22 changes: 11 additions & 11 deletions Machinus.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
6C920DD5221187630084E6E8 /* Machinus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C920DCB221187630084E6E8 /* Machinus.framework */; };
6C920DDA221187630084E6E8 /* MachinusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C920DD9221187630084E6E8 /* MachinusTests.swift */; };
6C920DDC221187630084E6E8 /* Machinus.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C920DCE221187630084E6E8 /* Machinus.h */; settings = {ATTRIBUTES = (Public, ); }; };
6C920DE622118BBF0084E6E8 /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C920DE522118BBF0084E6E8 /* State.swift */; };
6C920DE622118BBF0084E6E8 /* StateConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C920DE522118BBF0084E6E8 /* StateConfig.swift */; };
6C920DE822118BCB0084E6E8 /* Machinus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C920DE722118BCB0084E6E8 /* Machinus.swift */; };
6C920DEA22118CB20084E6E8 /* StateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C920DE922118CB20084E6E8 /* StateTests.swift */; };
6C920DEA22118CB20084E6E8 /* StateConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C920DE922118CB20084E6E8 /* StateConfigTests.swift */; };
6C920DEE221190B60084E6E8 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C920DED221190B60084E6E8 /* Nimble.framework */; };
6CFBB324221D8B08007E6722 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CFBB323221D8B08007E6722 /* Notifications.swift */; };
/* End PBXBuildFile section */
Expand All @@ -42,9 +42,9 @@
6C920DD4221187630084E6E8 /* MachinusTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MachinusTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6C920DD9221187630084E6E8 /* MachinusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MachinusTests.swift; sourceTree = "<group>"; };
6C920DDB221187630084E6E8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6C920DE522118BBF0084E6E8 /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = "<group>"; };
6C920DE522118BBF0084E6E8 /* StateConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateConfig.swift; sourceTree = "<group>"; };
6C920DE722118BCB0084E6E8 /* Machinus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Machinus.swift; sourceTree = "<group>"; };
6C920DE922118CB20084E6E8 /* StateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateTests.swift; sourceTree = "<group>"; };
6C920DE922118CB20084E6E8 /* StateConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateConfigTests.swift; sourceTree = "<group>"; };
6C920DEB221190110084E6E8 /* Cartfile.private */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile.private; sourceTree = "<group>"; };
6C920DED221190B60084E6E8 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = "<group>"; };
6CFBB323221D8B08007E6722 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,7 +102,7 @@
6C920DE722118BCB0084E6E8 /* Machinus.swift */,
6C258BE022439DA40033F66E /* MachinusError.swift */,
6CFBB323221D8B08007E6722 /* Notifications.swift */,
6C920DE522118BBF0084E6E8 /* State.swift */,
6C920DE522118BBF0084E6E8 /* StateConfig.swift */,
6C258BE222439DEF0033F66E /* StateMachine.swift */,
);
path = Machinus;
Expand All @@ -111,10 +111,10 @@
6C920DD8221187630084E6E8 /* MachinusTests */ = {
isa = PBXGroup;
children = (
6C920DD9221187630084E6E8 /* MachinusTests.swift */,
6C920DDB221187630084E6E8 /* Info.plist */,
6C920DE922118CB20084E6E8 /* StateTests.swift */,
6C920DD9221187630084E6E8 /* MachinusTests.swift */,
6C83733A222D3C39001C57E6 /* NotificationTests.swift */,
6C920DE922118CB20084E6E8 /* StateConfigTests.swift */,
);
path = MachinusTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -261,7 +261,7 @@
files = (
6C258BE122439DA40033F66E /* MachinusError.swift in Sources */,
6CFBB324221D8B08007E6722 /* Notifications.swift in Sources */,
6C920DE622118BBF0084E6E8 /* State.swift in Sources */,
6C920DE622118BBF0084E6E8 /* StateConfig.swift in Sources */,
6C258BE322439DEF0033F66E /* StateMachine.swift in Sources */,
6C920DE822118BCB0084E6E8 /* Machinus.swift in Sources */,
);
Expand All @@ -273,7 +273,7 @@
files = (
6C920DDA221187630084E6E8 /* MachinusTests.swift in Sources */,
6C83733B222D3C39001C57E6 /* NotificationTests.swift in Sources */,
6C920DEA22118CB20084E6E8 /* StateTests.swift in Sources */,
6C920DEA22118CB20084E6E8 /* StateConfigTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -419,7 +419,7 @@
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 3MHZ7974Y8;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_CURRENT_VERSION = 0.0.4;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Machinus/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
Expand All @@ -446,7 +446,7 @@
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 3MHZ7974Y8;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_CURRENT_VERSION = 0.0.4;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Machinus/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
Expand Down
2 changes: 1 addition & 1 deletion Machinus/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>0.0.4</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
Expand Down
47 changes: 30 additions & 17 deletions Machinus/Machinus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import os
/// A generalised implementation of the `StateMachine` protocol.
public class Machinus<T>: StateMachine where T: StateIdentifier {

private var current: State<T>
private var states: [State<T>]
private var current: StateConfig<T>
private var states: [StateConfig<T>]

private var beforeTransition: ((T, T) -> Void)?
private var afterTransition: ((T, T) -> Void)?
Expand All @@ -31,9 +31,9 @@ public class Machinus<T>: StateMachine where T: StateIdentifier {
return current.identifier
}

public var sameStateAsError: Bool = false

public var postNotifications: Bool = false
public var enableSameStateError = false
public var enableFinalStateTransitionError = false
public var postNotifications = false

public var transitionQ: DispatchQueue = DispatchQueue.main

Expand All @@ -44,9 +44,14 @@ public class Machinus<T>: StateMachine where T: StateIdentifier {
return
}

// Validate the state is known and not final
if state(forIdentifier: backgroundState).isFinal {
fatalError("More than one state is using the same identifier")
}

os_log("🤖 Setting .%@ as the background state.", type: .debug, String(describing: backgroundState))
_ = state(forIdentifier: backgroundState)

// Adding notification watching for backgrounding and foregrounding.
backgroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
guard let self = self else { return }
os_log("🤖 Transitioning to background state .%@", type: .debug, String(describing: backgroundState))
Expand All @@ -71,13 +76,13 @@ public class Machinus<T>: StateMachine where T: StateIdentifier {
}

public init(name: String = UUID().uuidString + "<" + String(describing: T.self) + ">",
withStates firstState: State<T>,
_ secondState: State<T>,
_ thirdState: State<T>,
_ otherStates: State<T>...) {
withStates firstState: StateConfig<T>,
_ secondState: StateConfig<T>,
_ thirdState: StateConfig<T>,
_ otherStates: StateConfig<T>...) {

self.name = name
let states:[State<T>] = [firstState, secondState, thirdState] + otherStates
let states:[StateConfig<T>] = [firstState, secondState, thirdState] + otherStates

self.states = states
self.current = firstState
Expand Down Expand Up @@ -146,9 +151,9 @@ public class Machinus<T>: StateMachine where T: StateIdentifier {

}

private func state(forIdentifier identifier: T) -> State<T> {
private func state(forIdentifier identifier: T) -> StateConfig<T> {
guard let state = states.first(where: { $0.identifier == identifier }) else {
fatalError("🤖 .\(identifier) is an unknown state.")
fatalError("🤖 State .\(identifier) not registered.")
}
return state
}
Expand All @@ -158,7 +163,7 @@ public class Machinus<T>: StateMachine where T: StateIdentifier {
return state == backgroundState || current == backgroundState
}

private func preflightTransition(toState toStateIdentifier: T, completion: @escaping (_ previousState: T?, _ error: Error?) -> Void) -> State<T>? {
private func preflightTransition(toState toStateIdentifier: T, completion: @escaping (_ previousState: T?, _ error: Error?) -> Void) -> StateConfig<T>? {

os_log("🤖 Pre-flighting transition ...", type: .debug)

Expand All @@ -167,16 +172,24 @@ public class Machinus<T>: StateMachine where T: StateIdentifier {
// If the state is the same state then do nothing.
guard current != toStateIdentifier else {
os_log("🤖 Already in state", type: .debug)
completion(nil, sameStateAsError ? MachinusError.alreadyInState : nil)
completion(nil, enableSameStateError ? MachinusError.alreadyInState : nil)
return nil
}

// Ignore the rest of the pre-flight if we are about to transition to or from the background state.
if isBackgroundTransition(toOrFromState: toStateIdentifier) {
os_log("🤖 Transitioning to or from background state .%@", type: .debug, String(describing: backgroundState))
os_log("🤖 Transitioning to or from background state .%@, ignoring allowed and barriers.", type: .debug, String(describing: backgroundState!))
return newState
}

// Check for a final state transition
if current.isFinal {
os_log("🤖 Final state, cannot transition", type: .error)
completion(nil, enableFinalStateTransitionError ? MachinusError.finalState : nil)
return nil
}


guard newState.transitionBarrier() else {
os_log("🤖 Transition barrier blocked transition", type: .debug)
completion(nil, MachinusError.transitionDenied)
Expand All @@ -192,7 +205,7 @@ public class Machinus<T>: StateMachine where T: StateIdentifier {
return newState
}

private func executeTransition(toState: State<T>, completion: @escaping (_ previousState: T?, _ error: Error?) -> Void) {
private func executeTransition(toState: StateConfig<T>, completion: @escaping (_ previousState: T?, _ error: Error?) -> Void) {

os_log("🤖 Executing transition ...", type: .debug)

Expand Down
3 changes: 3 additions & 0 deletions Machinus/MachinusError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ public enum MachinusError: Error {

/// Thrown when there is no dynamic transition defined on the current state.
case dynamicTransitionNotDefined

/// Thrown if youu attempt to transition from a final state.
case finalState
}
69 changes: 49 additions & 20 deletions Machinus/State.swift → Machinus/StateConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,23 @@ public protocol StateIdentifier: Hashable {}
/**
Defines the setup of an individual state.
*/
open class State<T> where T: StateIdentifier {
open class StateConfig<T> where T: StateIdentifier {

/// The unique identifier used to define this state. This will be used in all `Equatable` tests.
public let identifier: T

// Accessed during transitions
private(set) var isFinal = false
private(set) var beforeLeaving: ((T) -> Void)?
private(set) var afterLeaving: ((T) -> Void)?
private(set) var beforeEntering: ((T) -> Void)?
private(set) var afterEntering: ((T) -> Void)?
private(set) var dynamicTransition: (() -> T)?
private(set) var transitionBarrier: () -> Bool = { true }

private let allowedTransitions: [T]
private var isGlobal = false

var beforeLeaving: ((T) -> Void)?
var afterLeaving: ((T) -> Void)?
var beforeEntering: ((T) -> Void)?
var afterEntering: ((T) -> Void)?
var dynamicTransition: (() -> T)?

var transitionBarrier: () -> Bool = { true }

// MARK: - Lifecycle

/**
Expand All @@ -38,7 +39,7 @@ open class State<T> where T: StateIdentifier {
- Parameter identifier: The unique identifier of the state.
- Parameter allowedTransitions: A list of state identifiers for states that can be transitioned to.
*/
public init(withIdentifier identifier: T, allowedTransitions: T...) {
public init(identifier: T, allowedTransitions: T...) {
self.identifier = identifier
self.allowedTransitions = allowedTransitions
}
Expand All @@ -48,8 +49,8 @@ open class State<T> where T: StateIdentifier {

- Parameter toState: The state that is being queried.
- Returns: true if a transition from this state to the other state is allowed.
*/
func canTransition(toState: State<T>) -> Bool {
*/
func canTransition(toState: StateConfig<T>) -> Bool {
return toState.isGlobal || allowedTransitions.contains(toState.identifier)
}

Expand Down Expand Up @@ -77,6 +78,7 @@ open class State<T> where T: StateIdentifier {
*/
@discardableResult public func beforeLeaving(_ beforeLeaving: @escaping (_ nextState: T) -> Void) -> Self {
self.beforeLeaving = beforeLeaving
validateFinalState()
return self
}

Expand All @@ -89,6 +91,7 @@ open class State<T> where T: StateIdentifier {
*/
@discardableResult public func afterLeaving(_ afterLeaving: @escaping (_ nextState: T) -> Void) -> Self {
self.afterLeaving = afterLeaving
validateFinalState()
return self
}

Expand Down Expand Up @@ -121,9 +124,10 @@ open class State<T> where T: StateIdentifier {

- Parameter dynamicTransition: A closure whose return value defines the next state to transition to.
- Returns: self
*/
*/
@discardableResult public func withDynamicTransitions( _ dynamicTransition: @escaping () -> T) -> Self {
self.dynamicTransition = dynamicTransition
validateFinalState()
return self
}

Expand All @@ -134,38 +138,63 @@ open class State<T> where T: StateIdentifier {
Global states are suitable for things like errors.

- Returns: self.
*/
*/
@discardableResult public func makeGlobal() -> Self {
isGlobal = true
return self
}

/**
When set, defines a state as being a final state.

One a final state has been entered it cannot be left. Only a machine reset gets you out of a final start. Final states cannot have exit actions or dynamic transitions.

- Returns: self.
*/
@discardableResult public func makeFinal() -> Self {
isFinal = true
validateFinalState()
return self
}

// MARK: - Internal

private func validateFinalState() {
if isFinal && (
!allowedTransitions.isEmpty
|| beforeLeaving != nil
|| afterLeaving != nil
|| dynamicTransition != nil) {
fatalError("🤖 Illegal config, final state .\(identifier) cannot have allowedTransitions, leaving or dynamic transition closures.")
}
}
}

// MARK: - Hashable

extension State: Hashable {
extension StateConfig: Hashable {

public var hashValue: Int {
return identifier.hashValue
}

public static func == (lhs: State<T>, rhs: State<T>) -> Bool {
public static func == (lhs: StateConfig<T>, rhs: StateConfig<T>) -> Bool {
return lhs.identifier == rhs.identifier
}

public static func == (lhs: T, rhs: State<T>) -> Bool {
public static func == (lhs: T, rhs: StateConfig<T>) -> Bool {
return lhs == rhs.identifier
}

public static func == (lhs: State<T>, rhs: T) -> Bool {
public static func == (lhs: StateConfig<T>, rhs: T) -> Bool {
return lhs.identifier == rhs
}

public static func != (lhs: T, rhs: State<T>) -> Bool {
public static func != (lhs: T, rhs: StateConfig<T>) -> Bool {
return lhs != rhs.identifier
}

public static func != (lhs: State<T>, rhs: T) -> Bool {
public static func != (lhs: StateConfig<T>, rhs: T) -> Bool {
return lhs.identifier != rhs
}
}
Loading

0 comments on commit 0d59445

Please sign in to comment.