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)")
 | ||
|         }
 | ||
|     }
 | ||
| }
 |