FusionAds-iOS/FusionAds/Classes/fusion/utils/state/StateMachine.swift

567 lines
18 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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) {
// messagecallbackcallbackAndroid`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)")
}
}
}