Last active
October 26, 2021 18:39
-
-
Save tadija/674a6dc5c14597394fde2c8a48663263 to your computer and use it in GitHub Desktop.
AEFlow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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