如何多投一个正在进行的动作的结果或开始一个新动作
How to multi-cast the results of an in progress Action or start a new one
我有以下情况 - 我正在使用 ReactiveSwift 的 Action
在我的应用程序中触发网络请求。由于对其响应进行的处理,此网络请求可能很昂贵。因此,当调用者尝试应用操作时,我想执行以下操作:
- 确定操作是否已经在进行中
- 如果是,return 观察正在进行的 Action
结果的 SignalProducer
- 如果不是,return 将在启动时应用操作的 SignalProducer
理想情况下,解决方案应该是线程安全的,因为调用者可能会尝试从不同的线程应用 Action
。
现在我已经设法使用 ReactiveSwift 中的缓存示例拼凑出一些有用的东西,但我几乎可以肯定我做错了什么,特别是在我必须重置 MutableProperty
的方式上当 Action
完成时到 nil
。请注意,我还使用静态变量来确保 UseCase
的多个实例无法绕过我的预期行为。此外,我的示例信号 Never
输出,但在现实世界中它们可能:
class UseCase {
private static let sharedAction = Action<Void, Never, AnyError> {
return SignalProducer.empty.delay(10, on: QueueScheduler.main).on(completed: {
print("Done")
UseCase.sharedProducer.value = nil
})
}
private static let sharedProducer = MutableProperty<SignalProducer<Never, AnyError>?>(nil)
func sync() -> SignalProducer<Never, AnyError> {
let result = UseCase.sharedProducer.modify { value -> Result<SignalProducer<Never, AnyError>, NoError> in
if let inProgress = value {
print("Using in progress")
return Result(value: inProgress)
} else {
print("Starting new")
let producer = UseCase.sharedAction.apply().flatMapError { error -> SignalProducer<Never, AnyError> in
switch error {
case .disabled: return SignalProducer.empty
case .producerFailed(let error): return SignalProducer(error: error)
}
}.replayLazily(upTo: 1)
value = producer
return Result(value: producer)
}
}
guard let producer = result.value else {
fatalError("Unexpectedly found nil producer")
}
return producer
}
}
这也可能有点冗长,但至少应该更容易理解。如有任何问题,请随时提出。
注意:我让这个对象自己开始处理,而不是 return 一个 SignalProducer
调用者将开始处理。相反,我添加了一个只读 属性,听众可以在不开始处理的情况下观察。
我试图让我的观察者尽可能被动,从而使他们 "reactive" 多于 "proactive"。这种模式应该适合您的需要,即使它有点不同。
我试着让这个例子包括:
- 单个工作单元的共享结果。
- 错误处理。
- 信号保留的最佳实践。
- 评论中的一般解释,因为很难找到好的教程。
- 对不起,如果我在解释你已经知道的事情。我不确定假设您已经知道多少。
- 模拟处理延迟(在生产代码中删除)。
它远非完美,但应该提供您可以修改和扩展的可靠模式。
struct MyStruct {}
final class MyClass {
// MARK: Shared Singleton
static let shared = MyClass()
// MARK: Initialization
private init() {}
// MARK: Public Stuff
@discardableResult
func getValue() -> Signal<MyStruct, NoError> {
if !self.isGettingValue {
print("Get value")
self.start()
} else {
print("Already getting value.")
}
return self.latestValue
.signal
.skipNil()
}
var latestValue: Property<MyStruct?> {
// By using a read-only property, the listener can:
// 1. Choose to take/ignore the previous value.
// 2. Choose to listen via Signal, SignalProducer, or binding operator '<~'
return Property(self.latestValueProperty)
}
// MARK: Private Stuff
private var latestValueProperty = MutableProperty<MyStruct?>(nil)
private var isGettingValue = false {
didSet { print("isGettingValue: changed from '\(oldValue)' to '\(self.isGettingValue)'") }
}
private func start() {
// Binding with `<~` automatically starts the SignalProducer with the binding target (our property) as its single listener.
self.latestValueProperty <~ self.newValueProducer()
// For testing, delay signal to mock processing time.
// TODO: Remove in actual implementation.
.delay(5, on: QueueScheduler.main)
// If `self` were not a Singleton, this would be very important.
// Best practice says that you should hold on to signals and producers only as long as you need them.
.take(duringLifetimeOf: self)
// In accordance with best practices, take only as many as you need.
.take(first: 1)
// Track status.
.on(
starting: { [unowned self] in
self.isGettingValue = true
},
event: { [unowned self] event in
switch event {
case .completed, .interrupted:
self.isGettingValue = false
default:
break
}
}
)
}
private func newValueProducer() -> SignalProducer<MyStruct?, NoError> {
return SignalProducer<MyStruct?, AnyError> { observer, lifetime in
// Get Struct with possible error
let val = MyStruct()
// Send and complete the signal.
observer.send(value: val)
observer.sendCompleted()
}
// Don't hold on to errors longer than you need to.
// I like to handle them as close to the source as I can.
.flatMapError { [unowned self] error in
// Deal with error
self.handle(error: error)
// Transform error type from `AnyError` to `NoError`, to signify that the error has been handled.
// `.empty` returns a producer that sends no values and completes immediately.
// If you wanted to, you could return a producer that sends a default or alternative value.
return SignalProducer<MyStruct?, NoError>.empty
}
}
private func handle(error: AnyError) {
}
}
测试
// Test 1: Start processing and observe the results.
MyClass.shared
.getValue()
.take(first: 1)
.observeValues { _ in
print("Test 1 value received.")
}
// Test 2: Attempt to start (attempt ignored) and observe the same result from Test 1.
MyClass.shared
.getValue()
.take(first: 1)
.observeValues { _ in
print("Test 2 value received.")
}
// Test 3: Observe Value from Test 1 without attempting to restart.
MyClass.shared
.latestValue
.signal
.skipNil()
.take(first: 1)
.observeValues { _ in
print("Test 3 value received.")
}
// Test 4: Attempt to restart processing and discard signal
MyClass.shared.getValue()
输出:
Get value
isGettingValue: changed from 'false' to 'true'
Already getting value.
Already getting value.
(5 秒后)
Test 1 value received.
Test 2 value received.
Test 3 value received.
isGettingValue: changed from 'true' to 'false'
我有以下情况 - 我正在使用 ReactiveSwift 的 Action
在我的应用程序中触发网络请求。由于对其响应进行的处理,此网络请求可能很昂贵。因此,当调用者尝试应用操作时,我想执行以下操作:
- 确定操作是否已经在进行中
- 如果是,return 观察正在进行的 Action 结果的 SignalProducer
- 如果不是,return 将在启动时应用操作的 SignalProducer
理想情况下,解决方案应该是线程安全的,因为调用者可能会尝试从不同的线程应用 Action
。
现在我已经设法使用 ReactiveSwift 中的缓存示例拼凑出一些有用的东西,但我几乎可以肯定我做错了什么,特别是在我必须重置 MutableProperty
的方式上当 Action
完成时到 nil
。请注意,我还使用静态变量来确保 UseCase
的多个实例无法绕过我的预期行为。此外,我的示例信号 Never
输出,但在现实世界中它们可能:
class UseCase {
private static let sharedAction = Action<Void, Never, AnyError> {
return SignalProducer.empty.delay(10, on: QueueScheduler.main).on(completed: {
print("Done")
UseCase.sharedProducer.value = nil
})
}
private static let sharedProducer = MutableProperty<SignalProducer<Never, AnyError>?>(nil)
func sync() -> SignalProducer<Never, AnyError> {
let result = UseCase.sharedProducer.modify { value -> Result<SignalProducer<Never, AnyError>, NoError> in
if let inProgress = value {
print("Using in progress")
return Result(value: inProgress)
} else {
print("Starting new")
let producer = UseCase.sharedAction.apply().flatMapError { error -> SignalProducer<Never, AnyError> in
switch error {
case .disabled: return SignalProducer.empty
case .producerFailed(let error): return SignalProducer(error: error)
}
}.replayLazily(upTo: 1)
value = producer
return Result(value: producer)
}
}
guard let producer = result.value else {
fatalError("Unexpectedly found nil producer")
}
return producer
}
}
这也可能有点冗长,但至少应该更容易理解。如有任何问题,请随时提出。
注意:我让这个对象自己开始处理,而不是 return 一个 SignalProducer
调用者将开始处理。相反,我添加了一个只读 属性,听众可以在不开始处理的情况下观察。
我试图让我的观察者尽可能被动,从而使他们 "reactive" 多于 "proactive"。这种模式应该适合您的需要,即使它有点不同。
我试着让这个例子包括:
- 单个工作单元的共享结果。
- 错误处理。
- 信号保留的最佳实践。
- 评论中的一般解释,因为很难找到好的教程。
- 对不起,如果我在解释你已经知道的事情。我不确定假设您已经知道多少。
- 模拟处理延迟(在生产代码中删除)。
它远非完美,但应该提供您可以修改和扩展的可靠模式。
struct MyStruct {}
final class MyClass {
// MARK: Shared Singleton
static let shared = MyClass()
// MARK: Initialization
private init() {}
// MARK: Public Stuff
@discardableResult
func getValue() -> Signal<MyStruct, NoError> {
if !self.isGettingValue {
print("Get value")
self.start()
} else {
print("Already getting value.")
}
return self.latestValue
.signal
.skipNil()
}
var latestValue: Property<MyStruct?> {
// By using a read-only property, the listener can:
// 1. Choose to take/ignore the previous value.
// 2. Choose to listen via Signal, SignalProducer, or binding operator '<~'
return Property(self.latestValueProperty)
}
// MARK: Private Stuff
private var latestValueProperty = MutableProperty<MyStruct?>(nil)
private var isGettingValue = false {
didSet { print("isGettingValue: changed from '\(oldValue)' to '\(self.isGettingValue)'") }
}
private func start() {
// Binding with `<~` automatically starts the SignalProducer with the binding target (our property) as its single listener.
self.latestValueProperty <~ self.newValueProducer()
// For testing, delay signal to mock processing time.
// TODO: Remove in actual implementation.
.delay(5, on: QueueScheduler.main)
// If `self` were not a Singleton, this would be very important.
// Best practice says that you should hold on to signals and producers only as long as you need them.
.take(duringLifetimeOf: self)
// In accordance with best practices, take only as many as you need.
.take(first: 1)
// Track status.
.on(
starting: { [unowned self] in
self.isGettingValue = true
},
event: { [unowned self] event in
switch event {
case .completed, .interrupted:
self.isGettingValue = false
default:
break
}
}
)
}
private func newValueProducer() -> SignalProducer<MyStruct?, NoError> {
return SignalProducer<MyStruct?, AnyError> { observer, lifetime in
// Get Struct with possible error
let val = MyStruct()
// Send and complete the signal.
observer.send(value: val)
observer.sendCompleted()
}
// Don't hold on to errors longer than you need to.
// I like to handle them as close to the source as I can.
.flatMapError { [unowned self] error in
// Deal with error
self.handle(error: error)
// Transform error type from `AnyError` to `NoError`, to signify that the error has been handled.
// `.empty` returns a producer that sends no values and completes immediately.
// If you wanted to, you could return a producer that sends a default or alternative value.
return SignalProducer<MyStruct?, NoError>.empty
}
}
private func handle(error: AnyError) {
}
}
测试
// Test 1: Start processing and observe the results.
MyClass.shared
.getValue()
.take(first: 1)
.observeValues { _ in
print("Test 1 value received.")
}
// Test 2: Attempt to start (attempt ignored) and observe the same result from Test 1.
MyClass.shared
.getValue()
.take(first: 1)
.observeValues { _ in
print("Test 2 value received.")
}
// Test 3: Observe Value from Test 1 without attempting to restart.
MyClass.shared
.latestValue
.signal
.skipNil()
.take(first: 1)
.observeValues { _ in
print("Test 3 value received.")
}
// Test 4: Attempt to restart processing and discard signal
MyClass.shared.getValue()
输出:
Get value
isGettingValue: changed from 'false' to 'true'
Already getting value.
Already getting value.
(5 秒后)
Test 1 value received.
Test 2 value received.
Test 3 value received.
isGettingValue: changed from 'true' to 'false'