例如,有一个按钮,每次用户点击按钮,它都会触发一个任务(如网络请求,动画或IO)。
在此任务完成之前,按钮应忽略任何单击事件,以避免多次运行。
换句话说,我想要一个发布者扩展方法,它删除发布者发出的元素,直到与每个元素关联的异步任务完成。
我实现了Publisher的这个扩展。它好吗?有更好的解决方案吗?
import UIKit
import Combine
class SwiftViewController: UIViewController {
let clickedSubject = PassthroughSubject<(), Never>()
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// clickedSubject.flatMapAndDropUntilLastCompleted { () in
// print("clicked")
// return Future<Int, Never>.init { promise in
// DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// promise(Result.success(Int.random(in: 0...100)))
// }
// }.eraseToAnyPublisher()
// }.sink { data in
// print("finish \(data)")
// }.store(in: &cancellables)
clickedSubject.dropUntilTaskCompleted({ _ in
print("clicked")
try! await Task.sleep(nanoseconds: 1_000_000_000)
return Int.random(in: 0...100)
}).sink { data in
print("finish \(data)")
}.store(in: &cancellables)
let button = UIButton(type: .close)
view.addSubview(button)
button.center = view.center
button.addTarget(self, action: #selector(clicked), for: .touchUpInside)
}
@objc func clicked() {
clickedSubject.send(())
}
}
extension Publisher {
/// Applies a transformation to each element emitted by the publisher, but drops all subsequent elements until the last transformation completes.
/// - Parameter transform: A closure that takes an element emitted by the publisher and returns a new publisher.
/// - Returns: A publisher that emits elements from the last completed transformation.
public func flatMapAndDropUntilLastCompleted<P>(_ transform: @escaping (Self.Output) -> P) -> AnyPublisher<P.Output, P.Failure> where P: Publisher, Self.Failure == P.Failure {
var drop = false
return self.flatMap { output in
if drop {
return Empty<P.Output, P.Failure>().eraseToAnyPublisher()
} else {
drop = true
return transform(output).handleEvents(receiveCompletion: { _ in
drop = false
}).eraseToAnyPublisher()
}
}.eraseToAnyPublisher()
}
/// Drops elements emitted by the publisher until the asynchronous task associated with each element is completed.
/// - Parameter task: A closure that takes an element emitted by the publisher and returns an asynchronous task.
/// - Returns: A publisher that emits the results of the completed tasks.
public func dropUntilTaskCompleted<T>(_ task: @escaping (Self.Output) async -> T) -> AnyPublisher<T, Self.Failure> {
flatMapAndDropUntilLastCompleted { output in
Future { promise in
Task {
let result = await task(output)
promise(.success(result))
}
}
}
}
}
字符串
我提出这个问题是因为有一个共同的用例。
单击按钮从磁盘读取一些数据,然后导航到包含数据的新页面。
由于从磁盘阅读数据是无效的,因此不会立即推送到新页。
在这种情况下,如果用户快速连续地点击按钮,它可能会多次按下。
不仅是从磁盘上阅读数据。只要导航不同步。问题就在这里。
1条答案
按热度按时间erhoui1w1#
还有一种方法可以解决你的问题。你可以限制异步任务每次最多运行一个,而不是控制异步任务的触发器(在你的例子中是按钮点击/单击)。
在网络请求这样的场景中,您希望额外使用
.share()
运算符(Apple doc)。与多个订阅服务器共享上游发布服务器的输出。
字符串
现在,每次您在按钮操作后订阅
publisher
时,如果请求已经发生,则由于.share()
,它将“附加”到已经存在的正在进行的操作。这有额外的好处,因为你从按钮中抽象出来,你可以使用相同的
publisher.share()
示例来控制按钮和任何其他订阅者(例如,其他触发器不一定是按钮),不要对某些特定的网络操作发出多余的请求,只要共享响应在你的逻辑中是有效的。