Ios 在Swift Combine中以优雅的方式进行组合测试,而不会降低价值和不平衡的发布者
我有两个发布者A和B。它们是不平衡的,因为A将发出3个值,然后完成,B将只发出1个值,然后完成(A实际上可以发出一个可变数字,如果有帮助,B将保持1): B也异步运行,并且可能只在a已经发出其第二个值之后才发出一个值(见上图)。(B也可能只在任何时候发出,包括在A已经完成之后。) 我想发布A值与B值组合的元组:Ios 在Swift Combine中以优雅的方式进行组合测试,而不会降低价值和不平衡的发布者,ios,swift,combine,Ios,Swift,Combine,我有两个发布者A和B。它们是不平衡的,因为A将发出3个值,然后完成,B将只发出1个值,然后完成(A实际上可以发出一个可变数字,如果有帮助,B将保持1): B也异步运行,并且可能只在a已经发出其第二个值之后才发出一个值(见上图)。(B也可能只在任何时候发出,包括在A已经完成之后。) 我想发布A值与B值组合的元组: (1, X) (2, X) (3, X) CombineTest不适合该作业,因为它将跳过A的第一个值,并且只发出(2,X)和(3,X)zip对我不起作用,因为B只发出一个值 我正在寻
(1, X) (2, X) (3, X)
CombineTest
不适合该作业,因为它将跳过A的第一个值,并且只发出(2,X)
和(3,X)
<另一方面,code>zip对我不起作用,因为B只发出一个值
我正在寻找一种优雅的方式来实现这一点。谢谢
编辑并找到解决方案 有点哲学性,但我认为如果你想走
zip
或combineLatest
路线,有一个基本问题。当您等待较慢的发布服务器开始发出值时,您肯定需要某种存储,以便较快的发布服务器缓冲事件
一种解决方案可能是创建一个发布服务器,该发布服务器从a收集事件,直到B发出,然后发出所有收集的事件,并继续发出a给出的内容。这实际上可以通过
let bufferedSubject1 = Publishers.Concatenate(
prefix: Publishers.PrefixUntilOutput(upstream: subject1, other: subject2).collect().flatMap(\.publisher),
suffix: subject1)
PrefixUntilOutput
将收集所有内容,直到B发出(subject2
),然后切换到仅定期传递其输出
但是如果你跑
let cancel = bufferedSubject1.combineLatest(subject2)
.sink(receiveCompletion: { c in
print(c)
}, receiveValue: { v in
print(v)
})
您仍然缺少(1,X)
中的第一个值——这似乎有点像竞争条件:是bufferedSubject1
首先发出所有值,还是subject2首先向CombineTest
提供值
我认为有趣的是,没有任何异步调用,行为似乎是未定义的。如果您运行下面的示例,有时™️ 您将发出所有值。有时您会错过(1,X)
。因为这里没有异步调用,也没有dispatchQueue切换,所以我甚至认为这是一个bug
您可以通过在bufferedSubject1
和combinelatetest
之间提供delay
甚至只提供receive(在:DispatchQueue.main上)
来“脏修复”竞争条件,以便在继续管道之前,我们将控制权交还给DispatchQueue,并让subject2向combinelatetest
发出
然而,我不认为优雅和仍然在寻找一种解决方案,它使用zip
语义,但不必创建相同价值的无限集合(在我看来,这不适合顺序处理和无限需求)
样本:
var subject1 = PassthroughSubject<Int, Never>()
var subject2 = PassthroughSubject<String, Never>()
let bufferedSubject1 = Publishers.Concatenate(prefix: Publishers.PrefixUntilOutput(upstream: subject1, other: subject2).collect().flatMap(\.publisher),
suffix: subject1)
let bufferedSubject2 = Publishers.Concatenate(prefix: Publishers.PrefixUntilOutput(upstream: subject2, other: subject1).collect().flatMap(\.publisher),
suffix: subject2)
let cancel = bufferedSubject1.combineLatest(subject2)
.sink(receiveCompletion: { c in
print(c)
}, receiveValue: { v in
print(v)
})
subject1.send(1)
subject1.send(2)
subject2.send("X")
subject2.send(completion: .finished)
subject1.send(3)
subject1.send(completion: .finished)
var subject1=PassthroughSubject()
var subject2=PassthroughSubject()
让bufferedSubject1=publisher.Concatenate(前缀:publisher.PrefixUntilOutput(上游:subject1,其他:subject2).collect().flatMap(\.publisher),
后缀:subject1)
让bufferedSubject2=publisher.Concatenate(前缀:publisher.PrefixUntilOutput(上游:subject2,其他:subject1).collect().flatMap(\.publisher),
后缀:subject2)
让cancel=bufferedSubject1.CombineTest(subject2)
.sink(接收完成:{c in
印刷品(c)
},receiveValue:{v in
印刷品(五)
})
主题1.发送(1)
主题1.发送(2)
主题2.发送(“X”)
主题2.发送(完成:。已完成)
主题1.发送(3)
subject1.send(完成:。已完成)
编辑
在结结巴巴地讲了这个问题之后,我想知道您的示例中的问题是否与PassthroughSubject
无关:
如果下游没有任何要求,PassthroughSubject将删除值
事实上,使用:
var subject1 = Timer.publish(every: 1, on: .main, in: .default, options: nil)
.autoconnect()
.measureInterval(using: RunLoop.main, options: nil)
.scan(DateInterval()) { res, interval in
.init(start: res.start, duration: res.duration + interval.magnitude)
}
.map(\.duration)
.map { Int($0) }
.eraseToAnyPublisher()
var subject2 = PassthroughSubject<String, Never>()
let bufferedSubject1 = Publishers.Concatenate(prefix: Publishers.PrefixUntilOutput(upstream: subject1, other: subject2).collect().flatMap(\.publisher),
suffix: subject1)
let cancel = bufferedSubject1.combineLatest(subject2)
.sink(receiveCompletion: { c in
print(c)
}, receiveValue: { v in
print(v)
})
subject2.send("X")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
subject2.send("Y")
}
这似乎是人们想要的行为
我不知道这是否是一个优雅的解决方案,但您可以尝试使用: 输出:
(X, 1)
(X, 2)
(X, 3)
(Y, 3)
(Y, 4)
(Y, 5)
(Y, 6)
编辑
在结结巴巴地讲了这个问题之后,我想知道您的示例中的问题是否与PassthroughSubject
无关:
如果下游没有任何要求,PassthroughSubject将删除值
事实上,使用:
var subject1 = Timer.publish(every: 1, on: .main, in: .default, options: nil)
.autoconnect()
.measureInterval(using: RunLoop.main, options: nil)
.scan(DateInterval()) { res, interval in
.init(start: res.start, duration: res.duration + interval.magnitude)
}
.map(\.duration)
.map { Int($0) }
.eraseToAnyPublisher()
var subject2 = PassthroughSubject<String, Never>()
let bufferedSubject1 = Publishers.Concatenate(prefix: Publishers.PrefixUntilOutput(upstream: subject1, other: subject2).collect().flatMap(\.publisher),
suffix: subject1)
let cancel = bufferedSubject1.combineLatest(subject2)
.sink(receiveCompletion: { c in
print(c)
}, receiveValue: { v in
print(v)
})
subject2.send("X")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
subject2.send("Y")
}
这似乎是人们想要的行为
我不知道这是否是一个优雅的解决方案,但您可以尝试使用: 输出:
(X, 1)
(X, 2)
(X, 3)
(Y, 3)
(Y, 4)
(Y, 5)
(Y, 6)
另一方面,zip对我来说不起作用,因为B只发出一个值
正确,所以修正它,使其不正确。在B处启动管道。使用flatmap将其信号转换为该信号序列的发布者,重复。用拉链把它拉上
例如:
import UIKit
import Combine
func delay(_ delay:Double, closure:@escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class ViewController: UIViewController {
var storage = Set<AnyCancellable>()
let s1 = PassthroughSubject<Int,Never>()
let s2 = PassthroughSubject<String,Never>()
override func viewDidLoad() {
super.viewDidLoad()
let p1 = s1
let p2 = s2.flatMap { (val:String) -> AnyPublisher<String,Never> in
let seq = Array(repeating: val, count: 100)
return seq.publisher.eraseToAnyPublisher()
}
p1.zip(p2)
.sink{print($0)}
.store(in: &storage)
delay(1) {
self.s1.send(1)
}
delay(2) {
self.s1.send(2)
}
delay(3) {
self.s1.send(3)
}
delay(2.5) {
self.s2.send("X")
}
}
}
另一方面,zip对我来说不起作用,因为B只发出一个值
正确,所以修正它,使其不正确。在B处启动管道。使用flatmap将其信号转换为该信号序列的发布者,重复。用拉链把它拉上
例如:
import UIKit
import Combine
func delay(_ delay:Double, closure:@escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
class ViewController: UIViewController {
var storage = Set<AnyCancellable>()
let s1 = PassthroughSubject<Int,Never>()
let s2 = PassthroughSubject<String,Never>()
override func viewDidLoad() {
super.viewDidLoad()
let p1 = s1
let p2 = s2.flatMap { (val:String) -> AnyPublisher<String,Never> in
let seq = Array(repeating: val, count: 100)
return seq.publisher.eraseToAnyPublisher()
}
p1.zip(p2)
.sink{print($0)}
.store(in: &storage)
delay(1) {
self.s1.send(1)
}
delay(2) {
self.s1.send(2)
}
delay(3) {
self.s1.send(3)
}
delay(2.5) {
self.s2.send("X")
}
}
}
好的,这是一个有趣的挑战,虽然看起来很简单,但我找不到一个简单优雅的方法 这里有一种工作方法(虽然很难做到优雅),它似乎不受使用
PrefixUntilOutput
/串联
组合的竞争条件的影响
我们的想法是使用combinelatetest
,但是第一个发布者一发出,另一个值就是nil
,这样我们就不会丢失初始值。这里有一个方便的操作符,我称之为combineLatestOptional
:
extension Publisher {
func combineLatestOptional<Other: Publisher>(_ other: Other)
-> AnyPublisher<(Output?, Other.Output?), Failure>
where Other.Failure == Failure {
self.map { Optional.some($0) }.prepend(nil)
.combineLatest(
other.map { Optional.some($0) }.prepend(nil)
)
.dropFirst() // drop the first (nil, nil)
.eraseToAnyPublisher()
}
}
最后一个操作符combineLatestLossless
的实现方式如下:
extension Publisher {
func combineLatestLossless<Other: Publisher>(_ other: Other)
-> AnyPublisher<(Output, Other.Output), Failure>
where Failure == Other.Failure {
self.combineLatestOptional(other)
.scan(State<Output, Other.Output>.initial, { state, tuple in
switch (state, tuple.0, tuple.1) {
case (.initial, let l?, nil): // left emits first value
return .left([l]) // -> collect left values
case (.initial, nil, let r?): // right emits first value
return .right([r]) // -> collect right values
case (.left(let ls), let l?, nil): // left emits another
return .left(ls + [l]) // -> append to left values
case (.right(let rs), nil, let r?): // right emits another
return .right(rs + [r]) // -> append to right values
case (.left(let ls), _, let r?): // right emits after left
return .final(ls, [r]) // -> go to steady-state
case (.right(let rs), let l?, _): // left emits after right
return .final([l], rs) // -> go to steady-state
case (.final, let l?, let r?): // final steady-state
return .final([l], [r]) // -> pass the values as-is
default:
fatalError("shouldn't happen")
}
})
.flatMap { status -> AnyPublisher<(Output, Other.Output), Failure> in
if case .final(let ls, let rs) = status {
return ls.flatMap { l in rs.map { r in (l, r) }}
.publisher
.setFailureType(to: Failure.self)
.eraseToAnyPublisher()
} else {
return Empty().eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}
好的,这是一个有趣的挑战,虽然看起来很简单,但我找不到一个简单优雅的方法 这里有一种工作方法(虽然很难做到优雅),它似乎不受使用
PrefixUntilOutput
/fileprivate enum State<L, R> {
case initial // before any one publisher emitted
case left([L]) // left emitted; right hasn't emitted
case right([R]) // right emitted; left hasn't emitted
case final([L], [R]) // final steady-state
}
extension Publisher {
func combineLatestLossless<Other: Publisher>(_ other: Other)
-> AnyPublisher<(Output, Other.Output), Failure>
where Failure == Other.Failure {
self.combineLatestOptional(other)
.scan(State<Output, Other.Output>.initial, { state, tuple in
switch (state, tuple.0, tuple.1) {
case (.initial, let l?, nil): // left emits first value
return .left([l]) // -> collect left values
case (.initial, nil, let r?): // right emits first value
return .right([r]) // -> collect right values
case (.left(let ls), let l?, nil): // left emits another
return .left(ls + [l]) // -> append to left values
case (.right(let rs), nil, let r?): // right emits another
return .right(rs + [r]) // -> append to right values
case (.left(let ls), _, let r?): // right emits after left
return .final(ls, [r]) // -> go to steady-state
case (.right(let rs), let l?, _): // left emits after right
return .final([l], rs) // -> go to steady-state
case (.final, let l?, let r?): // final steady-state
return .final([l], [r]) // -> pass the values as-is
default:
fatalError("shouldn't happen")
}
})
.flatMap { status -> AnyPublisher<(Output, Other.Output), Failure> in
if case .final(let ls, let rs) = status {
return ls.flatMap { l in rs.map { r in (l, r) }}
.publisher
.setFailureType(to: Failure.self)
.eraseToAnyPublisher()
} else {
return Empty().eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}
let c = pub1.combineLatestLossless(pub2)
.sink { print($0) }