567 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Swift
		
	
	
		
		
			
		
	
	
			567 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Swift
		
	
	
|  | // StateMachine.swift | |||
|  | // Core state machine implementation | |||
|  | // Ported from Android implementation | |||
|  | 
 | |||
|  | import Foundation | |||
|  | 
 | |||
|  | private struct PendingWorkItem { | |||
|  |     let id: UUID | |||
|  |     weak var work: DispatchWorkItem? | |||
|  |     let what: Int | |||
|  |     let obj: Any? | |||
|  | } | |||
|  | 
 | |||
|  | /// A hierarchical state machine for managing states and transitions | |||
|  | public class StateMachine { | |||
|  |     // MARK: - Constants | |||
|  |      | |||
|  |     /// Message.what value when quitting | |||
|  |     private static let SM_QUIT_CMD = -1 | |||
|  |      | |||
|  |     /// Message.what value when initializing | |||
|  |     private static let SM_INIT_CMD = -2 | |||
|  |      | |||
|  |     // MARK: - Properties | |||
|  |      | |||
|  |     /// Name of the state machine, used for logging | |||
|  |     let name: String | |||
|  |      | |||
|  |     /// Current state of the state machine | |||
|  |     private(set) var currentState: IState? | |||
|  |      | |||
|  |     /// Initial state for the state machine | |||
|  |     private var initialState: IState? | |||
|  |      | |||
|  |     /// Destination state for transitions | |||
|  |     private var destState: IState? | |||
|  |      | |||
|  |     private var params: Any? | |||
|  |      | |||
|  |     /// Parent-child state relationships | |||
|  |     private var stateInfos = [String: StateInfo]() | |||
|  |      | |||
|  |     /// Monitors state changes | |||
|  |     private var monitors = [IStateMonitor]() | |||
|  |      | |||
|  |     /// Processing queue | |||
|  |     private let queue = DispatchQueue.main | |||
|  |      | |||
|  |     /// Queue for deferred messages | |||
|  |     private var deferredMessages = [Message]() | |||
|  |      | |||
|  |     /// Flag to indicate if the state machine has quit | |||
|  |     private var hasQuit = false | |||
|  |      | |||
|  |     /// The current message being processed | |||
|  |     private var currentMessage: Message? | |||
|  |      | |||
|  |     /// Debug flag | |||
|  |     private let isDebugEnabled = false | |||
|  |      | |||
|  |     private var pendingWorks: [PendingWorkItem] = [] | |||
|  |      | |||
|  |     // MARK: - Nested Types | |||
|  |      | |||
|  |     /// Halting state entered when transitionToHaltingState is called | |||
|  |     private class HaltingState: State { | |||
|  |         private weak var stateMachine: StateMachine? | |||
|  |          | |||
|  |         init(stateMachine: StateMachine) { | |||
|  |             self.stateMachine = stateMachine | |||
|  |             super.init() | |||
|  |         } | |||
|  |          | |||
|  |         override func processMessage(_ msg: Message) -> Bool { | |||
|  |             stateMachine?.haltedProcessMessage(msg) | |||
|  |             return true | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Quitting state entered when a valid quit message is handled | |||
|  |     private class QuittingState: State { | |||
|  |         override func processMessage(_ msg: Message) -> Bool { | |||
|  |             return false | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     public func isCurrent(_ state: IState) -> Bool { | |||
|  |         return (currentState as? State) === (state as? State) | |||
|  |     } | |||
|  |      | |||
|  |     /// Information about a state and its relationships | |||
|  |     private class StateInfo { | |||
|  |         let state: IState | |||
|  |         var parentStateInfo: StateInfo? | |||
|  |         var active = false | |||
|  |          | |||
|  |         init(state: IState) { | |||
|  |             self.state = state | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     // MARK: - Lifecycle | |||
|  |      | |||
|  |     /// Initialize a state machine with a name | |||
|  |     /// - Parameter name: Name of the state machine | |||
|  |     public init(name: String) { | |||
|  |         self.name = name | |||
|  |     } | |||
|  |      | |||
|  |     // MARK: - Public Methods | |||
|  |      | |||
|  |     /// Add a new state to the state machine | |||
|  |     /// - Parameters: | |||
|  |     ///   - state: The state to add | |||
|  |     ///   - parent: Optional parent state | |||
|  |     public func addState(_ state: IState, parent: IState? = nil) { | |||
|  |         let stateInfo = StateInfo(state: state) | |||
|  |         stateInfo.active = false | |||
|  |          | |||
|  |         if let parent = parent { | |||
|  |             // Look up the parent state info | |||
|  |             if let parentStateInfo = getStateInfo(parent) { | |||
|  |                 stateInfo.parentStateInfo = parentStateInfo | |||
|  |             } else { | |||
|  |                 log("StateMachine: parent state not found: \(parent.name)") | |||
|  |             } | |||
|  |         } | |||
|  |          | |||
|  |         stateInfo.active = false | |||
|  |         stateInfo.parentStateInfo = parent != nil ? getStateInfo(parent!) : nil | |||
|  |          | |||
|  |         stateInfos[state.name] = stateInfo | |||
|  |     } | |||
|  |      | |||
|  |     /// Set the initial state of the state machine | |||
|  |     /// - Parameter state: The initial state | |||
|  |     public func setInitialState(_ state: IState) { | |||
|  |         initialState = state | |||
|  |     } | |||
|  |      | |||
|  |     /// Add a monitor for state changes | |||
|  |     /// - Parameter monitor: The monitor to add | |||
|  |     public func addStateMonitor(_ monitor: IStateMonitor) { | |||
|  |         monitors.append(monitor) | |||
|  |     } | |||
|  |      | |||
|  |     /// Remove a monitor for state changes | |||
|  |     /// - Parameter monitor: The monitor to remove | |||
|  |     public func removeStateMonitor(_ monitor: IStateMonitor) { | |||
|  |         if let index = monitors.firstIndex(where: { $0 as AnyObject === monitor as AnyObject }) { | |||
|  |             monitors.remove(at: index) | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Start the state machine | |||
|  |     public func start() { | |||
|  |         guard !hasQuit, let initialState = initialState else { | |||
|  |             log("StateMachine: start called without an initial state or after quitting") | |||
|  |             return | |||
|  |         } | |||
|  |          | |||
|  |         queue.async { [weak self] in | |||
|  |             guard let self = self else { return } | |||
|  |             self.setupInitialState() | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Send a message to the state machine | |||
|  |     /// - Parameter message: The message to send | |||
|  |     public func sendMessage(_ message: Message) { | |||
|  |         sendMessageDelayed(message, delayed: .milliseconds(0)) | |||
|  |     } | |||
|  |      | |||
|  |     public func sendMessageDelayed(_ message: Message, delayed: DispatchTimeInterval) { | |||
|  |         if hasQuit { | |||
|  |             log("StateMachine: sendMessage called after quitting") | |||
|  |             return | |||
|  |         } | |||
|  |         let pendingItemId = UUID() | |||
|  |         var work: DispatchWorkItem? | |||
|  |         work = DispatchWorkItem() { | |||
|  |             [weak self] in | |||
|  |             guard let self = self, !(work?.isCancelled ?? true) else { | |||
|  |                 self?.pendingWorks.removeAll { | |||
|  |                     item in | |||
|  |                     return pendingItemId == item.id | |||
|  |                 } | |||
|  |                 return | |||
|  |             } | |||
|  |             if(message.callback != nil) { | |||
|  |                 // 如果message携带callback,则视作由callback承接执行过程,模拟Android的`Handler.post(runnable)`或`Handler.postDelayed(runnable, delay)` | |||
|  |                 message.callback?() | |||
|  |             } else { | |||
|  |                 self.processMessage(message) | |||
|  |             } | |||
|  |             self.pendingWorks.removeAll { | |||
|  |                 item in | |||
|  |                 return pendingItemId == item.id | |||
|  |             } | |||
|  |         } | |||
|  |         if delayed == DispatchTimeInterval.milliseconds(0) { | |||
|  |             queue.async(execute: work!) | |||
|  |         } else { | |||
|  |             pendingWorks.append(PendingWorkItem(id: pendingItemId, work: work, what: message.what, obj: message.obj)) | |||
|  |             queue.asyncAfter(deadline: .now() + delayed, execute: work!) | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     public func sendMessage(what: Int, obj: Any? = nil) { | |||
|  |         sendMessage(Message(what: what, obj: obj)) | |||
|  |     } | |||
|  |      | |||
|  |     public func sendMessageDelayed(what: Int, delayMillis: Int, obj: Any? = nil) { | |||
|  |          | |||
|  |         sendMessageDelayed(Message(what: what, obj: obj), delayed: .milliseconds(delayMillis)) | |||
|  |     } | |||
|  |      | |||
|  |     public func sendMessageDelayed(what: Int, delayed: DispatchTimeInterval, obj: Any? = nil) { | |||
|  |          | |||
|  |         sendMessageDelayed(Message(what: what, obj: obj), delayed: delayed) | |||
|  |     } | |||
|  |      | |||
|  |     public func removeMessages(what: Int) { | |||
|  |         if hasQuit { | |||
|  |             log("StateMachine: removeMessages called after quitting") | |||
|  |             return | |||
|  |         } | |||
|  |         pendingWorks.filter { | |||
|  |             pendingWorkItem in | |||
|  |             return pendingWorkItem.work == nil || pendingWorkItem.what == what | |||
|  |         }.forEach { | |||
|  |             pendingWorkItem in | |||
|  |             if pendingWorkItem.work != nil && !(pendingWorkItem.work?.isCancelled ?? true) { | |||
|  |                 pendingWorkItem.work?.cancel() | |||
|  |             } | |||
|  |         } | |||
|  |          | |||
|  |     } | |||
|  |      | |||
|  |     private func clearPendingWorks() { | |||
|  |         pendingWorks.forEach { | |||
|  |             pendingWorkItem in | |||
|  |             if pendingWorkItem.work != nil && !(pendingWorkItem.work?.isCancelled ?? true) { | |||
|  |                 pendingWorkItem.work?.cancel() | |||
|  |             } | |||
|  |         } | |||
|  |         pendingWorks.removeAll() | |||
|  |     } | |||
|  |      | |||
|  |     public func post(runnable: @escaping () -> Void, token: Any? = nil) { | |||
|  |         postDelayed(runnable: runnable, token: token, delayed: .microseconds(0)) | |||
|  |     } | |||
|  |      | |||
|  |     public func postDelayed(runnable: @escaping () -> Void, token: Any? = nil, delayed: DispatchTimeInterval) { | |||
|  |         sendMessageDelayed(Message(what: 0, obj: token, callback: runnable), delayed: delayed) | |||
|  |     } | |||
|  |      | |||
|  |     private func anyEquals(_ x : Any?, _ y : Any?) -> Bool { | |||
|  |         guard let x = x as? AnyHashable, | |||
|  |               let y = y as? AnyHashable else { return false } | |||
|  |         return x == y | |||
|  |     } | |||
|  |      | |||
|  |     public func removeCallbacksAndMessages(_ token: Any? = nil) { | |||
|  |         if(token == nil) { | |||
|  |             clearPendingWorks() | |||
|  |         } | |||
|  |         pendingWorks.removeAll { | |||
|  |             pendingWorkItem in | |||
|  |             let remove = anyEquals(pendingWorkItem.obj, token) | |||
|  |             if(remove && !(pendingWorkItem.work?.isCancelled ?? true)) { | |||
|  |                 pendingWorkItem.work?.cancel() | |||
|  |             } | |||
|  |             return remove | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Send a message to the front of the queue | |||
|  |     /// - Parameter message: The message to send | |||
|  |     public func sendMessageAtFrontOfQueue(_ message: Message) { | |||
|  |         if hasQuit { | |||
|  |             log("StateMachine: sendMessageAtFrontOfQueue called after quitting") | |||
|  |             return | |||
|  |         } | |||
|  |          | |||
|  |         queue.async(flags: .barrier) { [weak self] in | |||
|  |             guard let self = self else { return } | |||
|  |             self.processMessage(message) | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Transition to a new state | |||
|  |     /// - Parameters: | |||
|  |     ///   - state: The state to transition to | |||
|  |     ///   - params: Optional parameters for the transition | |||
|  |     public func transitionTo(_ state: IState, params: Any? = nil) { | |||
|  |         if hasQuit { | |||
|  |             log("StateMachine: transitionTo called after quitting") | |||
|  |             return | |||
|  |         } | |||
|  |          | |||
|  |         guard let stateInfo = getStateInfo(state) else { | |||
|  |             log("StateMachine: transitionTo called with unrecognized state: \(state.name)") | |||
|  |             return | |||
|  |         } | |||
|  |          | |||
|  |         destState = state | |||
|  |         self.params = params | |||
|  |     } | |||
|  |      | |||
|  |     /// Transition to the halting state | |||
|  |     public func transitionToHaltingState() { | |||
|  |         let haltingState = HaltingState(stateMachine: self) | |||
|  |         addState(haltingState) | |||
|  |         transitionTo(haltingState) | |||
|  |     } | |||
|  |      | |||
|  |     /// Quit the state machine | |||
|  |     public func quit() { | |||
|  |         if hasQuit { | |||
|  |             log("StateMachine: quit called after already quitting") | |||
|  |             return | |||
|  |         } | |||
|  |          | |||
|  |         let quittingState = QuittingState() | |||
|  |         addState(quittingState) | |||
|  |         transitionTo(quittingState) | |||
|  |          | |||
|  |         queue.async(flags: .barrier) { [weak self] in | |||
|  |             guard let self = self else { return } | |||
|  |             self.cleanupAfterQuitting() | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Defer a message to be processed after the next state transition | |||
|  |     /// - Parameter message: The message to defer | |||
|  |     public func deferMessage(_ message: Message) { | |||
|  |         if hasQuit { | |||
|  |             log("StateMachine: deferMessage called after quitting") | |||
|  |             return | |||
|  |         } | |||
|  |          | |||
|  |         deferredMessages.append(message) | |||
|  |     } | |||
|  |      | |||
|  |     // MARK: - public Methods | |||
|  |      | |||
|  |     /// Called for any message received after transitioning to halting state | |||
|  |     /// - Parameter message: The message received | |||
|  |     open func haltedProcessMessage(_ message: Message) { | |||
|  |         // Default implementation does nothing | |||
|  |     } | |||
|  |      | |||
|  |     /// Called once after transitioning to halting state | |||
|  |     open func onHalting() { | |||
|  |         // Default implementation does nothing | |||
|  |     } | |||
|  |      | |||
|  |     /// Called once after quitting | |||
|  |     open func onQuitting() { | |||
|  |         // Default implementation does nothing | |||
|  |     } | |||
|  |      | |||
|  |     /// Called when a message is not handled by any state | |||
|  |     /// - Parameter message: The unhandled message | |||
|  |     open func unhandledMessage(_ message: Message) { | |||
|  |         // Default implementation does nothing | |||
|  |     } | |||
|  |      | |||
|  |     // MARK: - Private Methods | |||
|  |      | |||
|  |     /// Get the StateInfo for a state | |||
|  |     /// - Parameter state: The state to get info for | |||
|  |     /// - Returns: StateInfo object or nil if not found | |||
|  |     private func getStateInfo(_ state: IState) -> StateInfo? { | |||
|  |         return stateInfos[state.name] | |||
|  |     } | |||
|  |      | |||
|  |     /// Setup the initial state | |||
|  |     private func setupInitialState() { | |||
|  |         guard let initialState = initialState else { return } | |||
|  |          | |||
|  |         currentState = initialState | |||
|  |          | |||
|  |         // Find all parent states of the initial state | |||
|  |         var parentStates = [IState]() | |||
|  |         var stateInfo = getStateInfo(initialState) | |||
|  |          | |||
|  |         while let info = stateInfo?.parentStateInfo { | |||
|  |             parentStates.insert(info.state, at: 0) | |||
|  |             stateInfo = info | |||
|  |         } | |||
|  |          | |||
|  |         // Enter parent states from top to bottom | |||
|  |         for state in parentStates { | |||
|  |             state.enter(from: nil, params: nil) | |||
|  |         } | |||
|  |          | |||
|  |         // Enter initial state | |||
|  |         initialState.enter(from: nil, params: nil) | |||
|  |          | |||
|  |         // Process any deferred messages | |||
|  |         processDeferredMessages() | |||
|  |     } | |||
|  |      | |||
|  |     /// Process a message through the state hierarchy | |||
|  |     /// - Parameter message: The message to process | |||
|  |     private func processMessage(_ message: Message) { | |||
|  |         if hasQuit { | |||
|  |             return | |||
|  |         } | |||
|  |          | |||
|  |         currentMessage = message | |||
|  |          | |||
|  |         if message.what == StateMachine.SM_QUIT_CMD { | |||
|  |             transitionTo(QuittingState()) | |||
|  |             return | |||
|  |         } | |||
|  |          | |||
|  |         var state: IState? = currentState | |||
|  |         var handled = false | |||
|  |          | |||
|  |         // Try to handle the message starting with the current state and going up the hierarchy | |||
|  |         while let currentState = state, !handled { | |||
|  |             handled = currentState.processMessage(message) | |||
|  |             if !handled { | |||
|  |                 state = getStateInfo(currentState)?.parentStateInfo?.state | |||
|  |             } | |||
|  |         } | |||
|  |          | |||
|  |         if !handled { | |||
|  |             unhandledMessage(message) | |||
|  |         } | |||
|  |          | |||
|  |         performStateTransitions(message) | |||
|  |          | |||
|  |         currentMessage = nil | |||
|  |     } | |||
|  |      | |||
|  |     /// Perform any pending state transitions | |||
|  |     /// - Parameter message: The message that triggered potential transitions | |||
|  |     private func performStateTransitions(_ message: Message) { | |||
|  |         if let destState = destState { | |||
|  |             let orgState = currentState | |||
|  |              | |||
|  |             // Find common ancestor state | |||
|  |             let (commonAncestor, statesToExit, statesToEnter) = findCommonAncestor(currentState!, destState) | |||
|  |              | |||
|  |             // Exit current states up to common ancestor | |||
|  |             for state in statesToExit { | |||
|  |                 state.exit(to: destState) | |||
|  |             } | |||
|  |              | |||
|  |             // Enter new states from common ancestor down to destination | |||
|  |             for state in statesToEnter { | |||
|  |                 state.enter(from: orgState, params: params) | |||
|  |             } | |||
|  |              | |||
|  |             // Update current state | |||
|  |             currentState = destState | |||
|  |              | |||
|  |             // Process deferred messages | |||
|  |             processDeferredMessages() | |||
|  |              | |||
|  |             // Notify monitors | |||
|  |             notifyMonitors(destState, lastState: orgState, params: message.obj) | |||
|  |              | |||
|  |             // Reset destination state | |||
|  |             self.destState = nil | |||
|  |             self.params = nil | |||
|  |              | |||
|  |             // Check if we're entering a special state | |||
|  |             if destState is QuittingState { | |||
|  |                 onQuitting() | |||
|  |                 cleanupAfterQuitting() | |||
|  |             } else if destState is HaltingState { | |||
|  |                 onHalting() | |||
|  |             } | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Find the common ancestor of two states and determine which states to exit and enter | |||
|  |     /// - Parameters: | |||
|  |     ///   - currentState: The current state | |||
|  |     ///   - destState: The destination state | |||
|  |     /// - Returns: Tuple containing common ancestor, states to exit, and states to enter | |||
|  |     private func findCommonAncestor(_ currentState: IState, _ destState: IState) -> (IState?, [IState], [IState]) { | |||
|  |         var commonAncestor: IState? = nil | |||
|  |         var statesToExit = [IState]() | |||
|  |         var statesToEnter = [IState]() | |||
|  |          | |||
|  |         // Build path from current state to root | |||
|  |         var currentPath = [IState]() | |||
|  |         var currentStateInfo = getStateInfo(currentState) | |||
|  |         while let info = currentStateInfo { | |||
|  |             currentPath.insert(info.state, at: 0) | |||
|  |             currentStateInfo = info.parentStateInfo | |||
|  |         } | |||
|  |          | |||
|  |         // Build path from destination state to root | |||
|  |         var destPath = [IState]() | |||
|  |         var destStateInfo = getStateInfo(destState) | |||
|  |         while let info = destStateInfo { | |||
|  |             destPath.insert(info.state, at: 0) | |||
|  |             destStateInfo = info.parentStateInfo | |||
|  |         } | |||
|  |          | |||
|  |         // Find common ancestor | |||
|  |         var commonIndex = 0 | |||
|  |         while commonIndex < min(currentPath.count, destPath.count) && currentPath[commonIndex].name == destPath[commonIndex].name { | |||
|  |             commonAncestor = currentPath[commonIndex] | |||
|  |             commonIndex += 1 | |||
|  |         } | |||
|  |          | |||
|  |         // States to exit - from current state up to (but not including) common ancestor | |||
|  |         statesToExit = Array(currentPath.reversed().prefix(currentPath.count - commonIndex)) | |||
|  |          | |||
|  |         // States to enter - from one below common ancestor down to destination state | |||
|  |         statesToEnter = Array(destPath.suffix(destPath.count - commonIndex)) | |||
|  |          | |||
|  |         return (commonAncestor, statesToExit, statesToEnter) | |||
|  |     } | |||
|  |      | |||
|  |     /// Process all deferred messages | |||
|  |     private func processDeferredMessages() { | |||
|  |         guard !deferredMessages.isEmpty else { return } | |||
|  |          | |||
|  |         let messages = deferredMessages | |||
|  |         deferredMessages.removeAll() | |||
|  |          | |||
|  |         for message in messages { | |||
|  |             sendMessageAtFrontOfQueue(message) | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Notify all monitors of a state change | |||
|  |     /// - Parameters: | |||
|  |     ///   - newState: The new state | |||
|  |     ///   - lastState: The previous state | |||
|  |     ///   - params: Optional parameters | |||
|  |     private func notifyMonitors(_ newState: IState, lastState: IState?, params: Any?) { | |||
|  |         for monitor in monitors { | |||
|  |             monitor.onStateChanged(stateMachine: self, state: newState, lastState: lastState, params: params) | |||
|  |         } | |||
|  |     } | |||
|  |      | |||
|  |     /// Clean up resources after quitting | |||
|  |     private func cleanupAfterQuitting() { | |||
|  |         hasQuit = true | |||
|  |         currentState = nil | |||
|  |         initialState = nil | |||
|  |         destState = nil | |||
|  |         deferredMessages.removeAll() | |||
|  |         stateInfos.removeAll() | |||
|  |         monitors.removeAll() | |||
|  |         clearPendingWorks() | |||
|  |     } | |||
|  |      | |||
|  |     /// Log a message if debugging is enabled | |||
|  |     /// - Parameter message: The message to log | |||
|  |     private func log(_ message: String) { | |||
|  |         if isDebugEnabled { | |||
|  |             print("\(name): \(message)") | |||
|  |         } | |||
|  |     } | |||
|  | } |