This repository has been archived by the owner on May 12, 2020. It is now read-only.
/
FeedbackLoop.swift
126 lines (103 loc) · 3.66 KB
/
FeedbackLoop.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import Combine
import Dispatch
import Foundation
open class FeedbackLoop<State, Event, Action>: ViewModel {
public enum Input {
/// A feedback has emitted an event.
case event(Event)
/// An API consumer has triggered an action.
case action(Action)
/// A publicly writable property of the state has been updated by a value
/// supplied by an API consumer.
case updated(PartialKeyPath<State>)
}
public struct Output {
/// The last processed input. `nil` means the system has just been spun up.
public let input: Input?
/// The state after having processed `input`.
public let state: State
public init(state: State, input: Input?) {
self.input = input
self.state = state
}
}
@StatePublished public var state: State
public let didChange: PassthroughSubject<Void, Never>
private let outputSubject: CurrentValueSubject<Output, Never>
private let reduce: (inout State, Input) -> Void
// NOTE: Beyond initialization, all inputs must be processed on `queue`.
private let queue: DispatchQueue
private let specificKey = DispatchSpecificKey<Void>()
public init(
initial: State,
reduce: @escaping (inout State, Input) -> Void,
feedbacks: [Feedback] = [],
usesMainQueue: Bool = true,
qos: DispatchQoS = .default
) {
self.reduce = reduce
self.didChange = PassthroughSubject()
self.outputSubject = CurrentValueSubject(Output(state: initial, input: nil))
self.$state = .init(subject: outputSubject)
self.queue = usesMainQueue
? .main
: DispatchQueue(
label: "FeedbackLoop",
qos: qos,
attributes: [],
autoreleaseFrequency: .inherit,
target: nil
)
queue.setSpecific(key: specificKey, value: ())
_ = Publishers.MergeMany(
feedbacks.map { $0.effects(AnyPublisher(outputSubject)) }
)
.sink { [weak self] event in
guard let self = self else { return }
self.process(.event(event)) { _ in }
}
}
deinit {
outputSubject.send(completion: .finished)
didChange.send(completion: .finished)
queue.setSpecific(key: specificKey, value: nil)
}
public func perform(_ action: Action) {
process(.action(action)) { _ in }
}
public func update<U>(_ value: U, for keyPath: WritableKeyPath<State, U>) {
process(.updated(keyPath)) {
$0[keyPath: keyPath] = value
}
}
private func process(_ input: Input, willReduce: @escaping (inout State) -> Void) {
func execute() {
var state = outputSubject.value.state
willReduce(&state)
reduce(&state, input)
outputSubject.value = Output(state: state, input: input)
didChange.send(())
}
if DispatchQueue.getSpecific(key: specificKey) != nil {
execute()
} else {
queue.async(execute: execute)
}
}
@propertyDelegate
public struct StatePublished {
public var value: State {
_read { yield subject.value.state }
}
public var publisher: AnyPublisher<State, Never> {
return subject.map { $0.state }.eraseToAnyPublisher()
}
private let subject: CurrentValueSubject<Output, Never>
fileprivate init(subject: CurrentValueSubject<Output, Never>) {
self.subject = subject
}
}
}
private func mainThreadAssertion() {
dispatchPrecondition(condition: .onQueue(.main))
}