Home

Awesome

HSM

UML statecharts (Hierarchical State Machine) framework implementation in Swift (Disclaimer: the framework is under development and is subject to change.)

<p> <img src="https://img.shields.io/badge/Swift-5.1-orange" alt="Swift" /> <img src="https://img.shields.io/badge/platform-macOS%20|%20iOS-orange.svg" alt="Platform" /> <img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-orange" alt="SPM" /> <a href="https://github.com/SerhiyButz/HSM/blob/master/LICENSE"> <img src="https://img.shields.io/badge/licence-MIT-orange" alt="License" /> </a> </p>

TODO: Work through the idea of using the Actor approach like in the ImageDownloader example from [https://developer.apple.com/videos/play/wwdc2021/10133] to address the run to completion problem when state transition in asynchronous/concurrent code. For example, the need for transient states "inProgress"/"is...ing" involves some overhead strategy logic, for example, to discard concurrent controls of flow when they meet the state machine in those states. Another approach whould be to use completion handlers for all calls to the state machine, that are automatically called back when the state is back to normal (non-transient).

TODO: My HSM doesn’t address the problem of state scope (lifetime). When transitioning from a state, its attributes (dependencies) should be destroyed, and on transitioning to a state, its attributes (dependencies) should be created anew.

Contents

Intro

User Interface (UI) - provides the interaction between people and machines. A good user interface is simple, efficient, enjoyable, ergonomic, and intuitive (user-friendly).

The first UI in which people interacted with a computer system in real time was the command line interface (CLI). It was a simple UI in terms of implementation, where the user had to type commands that the system would execute. In order to interact with the system, the user had to know the command language. And users were responsible for making sure that the interaction syntax was always correct.

With the development of computer graphics, another class of UI's emerged: Direct Manipulation Graphical User Interfaces (GUI's), which are much easier to use than CLI, because the user does not need to know the command language to interact with the system. Instead, individual graphical elements such as buttons, scrollbars, and windows represent different program entities, and the user can directly interact with these entities without having to type commands. This UI allows the user to interact effectively with the system without any prior knowledge of the interface. UI's with direct manipulation are easy to use because the state of the object is visible to the user and the user can manipulate the object directly.

GUI software is built on top of a reactive application framework (a supervisory event-driven infrastructure) with business logic actions performed asynchronously in event handler callbacks. Control resides in the event-driven infrastructure, so from the application standpoint the control is inverted compared to a traditional sequential program (as in the case of the old batch-interface software).

Each GUI object can respond to external events, such as those coming from the user (as part of the user interaction), the operating system, or the application itself. The sequence of events entering the application determines the control flow. The developers are responsible for ensuring the syntax of the control flow (including the user interaction) is always correct.

The correct (expected) control flow consists of valid event orderings or "happy paths". The problem is, as the number of possible events increases, the number of "unhappy paths" (unexpected orderings of events) has a factorial growth and it quickly outgrows the capacity of the human mind, and there always comes a point where bugs are inevitable.

UI developers face the challenge of ensuring that all UI objects and dialogs in the application are coordinated in such a way that it is impossible for a user to perform operations that would lead to an error. UI objects do not behave independently of each other and the developer is responsible for ensuring that a user can only supply valid events at any given time. In fact, the events a user supplies (when interacting with the UI) cause the application to move from one set of possible events to another. In other words, the UI moves from one state to another, and the state defines the set of possible events a user can supply. The states define the context in which an event occurs. Mostly, the states are not explicitly specified in the UI code. However, if these states were to be represented on a diagram explicitly and used as the basis for constructing the control layer objects, the UI would be much easier to build.

This is where the event-state-action paradigm (as opposed to the oversimplified event-action paradigm) comes in, leading to such a technique as Finite State Machine (FSM). But for a long time, the idea of building a real UI-based project on FSM was not considered viable because of the problem known as the state explosion problem (large number of states and event arrows) that occurs when using state transition diagrams as the design notation for designing any but trivial UI's. This problem was later addressed by UML statechart notation (originally proposed by David Harel), which is essentially a hierarchical extention to the FSM, Hierarchical State Machine (HSM).

Here are some facts about UML statecharts:

This framework was developed as part of a exploration of different architectures for structured behavior modeling in the development of the UI control layer in Swift.

Features:

The framework was developed with the following aspirations:

Note: These requirements often contradict each other, so this implementation strives to achieve a balanced implementation.

Further development objectives:

Usage

An understanding of Harel's statecharts and their concepts is required for work with this framework. At the end of this page you will find some links to resources on this topic.

