增加换肤功能

This commit is contained in:
启星
2025-08-14 10:07:49 +08:00
parent f6964c1e89
commit 4f9318d98e
8789 changed files with 978530 additions and 2 deletions

View 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
}
}

View 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) }
}

View File

@@ -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)
}
}

View File

@@ -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)")
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View 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)
}
}
}

View 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
}
}

View 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
}
}
}

View File

@@ -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

View File

@@ -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