I have created this simple example to show the issue.
I have 4 buttons for upvoting, favouriting, flagging, and hiding. User can tap any button whenever they like and as many times as they want. However, the network request which is sent after the tap must always have a minimum delay of 2 seconds in between. Also, all requests must be sent in order.
For example, suppose a user taps "upvote" 0.1 seconds after tapping "flag". The flagging request should be sent immediately after tapping "flag", and 2 seconds after receiving the response from the flag request, the upvote request should be sent.
The following seems to work but sometimes, it doesn't obey the 2 second delay and sends multiple requests at the same time without any delay in between. I am having a hard time figuring out what's causing this. Is there a better way to do this? Or what's the problem with this?
import UIKit
import SnapKit
extension UIControl {
func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping()->()) {
addAction(UIAction { (action: UIAction) in closure() }, for: controlEvents)
}
}
let networkDelay = TimeInterval(2)
var requestWorkItems = [DispatchWorkItem]()
var lastRequestCompletionTime: Date?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let stack = UIStackView()
stack.axis = .vertical
view.addSubview(stack)
stack.snp.makeConstraints { make in
make.center.equalToSuperview()
}
["UPVOTE","FAVORITE","FLAG","HIDE"].forEach { title in
let button = UIButton()
button.setTitle(title, for: .normal)
button.addAction { [weak self] in
let urlString = "https://example.com/api?action=\(title)"
self?.scheduleNetworkRequest(urlString: urlString)
}
button.titleLabel?.font = UIFont.systemFont(ofSize: 30, weight: .bold)
button.snp.makeConstraints { make in
make.height.equalTo(100)
}
stack.addArrangedSubview(button)
}
}
func scheduleNetworkRequest(urlString : String) {
let workItem = DispatchWorkItem { [weak self] in
self?.sendNetworkRequest(urlString: urlString)
}
requestWorkItems.append(workItem)
if Date().timeIntervalSince(lastRequestCompletionTime ?? .distantPast) > networkDelay {
scheduleNextWork(delay: 0)
}
}
func scheduleNextWork(delay : TimeInterval) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
if let workItem = requestWorkItems.first {
requestWorkItems.removeFirst()
print("Tasks remaining: \(requestWorkItems.count)")
workItem.perform()
}
}
}
func sendNetworkRequest(urlString : String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
if let error = error {
print("Error: \(error)")
} else if let _ = data {
print("Completed: \(urlString) at: \(Date())")
}
lastRequestCompletionTime = Date()
self?.scheduleNextWork(delay: networkDelay)
}
task.resume()
}
}
I would use an AsyncStream
to record the URLs, and use a Task
to consume the stream, waiting 2 seconds in between.
// add these properties to the view controller
var task: Task<Void, Error>?
let (stream, continuation) = AsyncStream.makeStream(of: URL.self)
In the button's action, add a URL to the continuation
:
button.addAction { [weak self] in
self?.continuation.yield(URL(string: "https://example.com/api?action=\(title)")!)
}
and initialise task
in viewDidLoad
like this:
task = Task {
for await url in stream {
do {
print("Sending request to \(url)")
let (data, response) = try await URLSession.shared.data(from: url)
// do something with data & response
try await Task.sleep(for: .seconds(2))
} catch let error as CancellationError {
throw error
} catch {
// handle network errors...
}
}
}
Lastly, call task?.cancel()
in deinit
.