Consider the following statechart diagram:

You can represent this diagram in the framework with the following code:

class MyTop: TopState<MyEvent> {
    class Substate1: State<MyTop, MyTop> {
        override func entry() { ... } // optional
        override func exit() { ... } // optional
        override func handle(_ event: MyEvent) -> Transition? { // optional
            switch event {
                case .evt1(let flag) where flag:
                    return Transition(to: superior.substate2)
                default:
                    return nil
            }
        }
    }
    let substate1 = Substate1()

    class Substate2: State<MyTop, MyTop> {
        override func entry() { ... } // optional
        override func exit() { ... } // optional
        override func handle(_ event: MyEvent) -> Transition? { ... } // optional
    }
    let substate2 = Substate2()

    override func initialize() {
        bind(substate1, substate2)
        initial = substate1 // optional
        historyMode = .shallow // optional
    }
}

Here we have a top state MyTop which is a composite state and it's parameterized by our event type MyEvent. The MyTop contains 2 sub-states Substate1 and Substate2. All sub-states in the framework have the same structure. A substate must be explicitly parameterized with two types: (1) its immediate superior state, the immediate container (or parent), (2) and the top state (which is always the most superior state). This serves to bind all state types nested in the hierarchy of states, and to enable statically referring to the immediate superior and topmost state from event handlers and state reactions; for this each state has 2 properties superior and top.

Each state must bind its sub-states in the initialize() method, passing them as arguments in the bind(...) call. The initialize() method is also the method in which you configure initial (or default) sub-state as well as the state history mode.

Demos

Here are simple demo apps so that you can try and hands-on experiment with the framework yourself.

HSM-based running lights demo

Statechart diagram:

Here's the source code of the HSM-based application controller for the above statechart diagram:

import Foundation
import HSM

/// HSM-based controller
class Controller: TopState<Event> {
    // MARK: - Substates

    class Lights: State<Controller, Controller> {
        class Red: State<Lights, Controller> {
            override func entry() {
                top.actions.turnOnRedLed()
            }
            override func exit() {
                top.actions.turnOffRedLed()
            }
            override func handle(_ event: Event) -> Transition? {
                switch event {
                case .timerTick: return Transition(to: superior.green)
                default: return nil
                }
            }
        }
        let red = Red()

        class Green: State<Lights, Controller> {
            override func entry() {
                top.actions.turnOnGreenLed()
            }
            override func exit() {
                top.actions.turnOffGreenLed()
            }
            override func handle(_ event: Event) -> Transition? {
                switch event {
                case .timerTick: return Transition(to: superior.blue)
                default: return nil
                }
            }
        }
        let green = Green()

        class Blue: State<Lights, Controller> {
            override func entry() {
                top.actions.turnOnBlueLed()
            }
            override func exit() {
                top.actions.turnOffBlueLed()
            }
            override func handle(_ event: Event) -> Transition? {
                switch event {
                case .timerTick: return Transition(to: superior.red)
                default: return nil
                }
            }
        }
        let blue = Blue()

        // MARK: - Initialization

        override func initialize() {
            bind(red, green, blue)
            initial = red
            historyMode = .shallow
        }

        // MARK: - Lifecycle

        override func entry() {
            top.actions.clear()
        }

        override func handle(_ event: Event) -> Transition? {
            switch event {
            case .buttonTap: return Transition(to: superior.paused)
            default: return nil
            }
        }
    }
    let lights = Lights()

    class Paused: State<Controller, Controller> {
        override func handle(_ event: Event) -> Transition? {
            switch event {
            case .buttonTap: return Transition(to: superior.lights)
            default: return nil
            }
        }
    }
    let paused = Paused()

    // MARK: - Properties

    let actions: Actions

    // MARK: - Initialization

    init(actions: Actions) {
        self.actions = actions
    }

    override func initialize() {
        bind(lights, paused)
        initial = lights
    }
}

HSM-based calculator demo

Statechart diagram:

Examine the source code of the HSM-based application controller for the above statechart diagram here.

Installation

Swift Package as dependency in Xcode 11+

  1. Go to "File" -> "Swift Packages" -> "Add Package Dependency"
  2. Paste HSM repository URL into the search field:
https://github.com/SerhiyButz/HSM.git
  1. Click "Next"
  2. Ensure that the "Rules" field is set to something like this: "Version: Up To Next Minor: 0.11.1"
  3. Click "Next" to finish

For more info, check out here.

License

This project is licensed under the MIT license.

Resources