Skip to content

Instantly share code, notes, and snippets.

@tadija
Last active October 26, 2021 18:39
Show Gist options
  • Save tadija/674a6dc5c14597394fde2c8a48663263 to your computer and use it in GitHub Desktop.
Save tadija/674a6dc5c14597394fde2c8a48663263 to your computer and use it in GitHub Desktop.
AEFlow
/**
* https://gist.github.com/tadija/674a6dc5c14597394fde2c8a48663263
* Revision 21
* Copyright © 2018-2021 Marko Tadić
* Licensed under the MIT license
*/
import Foundation
@dynamicMemberLookup
public protocol Flow: AnyObject, CustomDebugStringConvertible {
var parent: Flow? { get set }
var children: [Flow] { get set }
var dynamicStorage: [String: Any] { get set }
func start()
func stop()
func send(_ message: FlowMessage, to receiver: Flow?)
func receive(_ message: FlowMessage, from sender: Flow)
}
public typealias FlowMessage = Notification.Name
public extension FlowMessage {
static let flowWillStart = FlowMessage("flowWillStart")
static let flowDidStart = FlowMessage("flowDidStart")
static let flowWillStop = FlowMessage("flowWillStop")
static let flowDidStop = FlowMessage("flowDidStop")
}
public extension Flow {
subscript(dynamicMember key: String) -> Any? {
get { dynamicStorage[key] }
set { dynamicStorage[key] = newValue }
}
func send(_ message: FlowMessage) {
parent?.receive(message, from: self)
}
}
public extension Flow {
func startChild<T: Flow>(_ child: T) {
addChild(child).start()
}
@discardableResult
func addChild<T: Flow>(_ child: T) -> T {
if !children.contains(where: { $0 === child }) {
child.parent = self
children.append(child)
}
return child
}
func children<T: Flow>(ofType: T.Type) -> [T] {
children.compactMap({ $0 as? T })
}
func removeFromParent() {
parent?.removeChild(self)
}
func removeChild<T: Flow>(_ child: T) {
if let index = children.firstIndex(where: { $0 === child }) {
child.parent = nil
children.remove(at: index)
}
}
func removeAllChildren() {
while !children.isEmpty {
for child in children {
child.removeAllChildren()
child.removeFromParent()
}
}
}
}
public extension Flow {
var root: Flow {
var root: Flow = self
while let parent = root.parent {
root = parent
}
return root
}
var rootDescription: String {
root.recursiveDescription
}
var debugDescription: String {
let address = Unmanaged.passUnretained(self).toOpaque()
return "<\(type(of: self)): \(address)>"
}
var recursiveDescription: String {
var string = String()
addDescription(to: &string, indentLevel: 0)
return string
}
private func addDescription(to string: inout String, indentLevel: Int) {
var level = indentLevel
while level > 0 {
string += "\t"
level -= 1
}
string += "\(debugDescription)\n"
children.forEach {
$0.addDescription(to: &string, indentLevel: indentLevel + 1)
}
}
}
open class FlowObject: Flow, ObservableObject {
public static var isLogEnabled = true
open var isLogEnabled: Bool { Self.isLogEnabled }
public var parent: Flow?
public var children = [Flow]()
public var dynamicStorage = [String: Any]()
public init() {}
open func start() {
send(.flowWillStart)
log("START", sender: self)
}
open func stop() {
send(.flowWillStop)
log("STOP", sender: self)
}
open func send(_ message: FlowMessage, to receiver: Flow? = nil) {
let receiver = receiver ?? parent
receiver?.receive(message, from: self)
}
open func receive(_ message: FlowMessage, from sender: Flow) {
if message == .flowDidStop {
sender.removeFromParent()
log("DID STOP", sender: sender)
}
}
open func log(_ message: String, sender: Flow) {
if isLogEnabled {
print("[AEFlow] \(sender.debugDescription) --> \(message)")
}
}
}
public protocol UIFlowProtocol: Flow {
associatedtype UIType
var ui: UIType! { get set }
}
open class UIFlow<UIType>: FlowObject, UIFlowProtocol {
open var ui: UIType! {
get {
guard let ui = self.dynamicUI as? UIType else {
preconditionFailure("\(type(of: self)) missing `dynamicUI` in storage: \(dynamicStorage)")
}
return ui
}
set {
self.dynamicUI = newValue
}
}
open func getParentUI<T: Any>() -> T {
guard let parent = parent else {
preconditionFailure("\(type(of: self)) missing parent")
}
guard let ui = parent.dynamicUI as? T else {
preconditionFailure("\(type(of: parent)) missing `dynamicUI` in parent storage: \(parent.dynamicStorage)")
}
return ui
}
}
#if canImport(UIKit)
import UIKit
public extension UIFlow {
var parentUI: UIViewController {
getParentUI()
}
}
#endif
public protocol StateFlowProtocol: Flow {
associatedtype StateType
var state: StateType? { get set }
func transition(from oldState: StateType?, to newState: StateType)
}
open class UIStateFlow<UIType, StateType>: UIFlow<UIType>, StateFlowProtocol {
open var state: StateType? {
didSet {
guard let state = state else {
assertionFailure("invalid state"); return
}
transition(from: oldValue, to: state)
}
}
open func transition(from oldState: StateType?, to newState: StateType) {
log("STATE: \(newState)]", sender: self)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment