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