增加换肤功能
This commit is contained in:
146
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Action.swift
Normal file
146
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Action.swift
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
An event happening in an application.
|
||||
|
||||
`Action`s are dispatched on the `Store`.
|
||||
*/
|
||||
public protocol Action {}
|
||||
|
||||
/// An `Action` which can be encoded.
|
||||
public protocol EncodableAction: Action, Encodable {
|
||||
/**
|
||||
To enable encoding of the `Action` this helper function is needed.
|
||||
|
||||
`JSONEncoder` can't encode an `Encodable` type unless it has the specific type.
|
||||
By using an `extension` of the `Action` we have this specific type and can encode it.
|
||||
*/
|
||||
func encode(with encoder: JSONEncoder) -> Data?
|
||||
}
|
||||
|
||||
public extension EncodableAction {
|
||||
func encode(with encoder: JSONEncoder) -> Data? {
|
||||
return try? encoder.encode(self)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A template for creating `Action`s.
|
||||
|
||||
The template can have a `Payload`type which is used when creating an actual `Action` from the template.
|
||||
*/
|
||||
public struct ActionTemplate<Payload> {
|
||||
/// The identifier for the `ActionTemplate`
|
||||
public let id: String
|
||||
/// The type of the `Payload`
|
||||
public let payloadType: Payload.Type
|
||||
|
||||
/**
|
||||
Initializes an `ActionTemplate` with the given `payloadType`.
|
||||
|
||||
- Parameter id: The identifier for the `ActionTemplate`
|
||||
- Parameter payloadType: The type of the `Payload`
|
||||
*/
|
||||
public init(id: String, payloadType: Payload.Type) {
|
||||
self.id = id
|
||||
self.payloadType = payloadType
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an `AnonymousAction` with the `ActionTemplate`s `id` and the given `payload`.
|
||||
|
||||
- Parameter payload: The payload to create the `AnonymousAction` with
|
||||
*/
|
||||
public func createAction(payload: Payload) -> AnonymousAction<Payload> {
|
||||
return .init(id: id, payload: payload)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an `AnonymousAction` with the `ActionTemplate`s `id` and the given `payload`.
|
||||
|
||||
- Parameter payload: The payload to create the `AnonymousAction` with
|
||||
*/
|
||||
public func callAsFunction(payload: Payload) -> AnonymousAction<Payload> {
|
||||
return createAction(payload: payload)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ActionTemplate where Payload == Void {
|
||||
/**
|
||||
Initializes an `ActionTemplate` with no `Payload`.
|
||||
|
||||
- Parameter id: The identifier for the `ActionTemplate`
|
||||
*/
|
||||
init(id: String) {
|
||||
self.init(id: id, payloadType: Payload.self)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an `AnonymousAction` with the `ActionTemplate`s `id`.
|
||||
*/
|
||||
func createAction() -> AnonymousAction<Payload> {
|
||||
return .init(id: id, payload: ())
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an `AnonymousAction` with the `ActionTemplate`s `id`.
|
||||
*/
|
||||
func callAsFunction() -> AnonymousAction<Payload> {
|
||||
return createAction()
|
||||
}
|
||||
}
|
||||
|
||||
/// An `Action` with an identifier.
|
||||
public protocol IdentifiableAction: Action {
|
||||
/// The identifier
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
/// An `Action` created from an `ActionTemplate`.
|
||||
public struct AnonymousAction<Payload>: IdentifiableAction {
|
||||
/// The identifier for the `AnonymousAction`
|
||||
public let id: String
|
||||
/// The `Payload` for the `AnonymousAction`
|
||||
public let payload: Payload
|
||||
|
||||
/**
|
||||
Check if the `AnonymousAction` was created from a given `ActionTemplate`.
|
||||
|
||||
- Parameter actionTemplate: The `ActionTemplate` to check
|
||||
*/
|
||||
public func wasCreated(from actionTemplate: ActionTemplate<Payload>) -> Bool {
|
||||
return actionTemplate.id == id
|
||||
}
|
||||
}
|
||||
|
||||
extension AnonymousAction: EncodableAction {
|
||||
private var encodablePayload: [String: AnyCodable]? {
|
||||
guard type(of: payload) != Void.self else { return nil }
|
||||
let mirror = Mirror(reflecting: payload)
|
||||
return mirror.children.reduce(into: [String: AnyCodable]()) {
|
||||
guard let key = $1.label else { return }
|
||||
$0[key] = AnyCodable($1.value)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
if payload is Encodable {
|
||||
try container.encode(AnyCodable(payload), forKey: .payload)
|
||||
} else {
|
||||
try container.encodeIfPresent(encodablePayload, forKey: .payload)
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case payload
|
||||
}
|
||||
}
|
||||
55
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Effects.swift
Normal file
55
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Effects.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
#if USE_OPENCOMBINE
|
||||
import OpenCombine
|
||||
#else
|
||||
import Combine
|
||||
#endif
|
||||
|
||||
/**
|
||||
A side effect that happens as a response to a dispatched `Action`.
|
||||
|
||||
An `Effect` can:
|
||||
- give a new `Action` to dispatch (a `dispatchingOne` effect)
|
||||
- give an array of new `Action`s to dispatch (a `dispatchingMultiple` effect)
|
||||
- give nothing (a `nonDispatching` effect)
|
||||
*/
|
||||
public enum Effect<Environment> {
|
||||
/// An `Effect` that publishes an `Action` to dispatch.
|
||||
case dispatchingOne(_ publisher: (AnyPublisher<Action, Never>, Environment) -> AnyPublisher<Action, Never>)
|
||||
/// An `Effect` that publishes multiple `Action`s to dispatch.
|
||||
case dispatchingMultiple(_ publisher: (AnyPublisher<Action, Never>, Environment) -> AnyPublisher<[Action], Never>)
|
||||
/// An `Effect` that handles the action but doesn't publish a new `Action`.
|
||||
case nonDispatching(_ cancellable: (AnyPublisher<Action, Never>, Environment) -> AnyCancellable)
|
||||
}
|
||||
|
||||
/**
|
||||
A collection of `Effect`s.
|
||||
|
||||
The only requirement for the protocol is to specify the type of the `Environment`
|
||||
which should be used in the `Effect`s. The properties have default implementations.
|
||||
*/
|
||||
public protocol Effects {
|
||||
/// The environment set up in the `Store` passed to every `Effect`.
|
||||
associatedtype Environment
|
||||
/**
|
||||
The `Effect`s to register on the `Store`. The default implementation takes all the `Effect`s in the type,
|
||||
|
||||
It is possible to implement the property manually to only enable a subset of `Effect`s to enable.
|
||||
*/
|
||||
var enabledEffects: [Effect<Environment>] { get }
|
||||
/// The identifier for the `Effects`. The default implementation is just the name of the type.
|
||||
static var id: String { get }
|
||||
}
|
||||
|
||||
public extension Effects {
|
||||
var enabledEffects: [Effect<Environment>] {
|
||||
Mirror(reflecting: self).children.compactMap { $0.value as? Effect<Environment> }
|
||||
}
|
||||
|
||||
static var id: String { .init(describing: Self.self) }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
/// A type which intercepts all `Action`s and the `State` changes happening in a `Store`.
|
||||
public protocol Interceptor {
|
||||
/// The type of `State` the `Interceptor` will get.
|
||||
associatedtype State
|
||||
/**
|
||||
The function called when an `Action` is dispatched on a `Store`.
|
||||
|
||||
- Parameter action: The `Action` dispatched
|
||||
- Parameter oldState: The `State` before the `Action` was dispatched
|
||||
- Parameter newState: The `State` after the `Action` was dispatched
|
||||
*/
|
||||
func actionDispatched(action: Action, oldState: State, newState: State)
|
||||
/// The identifier for the `Interceptor`
|
||||
static var id: String { get }
|
||||
}
|
||||
|
||||
public extension Interceptor {
|
||||
static var id: String { .init(describing: self) }
|
||||
}
|
||||
|
||||
/// A type-erased `Interceptor` used to store all `Interceptor`s in an array in the `Store`.
|
||||
internal struct AnyInterceptor<State>: Interceptor {
|
||||
let originalId: String
|
||||
private let _actionDispatched: (Action, State, State) -> Void
|
||||
|
||||
init<I: Interceptor>(_ interceptor: I) where I.State == State {
|
||||
originalId = type(of: interceptor).id
|
||||
_actionDispatched = interceptor.actionDispatched
|
||||
}
|
||||
|
||||
func actionDispatched(action: Action, oldState: State, newState: State) {
|
||||
_actionDispatched(action, oldState, newState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A `Interceptor` to use when debugging. Every `Action`s and `State` change are printed to the console.
|
||||
public class PrintInterceptor<State: Encodable>: Interceptor {
|
||||
private let print: (String) -> Void
|
||||
|
||||
/// Initializes the `PrintInterceptor`.
|
||||
public convenience init() {
|
||||
self.init(print: { Swift.print($0) })
|
||||
}
|
||||
|
||||
internal init(print: @escaping (String) -> Void) {
|
||||
self.print = print
|
||||
}
|
||||
|
||||
/**
|
||||
The function called when an `Action` is dispatched on a `Store`.
|
||||
|
||||
- Parameter action: The `Action` dispatched
|
||||
- Parameter oldState: The `State` before the `Action` was dispatched
|
||||
- Parameter newState: The `State` after the `Action` was dispatched
|
||||
*/
|
||||
public func actionDispatched(action: Action, oldState: State, newState: State) {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
let name = String(describing: type(of: self))
|
||||
|
||||
let actionName = String(describing: type(of: action))
|
||||
var actionLog = "\(name) - action dispatched: \(actionName)"
|
||||
if Mirror(reflecting: action).children.count > 0 {
|
||||
if let encodableAction = action as? EncodableAction,
|
||||
let actionData = encodableAction.encode(with: encoder),
|
||||
let actionJSON = String(data: actionData, encoding: .utf8),
|
||||
actionJSON.replacingOccurrences(of: "\n", with: "") != "{}" {
|
||||
actionLog += ", data: \(actionJSON)"
|
||||
} else {
|
||||
actionLog += "\n⚠️ The payload of the Action has properties but aren't Encodable."
|
||||
actionLog += " Make it Encodable to get them printed."
|
||||
}
|
||||
}
|
||||
self.print(actionLog)
|
||||
|
||||
if let stateData = try? encoder.encode(newState),
|
||||
let newStateJSON = String(data: stateData, encoding: .utf8) {
|
||||
self.print("\(name) - state changed to: \(newStateJSON)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
#if USE_OPENCOMBINE
|
||||
import OpenCombine
|
||||
#else
|
||||
import Combine
|
||||
#endif
|
||||
|
||||
/// Operators for narrowing down `Action`s in Publisher streams.
|
||||
public extension Publisher where Output == Action {
|
||||
/**
|
||||
Only lets `Action`s of a certain type get through the stream.
|
||||
|
||||
actions
|
||||
.ofType(FetchTodosAction.self)
|
||||
.sink(receiveValue: { action in
|
||||
print("This is a FetchTodosAction: \(action)")
|
||||
})
|
||||
|
||||
- Parameter typeToMatch: A type of `Action` to match
|
||||
*/
|
||||
func ofType<T>(_ typeToMatch: T.Type) -> AnyPublisher<T, Self.Failure> {
|
||||
compactMap { $0 as? T }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
#if USE_OPENCOMBINE
|
||||
import OpenCombine
|
||||
#else
|
||||
import Combine
|
||||
#endif
|
||||
|
||||
/// Operators for narrowing down `Action`s in Publisher streams.
|
||||
public extension Publisher where Output == Action {
|
||||
/**
|
||||
Only lets `Action`s created from the given `ActionTemplate`s get through the stream.
|
||||
|
||||
actions
|
||||
.wasCreated(from: fetchTodosActionTemplate)
|
||||
.sink(receiveValue: { action in
|
||||
print("This is a FetchTodosAction: \(action)")
|
||||
})
|
||||
|
||||
- Parameter actionTemplate: An `ActionTemplate` to check
|
||||
*/
|
||||
func wasCreated<Payload>(from actionTemplate: ActionTemplate<Payload>)
|
||||
-> AnyPublisher<AnonymousAction<Payload>, Self.Failure> {
|
||||
ofType(AnonymousAction<Payload>.self)
|
||||
.filter { $0.wasCreated(from: actionTemplate) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
#if USE_OPENCOMBINE
|
||||
import OpenCombine
|
||||
#else
|
||||
import Combine
|
||||
#endif
|
||||
|
||||
/// Operators for narrowing down `Action`s in Publisher streams.
|
||||
public extension Publisher where Output == Action {
|
||||
/**
|
||||
Only lets `AnonymousAction`s with a certain identifier get through the stream.
|
||||
|
||||
actions
|
||||
.withIdentifier("FetchTodosAction")
|
||||
.sink(receiveValue: { action in
|
||||
print("This is an AnonymousAction with the id 'FetchTodosAction': \(action)")
|
||||
})
|
||||
|
||||
- Parameter identifierToMatch: A identifier to match
|
||||
*/
|
||||
func withIdentifier(_ identifierToMatch: String) -> AnyPublisher<Action, Self.Failure> {
|
||||
filter { ($0 as? IdentifiableAction)?.id == identifierToMatch }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
79
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Reducer.swift
Normal file
79
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Reducer.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A type which takes a `State` and `Action` returns a new `State`.
|
||||
public struct Reducer<State> {
|
||||
/// An unique identifier used when registering/unregistering the `Reducer` on the `Store`.
|
||||
public let id: String
|
||||
/// A pure function which takes the a `State` and an `Action` and returns a new `State`.
|
||||
public let reduce: (inout State, Action) -> Void
|
||||
|
||||
/**
|
||||
Creates a `Reducer` from a `reduce` function.
|
||||
|
||||
The `reduce` function is a pure function which takes the a `State` and an `Action` and returns a new `State`.
|
||||
|
||||
- Parameter reduce: The `reduce` function to create a `Reducer` from
|
||||
- Parameter state: The `State` to mutate
|
||||
- Parameter action: The `Action` dispatched
|
||||
*/
|
||||
public init(id: String = UUID().uuidString, reduce: @escaping (_ state: inout State, _ action: Action) -> Void) {
|
||||
self.id = id
|
||||
self.reduce = reduce
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Reducer` from a list of `ReduceOn`s.
|
||||
|
||||
- Parameter reduceOns: The `ReduceOn`s which the created `Reducer` should contain
|
||||
*/
|
||||
public init(id: String = UUID().uuidString, _ reduceOns: ReduceOn<State>...) {
|
||||
self.id = id
|
||||
self.reduce = { state, action in
|
||||
reduceOns.forEach { $0.reduce(&state, action) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A part of a `Reducer` which only gets triggered on certain `Action`s or `ActionTemplate`s.
|
||||
public struct ReduceOn<State> {
|
||||
/// A pure function which takes the a `State` and an `Action` and returns a new `State`.
|
||||
public let reduce: (inout State, Action) -> Void
|
||||
|
||||
/**
|
||||
Creates a `ReduceOn` which only runs `reduce` with actions of the type specificed in `actionType`.
|
||||
|
||||
- Parameter actionType: The type of `Action` to filter on
|
||||
- Parameter reduce: A pure function which takes a `State` and an `Action` and returns a new `State`.
|
||||
- Parameter state: The `State` to mutate
|
||||
- Parameter action: The `Action` dispatched
|
||||
*/
|
||||
public init<A: Action>(_ actionType: A.Type, reduce: @escaping (_ state: inout State, _ action: A) -> Void) {
|
||||
self.reduce = { state, action in
|
||||
guard let action = action as? A else { return }
|
||||
reduce(&state, action)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `ReduceOn` which only runs `reduce` with actions created from the specificed `ActionTemplate`s.
|
||||
|
||||
- Parameter actionTemplates: The `ActionTemplate`s to filter on
|
||||
- Parameter reduce: A pure function which takes a `State` and an `Action` and returns a new `State`.
|
||||
- Parameter state: The `State` to mutate
|
||||
- Parameter action: The `Action` dispatched
|
||||
*/
|
||||
public init<Payload>(_ actionTemplates: ActionTemplate<Payload>...,
|
||||
reduce: @escaping (_ state: inout State, _ action: AnonymousAction<Payload>) -> Void) {
|
||||
self.reduce = { state, action in
|
||||
guard let anonymousAction = action as? AnonymousAction<Payload> else { return }
|
||||
guard actionTemplates.contains(where: { anonymousAction.wasCreated(from: $0) }) else { return }
|
||||
reduce(&state, anonymousAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
340
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Selector.swift
Normal file
340
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Selector.swift
Normal file
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable function_parameter_count
|
||||
|
||||
/// Something which selects a `Value` from the specified `State`.
|
||||
public protocol SelectorProtocol {
|
||||
/// The input for the `Selector`.
|
||||
associatedtype State
|
||||
/// The output of the `Selector`,
|
||||
associatedtype Value
|
||||
|
||||
/**
|
||||
A pure function which takes a `State` and returns a `Value` from it.
|
||||
|
||||
- Parameter state: The `State` to map
|
||||
- Returns: The `Value` mapped from the `State`
|
||||
*/
|
||||
func map(_ state: State) -> Value
|
||||
}
|
||||
|
||||
/**
|
||||
A type which takes a `State` and returns a `Value` from it.
|
||||
|
||||
`Selector`s can be based on other `Selector`s making it possible to select a combined `Value`.
|
||||
*/
|
||||
public class Selector<State, Value>: SelectorProtocol {
|
||||
/// An unique identifier used when overriding the `Selector` on the `MockStore`.
|
||||
public let id = UUID()
|
||||
/// The closue used for the mapping.
|
||||
private let _projector: (State) -> Value
|
||||
/// The latest value for a state hash.
|
||||
internal private(set) var result: (stateHash: UUID, value: Value)?
|
||||
|
||||
/**
|
||||
Creates a `Selector` from a `keyPath`.
|
||||
|
||||
- Parameter keyPath: The `keyPath` to create the `Selector` from
|
||||
*/
|
||||
public convenience init(keyPath: KeyPath<State, Value>) {
|
||||
self.init(projector: { $0[keyPath: keyPath] })
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Selector` from a `projector` closure.
|
||||
|
||||
- Parameter projector: The `projector` closure to create the `Selector` from
|
||||
*/
|
||||
public init(projector: @escaping (State) -> Value) {
|
||||
_projector = projector
|
||||
}
|
||||
|
||||
public func map(_ state: State) -> Value {
|
||||
_projector(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Selector` created from a `Selector`s and a `projector` function.
|
||||
public class Selector1<State, S1, Value>: Selector<State, Value> where
|
||||
S1: SelectorProtocol, S1.State == State {
|
||||
/// A pure function which takes the `Value` from the other `Selector` and returns a new `Value`.
|
||||
public let projector: (S1.Value) -> Value
|
||||
|
||||
/**
|
||||
Creates a `Selector` from a `Selector` and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter projector: The closure to pass the value from the `Selector` to
|
||||
*/
|
||||
public init(_ selector1: S1, _ projector: @escaping (S1.Value) -> Value) {
|
||||
self.projector = projector
|
||||
super.init(projector: { projector(selector1.map($0)) })
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Selector` from a `Selector` and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter projector: The closure to pass the value from the `Selector` to
|
||||
*/
|
||||
public convenience init(_ selector1: S1, keyPath: KeyPath<S1.Value, Value>) {
|
||||
self.init(selector1) { $0[keyPath: keyPath] }
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Selector` created from two `Selector`s and a `projector` function.
|
||||
public class Selector2<State, S1, S2, Value>: Selector<State, Value> where
|
||||
S1: SelectorProtocol, S1.State == State,
|
||||
S2: SelectorProtocol, S2.State == State {
|
||||
/// A pure function which takes the `Value`s from the other `Selector`s and returns a new `Value`.
|
||||
public let projector: (S1.Value, S2.Value) -> Value
|
||||
|
||||
/**
|
||||
Creates a `Selector` from two `Selector`s and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter selector2: The second `Selector`
|
||||
- Parameter projector: The closure to pass the values from the `Selector`s to
|
||||
*/
|
||||
public init(_ selector1: S1,
|
||||
_ selector2: S2,
|
||||
_ projector: @escaping (S1.Value, S2.Value) -> Value) {
|
||||
self.projector = projector
|
||||
super.init(projector: { projector(selector1.map($0), selector2.map($0)) })
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Selector` created from three `Selector`s and a `projector` function.
|
||||
public class Selector3<State, S1, S2, S3, Value>: Selector<State, Value> where
|
||||
S1: SelectorProtocol, S1.State == State,
|
||||
S2: SelectorProtocol, S2.State == State,
|
||||
S3: SelectorProtocol, S3.State == State {
|
||||
/// A pure function which takes the `Value`s from the other `Selector`s and returns a new `Value`.
|
||||
public let projector: (S1.Value, S2.Value, S3.Value) -> Value
|
||||
|
||||
/**
|
||||
Creates a `Selector` from three `Selector`s and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter selector2: The second `Selector`
|
||||
- Parameter selector3: The third `Selector`
|
||||
- Parameter projector: The closure to pass the values from the `Selectors` to
|
||||
*/
|
||||
public init(_ selector1: S1,
|
||||
_ selector2: S2,
|
||||
_ selector3: S3,
|
||||
_ projector: @escaping (S1.Value, S2.Value, S3.Value) -> Value) {
|
||||
self.projector = projector
|
||||
super.init(projector: { projector(selector1.map($0),
|
||||
selector2.map($0),
|
||||
selector3.map($0)) })
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Selector` created from four `Selector`s and a `projector` function.
|
||||
public class Selector4<State, S1, S2, S3, S4, Value>: Selector<State, Value> where
|
||||
S1: SelectorProtocol, S1.State == State,
|
||||
S2: SelectorProtocol, S2.State == State,
|
||||
S3: SelectorProtocol, S3.State == State,
|
||||
S4: SelectorProtocol, S4.State == State {
|
||||
/// A pure function which takes the `Value`s from the other `Selector`s and returns a new `Value`.
|
||||
public let projector: (S1.Value, S2.Value, S3.Value, S4.Value) -> Value
|
||||
|
||||
/**
|
||||
Creates a `Selector` from four `Selector`s and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter selector2: The second `Selector`
|
||||
- Parameter selector3: The third `Selector`
|
||||
- Parameter selector4: The fourth `Selector`
|
||||
- Parameter projector: The closure to pass the values from the `Selectors` to
|
||||
*/
|
||||
public init(_ selector1: S1,
|
||||
_ selector2: S2,
|
||||
_ selector3: S3,
|
||||
_ selector4: S4,
|
||||
_ projector: @escaping (S1.Value, S2.Value, S3.Value, S4.Value) -> Value) {
|
||||
self.projector = projector
|
||||
super.init(projector: { projector(selector1.map($0),
|
||||
selector2.map($0),
|
||||
selector3.map($0),
|
||||
selector4.map($0)) })
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Selector` created from five `Selector`s and a `projector` function.
|
||||
public class Selector5<State, S1, S2, S3, S4, S5, Value>: Selector<State, Value> where
|
||||
S1: SelectorProtocol, S1.State == State,
|
||||
S2: SelectorProtocol, S2.State == State,
|
||||
S3: SelectorProtocol, S3.State == State,
|
||||
S4: SelectorProtocol, S4.State == State,
|
||||
S5: SelectorProtocol, S5.State == State {
|
||||
/// A pure function which takes the `Value`s from the other `Selector`s and returns a new `Value`.
|
||||
public let projector: (S1.Value, S2.Value, S3.Value, S4.Value, S5.Value) -> Value
|
||||
|
||||
/**
|
||||
Creates a `Selector` from five `Selector`s and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter selector2: The second `Selector`
|
||||
- Parameter selector3: The third `Selector`
|
||||
- Parameter selector4: The fourth `Selector`
|
||||
- Parameter selector5: The fifth `Selector`
|
||||
- Parameter projector: The closure to pass the values from the `Selectors` to
|
||||
*/
|
||||
public init(_ selector1: S1,
|
||||
_ selector2: S2,
|
||||
_ selector3: S3,
|
||||
_ selector4: S4,
|
||||
_ selector5: S5,
|
||||
_ projector: @escaping (S1.Value, S2.Value, S3.Value, S4.Value, S5.Value) -> Value) {
|
||||
self.projector = projector
|
||||
super.init(projector: { projector(selector1.map($0),
|
||||
selector2.map($0),
|
||||
selector3.map($0),
|
||||
selector4.map($0),
|
||||
selector5.map($0)) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Creator functions.
|
||||
public extension Selector {
|
||||
/**
|
||||
Creates a `Selector` from a `Selector` and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter projector: The closure to pass the value from the `Selector` to
|
||||
- Returns: A `Selector` from the given `Selector` and the `projector` function
|
||||
*/
|
||||
static func with<S1>(_ selector1: S1,
|
||||
projector: @escaping (S1.Value) -> Value)
|
||||
-> Selector1<State, S1, Value> {
|
||||
.init(selector1, projector)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Selector` from a `Selector` and a `KeyPath`.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter keyPath: The `keyPath` to create the `Selector` from
|
||||
- Parameter keyPath: The `KeyPath` to subscript in the value from the `Selector`
|
||||
- Returns: A `Selector` from the given `Selector` and the `KeyPath`
|
||||
*/
|
||||
static func with<S1>(_ selector1: S1,
|
||||
keyPath: KeyPath<S1.Value, Value>)
|
||||
-> Selector1<State, S1, Value> {
|
||||
.init(selector1, keyPath: keyPath)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Selector` from two `Selector`s and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter selector2: The second `Selector`
|
||||
- Parameter projector: The closure to pass the values from the `Selector`s to
|
||||
- Returns: A `Selector` from the given `Selector`s and the `projector` function
|
||||
*/
|
||||
static func with<S1, S2>(_ selector1: S1,
|
||||
_ selector2: S2,
|
||||
projector: @escaping (S1.Value, S2.Value) -> Value)
|
||||
-> Selector2<State, S1, S2, Value> {
|
||||
.init(selector1, selector2, projector)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Selector` from three `Selector`s and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter selector2: The second `Selector`
|
||||
- Parameter selector3: The third `Selector`
|
||||
- Parameter projector: The closure to pass the values from the `Selectors` to
|
||||
- Returns: A `Selector` from the given `Selector`s and the `projector` function
|
||||
*/
|
||||
static func with<S1, S2, S3>(_ selector1: S1,
|
||||
_ selector2: S2,
|
||||
_ selector3: S3,
|
||||
projector: @escaping (S1.Value, S2.Value, S3.Value) -> Value)
|
||||
-> Selector3<State, S1, S2, S3, Value> {
|
||||
.init(selector1, selector2, selector3, projector)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Selector` from four `Selector`s and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter selector2: The second `Selector`
|
||||
- Parameter selector3: The third `Selector`
|
||||
- Parameter selector4: The fourth `Selector`
|
||||
- Parameter projector: The closure to pass the values from the `Selectors` to
|
||||
- Returns: A `Selector` from the given `Selector`s and the `projector` function
|
||||
*/
|
||||
static func with<S1, S2, S3, S4>(_ selector1: S1,
|
||||
_ selector2: S2,
|
||||
_ selector3: S3,
|
||||
_ selector4: S4,
|
||||
projector: @escaping (S1.Value, S2.Value, S3.Value, S4.Value) -> Value)
|
||||
-> Selector4<State, S1, S2, S3, S4, Value> {
|
||||
.init(selector1, selector2, selector3, selector4, projector)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Selector` from five `Selector`s and a `projector` function.
|
||||
|
||||
- Parameter selector1: The first `Selector`
|
||||
- Parameter selector2: The second `Selector`
|
||||
- Parameter selector3: The third `Selector`
|
||||
- Parameter selector4: The fourth `Selector`
|
||||
- Parameter selector5: The fifth `Selector`
|
||||
- Parameter projector: The closure to pass the values from the `Selectors` to
|
||||
- Returns: A `Selector` from the given `Selector`s and the `projector` function
|
||||
*/
|
||||
static func with<S1, S2, S3, S4, S5>(_ selector1: S1,
|
||||
_ selector2: S2,
|
||||
_ selector3: S3,
|
||||
_ selector4: S4,
|
||||
_ selector5: S5,
|
||||
projector: @escaping (S1.Value, S2.Value, S3.Value,
|
||||
S4.Value, S5.Value) -> Value)
|
||||
-> Selector5<State, S1, S2, S3, S4, S5, Value> {
|
||||
.init(selector1, selector2, selector3, selector4, selector5, projector)
|
||||
}
|
||||
}
|
||||
|
||||
/// Memoization support, where the `Selector` remembers the last result to speed up mapping.
|
||||
internal extension Selector {
|
||||
/**
|
||||
Sets the value and the corresponding `stateHash`.
|
||||
|
||||
- Parameter value: The value to save
|
||||
- Parameter stateHash: The hash of the state the value was selected from
|
||||
*/
|
||||
func setResult(value: Value, forStateHash stateHash: UUID) {
|
||||
result = (stateHash: stateHash, value: value)
|
||||
}
|
||||
|
||||
/**
|
||||
Selects the `Value` from the `State` based on the subclass's `map` function and saves the result.
|
||||
|
||||
- If a value is already saved and the saved state hash matches the passed, the saved value is returned.
|
||||
- If a value is already saved but the saved state hash doesn't match the passed
|
||||
a new value is selected and saved along with the passed state hash
|
||||
|
||||
- Parameter state: The `State` to select from
|
||||
- Parameter stateHash: The hash of the `State` to select from
|
||||
- Returns: The `Value` mapped with the `projector`
|
||||
*/
|
||||
func map(_ state: State, stateHash: UUID) -> Value {
|
||||
if let result = result, result.stateHash == stateHash {
|
||||
return result.value
|
||||
}
|
||||
let value = map(state)
|
||||
setResult(value: value, forStateHash: stateHash)
|
||||
return value
|
||||
}
|
||||
}
|
||||
296
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Store.swift
Normal file
296
TUIKit/TUIRoomKit/Source/Common/Basic/Fluxor/Store.swift
Normal file
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
#if USE_OPENCOMBINE
|
||||
import OpenCombine
|
||||
import OpenCombineDispatch
|
||||
public typealias ObservableObjectCompat = OpenCombine.ObservableObject
|
||||
public typealias PublishedCompat = OpenCombine.Published
|
||||
#else
|
||||
import Combine
|
||||
public typealias ObservableObjectCompat = Combine.ObservableObject
|
||||
public typealias PublishedCompat = Combine.Published
|
||||
#endif
|
||||
|
||||
/**
|
||||
The `Store` is a centralized container for a single-source-of-truth `State`.
|
||||
|
||||
A `Store` is configured by registering all the desired `Reducer`s and `Effects`.
|
||||
|
||||
An `Environment` can be set up to enable dependency injection in `Effect`s.
|
||||
|
||||
## Usage
|
||||
To update the `State` callers dispatch `Action`s on the `Store`.
|
||||
|
||||
## Selecting
|
||||
To select a value in the `State` the callers can either use a `Selector` or a key path.
|
||||
It is possible to get a `Publisher` for the value or just to select the current value.
|
||||
|
||||
## Interceptors
|
||||
It is possible to intercept all `Action`s and `State` changes by registering an `Interceptor`.
|
||||
*/
|
||||
open class Store<State, Environment>: ObservableObjectCompat {
|
||||
/// The state of the `Store`. It can only be modified by the registered `Reducer`s when `Action`s are dispatched.
|
||||
@PublishedCompat public private(set) var state: State
|
||||
/// The environment passed to the `Effects`. The `Environment` can contain services and other dependencies.
|
||||
public let environment: Environment
|
||||
internal private(set) var stateHash = UUID()
|
||||
private let actions = PassthroughSubject<Action, Never>()
|
||||
private var reducers = [KeyedReducer<State>]()
|
||||
private var effects = [String: [AnyCancellable]]()
|
||||
private var interceptors = [AnyInterceptor<State>]()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
Initializes the `Store` with an initial `State`, an `Environment` and eventually `Reducer`s.
|
||||
|
||||
- Parameter initialState: The initial `State` for the `Store`
|
||||
- Parameter environment: The `Environment` to pass to `Effect`s
|
||||
- Parameter reducers: The `Reducer`s to register
|
||||
*/
|
||||
public init(initialState: State, environment: Environment, reducers: [Reducer<State>] = []) {
|
||||
state = initialState
|
||||
self.environment = environment
|
||||
reducers.forEach(register(reducer:))
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit \(type(of: self))")
|
||||
}
|
||||
|
||||
// MARK: - Dispatching
|
||||
|
||||
/**
|
||||
Dispatches an `Action` and creates a new `State` by running the current `State` and the `Action`
|
||||
through all registered `Reducer`s.
|
||||
|
||||
After the `State` is set, all registered `Interceptor`s are notified of the change.
|
||||
Lastly the `Action` is dispatched to all registered `Effect`s.
|
||||
|
||||
- Parameter action: The `Action` to dispatch
|
||||
*/
|
||||
public func dispatch(action: Action) {
|
||||
let oldState = state
|
||||
var newState = oldState
|
||||
reducers.forEach { $0.reduce(&newState, action) }
|
||||
stateHash = UUID()
|
||||
state = newState
|
||||
interceptors.forEach { $0.actionDispatched(action: action, oldState: oldState, newState: newState) }
|
||||
actions.send(action)
|
||||
}
|
||||
|
||||
// MARK: - Reducers
|
||||
|
||||
/**
|
||||
Registers the given `Reducer`. The `Reducer` will be run for all subsequent actions.
|
||||
|
||||
- Parameter reducer: The `Reducer` to register
|
||||
*/
|
||||
public func register(reducer: Reducer<State>) {
|
||||
register(reducer: reducer, for: \.self)
|
||||
}
|
||||
|
||||
/**
|
||||
Registers the given `Reducer` for a slice of the `State`. The `Reducer` will be run for all subsequent actions.
|
||||
|
||||
- Parameter reducer: The `Reducer` to register
|
||||
- Parameter keyPath: The `KeyPath` for which the `Reducer` should be run
|
||||
*/
|
||||
public func register<Substate>(reducer: Reducer<Substate>, for keyPath: WritableKeyPath<State, Substate>) {
|
||||
reducers.append(KeyedReducer(keyPath: keyPath, reducer: reducer))
|
||||
}
|
||||
|
||||
/**
|
||||
Unregisters the given `Reducer`. The `Reducer` will no longer be run when `Action`s are dispatched.
|
||||
|
||||
- Parameter reducer: The `Reducer` to unregister
|
||||
*/
|
||||
public func unregister<SomeState>(reducer: Reducer<SomeState>) {
|
||||
reducers.removeAll { $0.id == reducer.id }
|
||||
}
|
||||
|
||||
// MARK: - Effects
|
||||
|
||||
/**
|
||||
Registers the given `Effects`. The `Effects` will receive all subsequent actions.
|
||||
|
||||
- Parameter effects: The `Effects` to register
|
||||
*/
|
||||
public func register<E: Effects>(effects: E) where E.Environment == Environment {
|
||||
self.effects[E.id] = createCancellables(for: effects.enabledEffects)
|
||||
}
|
||||
|
||||
/**
|
||||
Registers the given `Effect`s. The `Effect`s will receive all subsequent actions.
|
||||
|
||||
- Parameter effects: The array of `Effect`s to register
|
||||
- Parameter id: The identifier for the `Effect`s. Only used to enable unregistering the `Effect`s later
|
||||
*/
|
||||
public func register(effects: [Effect<Environment>], id: String = "*") {
|
||||
self.effects[id] = createCancellables(for: effects)
|
||||
}
|
||||
|
||||
/**
|
||||
Registers the given `Effect`. The `Effect` will receive all subsequent actions.
|
||||
|
||||
Only `Effect`s registered from a type conforming to `Effects` can be unregistered.
|
||||
|
||||
- Parameter effect: The `Effect` to register
|
||||
- Parameter id: The identifier for the `Effect`. Only used to enable unregistering the `Effect` later
|
||||
*/
|
||||
public func register(effect: Effect<Environment>, id: String = "*") {
|
||||
effects[id] = (effects[id] ?? []) + [createCancellable(for: effect)]
|
||||
}
|
||||
|
||||
/**
|
||||
Unregisters the given `Effects`. The `Effects` will no longer receive any actions.
|
||||
|
||||
- Parameter effects: The `Effects` to register
|
||||
*/
|
||||
public func unregisterEffects<E: Effects>(ofType effects: E.Type) where E.Environment == Environment {
|
||||
self.effects.removeValue(forKey: effects.id) // An AnyCancellable instance calls cancel() when deinitialized
|
||||
}
|
||||
|
||||
/**
|
||||
Unregisters the `Effect`s registered with the id, so they will no longer receive any actions.
|
||||
|
||||
- Parameter id: The identifier used to register the `Effect`s
|
||||
*/
|
||||
public func unregisterEffects(withId id: String) {
|
||||
effects.removeValue(forKey: id) // An AnyCancellable instance calls cancel() when deinitialized
|
||||
}
|
||||
|
||||
// MARK: - Interceptors
|
||||
|
||||
/**
|
||||
Registers the given `Interceptor`. The `Interceptor` will receive all subsequent `Action`s and state changes.
|
||||
|
||||
- Parameter interceptor: The `Interceptor` to register
|
||||
*/
|
||||
public func register<I: Interceptor>(interceptor: I) where I.State == State {
|
||||
interceptors.append(AnyInterceptor(interceptor))
|
||||
}
|
||||
|
||||
/**
|
||||
Unregisters all registered `Interceptor`s of the given type.
|
||||
The `Interceptor`s will no longer receive any `Action`s or state changes.
|
||||
|
||||
- Parameter interceptor: The type of`Interceptor` to unregister
|
||||
*/
|
||||
|
||||
public func unregisterInterceptors<I: Interceptor>(ofType interceptor: I.Type) where I.State == State {
|
||||
interceptors.removeAll { $0.originalId == interceptor.id }
|
||||
}
|
||||
|
||||
// MARK: - Selecting
|
||||
|
||||
/**
|
||||
Creates a `Publisher` for a `Selector`.
|
||||
|
||||
- Parameter selector: The `Selector` to use when getting the value in the `State`
|
||||
- Returns: A `Publisher` for the `Value` in the `State`
|
||||
*/
|
||||
open func select<Value>(_ selector: Selector<State, Value>) -> AnyPublisher<Value, Never> {
|
||||
$state.map { selector.map($0, stateHash: self.stateHash) }.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the current value in the `State` for a `Selector`.
|
||||
|
||||
- Parameter selector: The `Selector` to use when getting the value in the `State`
|
||||
- Returns: The current `Value` in the `State`
|
||||
*/
|
||||
open func selectCurrent<Value>(_ selector: Selector<State, Value>) -> Value {
|
||||
selector.map(state, stateHash: stateHash)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Void Environment
|
||||
|
||||
public extension Store where Environment == Void {
|
||||
/**
|
||||
Initializes the `Store` with an initial `State` and eventually `Reducer`s.
|
||||
|
||||
Using this initializer will give all `Effects` a `Void` environment.
|
||||
|
||||
- Parameter initialState: The initial `State` for the `Store`
|
||||
- Parameter reducers: The `Reducer`s to register
|
||||
*/
|
||||
convenience init(initialState: State, reducers: [Reducer<State>] = []) {
|
||||
self.init(initialState: initialState, environment: (), reducers: reducers)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscriptions
|
||||
|
||||
extension Store: Subscriber {
|
||||
public typealias Input = Action
|
||||
public typealias Failure = Never
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
public func receive(_ input: Action) -> Subscribers.Demand {
|
||||
dispatch(action: input)
|
||||
return .unlimited
|
||||
}
|
||||
|
||||
public func receive(completion _: Subscribers.Completion<Never>) {}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension Store {
|
||||
/**
|
||||
Creates `Cancellable`s for the given `Effect`s.
|
||||
|
||||
- Parameter effects: The `Effect`s to create `Cancellable`s for
|
||||
- Returns: The `Cancellable`s for the given `Effect`s
|
||||
*/
|
||||
func createCancellables(for effects: [Effect<Environment>]) -> [AnyCancellable] {
|
||||
effects.map(createCancellable(for:))
|
||||
}
|
||||
|
||||
/**
|
||||
Creates `Cancellable` for the given `Effect`.
|
||||
|
||||
- Parameter effect: The `Effect` to create `Cancellable` for
|
||||
- Returns: The `Cancellable` for the given `Effect`
|
||||
*/
|
||||
func createCancellable(for effect: Effect<Environment>) -> AnyCancellable {
|
||||
switch effect {
|
||||
case let .dispatchingOne(effectCreator):
|
||||
return effectCreator(actions.eraseToAnyPublisher(), environment)
|
||||
.receive(on: DispatchQueue.mainQueue)
|
||||
.sink(receiveValue: dispatch(action:))
|
||||
case let .dispatchingMultiple(effectCreator):
|
||||
return effectCreator(actions.eraseToAnyPublisher(), environment)
|
||||
.receive(on: DispatchQueue.mainQueue)
|
||||
.sink { $0.forEach(self.dispatch(action:)) }
|
||||
case let .nonDispatching(effectCreator):
|
||||
return effectCreator(actions.eraseToAnyPublisher(), environment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for a `Reducer` for a specific `KeyPath`.
|
||||
private struct KeyedReducer<State> {
|
||||
let id: String
|
||||
let reduce: (inout State, Action) -> Void
|
||||
|
||||
init<Substate>(keyPath: WritableKeyPath<State, Substate>, reducer: Reducer<Substate>) {
|
||||
id = reducer.id
|
||||
reduce = { state, action in
|
||||
var substate = state[keyPath: keyPath]
|
||||
reducer.reduce(&substate, action)
|
||||
state[keyPath: keyPath] = substate
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2020
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - SwiftUI bindings
|
||||
|
||||
public extension Store {
|
||||
/**
|
||||
Creates a `Binding` from the given `Selector` and `ActionTemplate`.
|
||||
|
||||
When the `wrappedValue` is updated an `Action`, created from the `ActionTemplate`, is dispatched on the `Store`.
|
||||
|
||||
- Parameter selector: The `Selector`s to use for getting the current value
|
||||
- Parameter actionTemplate: The `ActionTemplate` to use for dispatching an `Action` when the value changes
|
||||
- Returns: A `Binding` based on the given `Selector` and `ActionTemplate`
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
func binding<Value>(get selector: Selector<State, Value>,
|
||||
send actionTemplate: ActionTemplate<Value>) -> Binding<Value> {
|
||||
.init(get: { self.selectCurrent(selector) },
|
||||
set: { self.dispatch(action: actionTemplate.createAction(payload: $0)) })
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Binding` from the given `Selector` and `ActionTemplate`s for enabling and disabling the value.
|
||||
|
||||
When the `wrappedValue` is enabled/disabled, an `Action`, created from one of the `ActionTemplate`s,
|
||||
is dispatched on the `Store`.
|
||||
|
||||
- Parameter selector: The `Selector`s to use for getting the current value
|
||||
- Parameter enableActionTemplate: The `ActionTemplate` to use for dispatching an `Action`
|
||||
when the value should be enabled
|
||||
- Parameter disableActionTemplate: The `ActionTemplate` to use for dispatching an `Action`
|
||||
when the value should be disabled
|
||||
- Returns: A `Binding` based on the given `Selector` and `ActionTemplate`s
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
func binding(get selector: Selector<State, Bool>,
|
||||
enable enableActionTemplate: ActionTemplate<Void>,
|
||||
disable disableActionTemplate: ActionTemplate<Void>)
|
||||
-> Binding<Bool> {
|
||||
return .init(get: { self.selectCurrent(selector) },
|
||||
set: { self.dispatch(action: ($0 ? enableActionTemplate : disableActionTemplate)()) })
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `Binding` from the given `Selector` and closure.
|
||||
|
||||
When the `wrappedValue` is updated an `Action` (returned by the closure), is dispatched on the `Store`.
|
||||
|
||||
- Parameter selector: The `Selector`s to use for getting the current value
|
||||
- Parameter action: A closure which returns an `Action` to be dispatched when the value changes
|
||||
- Parameter value: The value used to decide which `Action` to be dispatched.
|
||||
- Returns: A `Binding` based on the given `Selector` and closure
|
||||
*/
|
||||
@available(iOS 13.0, *)
|
||||
func binding<Value>(get selector: Selector<State, Value>,
|
||||
send action: @escaping (_ value: Value) -> Action)
|
||||
-> Binding<Value> {
|
||||
return .init(get: { self.selectCurrent(selector) },
|
||||
set: { self.dispatch(action: action($0)) })
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Fluxor
|
||||
* Copyright (c) Morten Bjerg Gregersen 2021
|
||||
* MIT license, see LICENSE file for details
|
||||
*/
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
|
||||
#if USE_OPENCOMBINE
|
||||
import OpenCombine
|
||||
#else
|
||||
import Combine
|
||||
#endif
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
A property wrapper for observing a value in the `Store`.
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DrawView: View {
|
||||
@StoreValue(Current.store, Selectors.canClear) private var canClear: Bool
|
||||
|
||||
var body: some View {
|
||||
Button(action: { ... }, label: { Text("Clear") })
|
||||
.disabled(!canClear)
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@propertyWrapper public struct StoreValue<State, Value> where Value: Equatable {
|
||||
/// The current value in the `Store`
|
||||
public var wrappedValue: Value { selectCurrent() }
|
||||
/// A `Publisher` for the selecterd value in the `Store`
|
||||
public var projectedValue: AnyPublisher<Value, Never>
|
||||
/// A closure for selecting the current value in the `Store`
|
||||
private let selectCurrent: () -> Value
|
||||
|
||||
/**
|
||||
Initializes the `StoreValue` property wrapper with a `Store` and a `Selector`.
|
||||
|
||||
- Parameter store: The `Store` to select the value from
|
||||
- Parameter selector: The `Selector` to use for selecting the value
|
||||
*/
|
||||
public init<Environment>(_ store: Store<State, Environment>, _ selector: Selector<State, Value>) {
|
||||
projectedValue = store.select(selector)
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
selectCurrent = { store.selectCurrent(selector) }
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user