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()
}
}
2
Answers
I would use an
AsyncStream
to record the URLs, and use aTask
to consume the stream, waiting 2 seconds in between.In the button’s action, add a URL to the
continuation
:and initialise
task
inviewDidLoad
like this:Lastly, call
task?.cancel()
indeinit
.Given that you want to support iOS 11, the legacy pattern for managing dependencies between asynchronous operations would be a custom
Operation
subclass.First, we would define a base class for our asynchronous operations:
Once you have that, making an operation that performs a network request:
And you can write one that performs the
DataOperation
, but delays the completion of the operation for two seconds:Then you can add operations to your queue:
That would ensure that operations that you add to the queue will pause 2 seconds between network requests.