skip to Main Content

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


  1. 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, Never>?
    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
    
                // wait 2 seconds before getting the next URL
                try await Task.sleep(for: .seconds(2))
            } catch is CancellationError {
                break
            } catch {
                // handle network errors...
            }
            if Task.isCancelled {
                break
            }
        }
    }
    

    Lastly, call task?.cancel() in deinit.

    Login or Signup to reply.
  2. 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:

    /// Asynchronous Operation base class
    ///
    /// This class performs all of the necessary KVC notifications of `isFinished` and
    /// `isExecuting` for a concurrent `Operation` subclass. So, to develope
    /// a concurrent `Operation` subclass, you instead subclass this class which:
    ///
    /// - must override `main()` with the tasks that initiate the asynchronous task;
    ///
    /// - must call `completeOperation()` function when the asynchronous task is done;
    ///
    /// - optionally, periodically check `self.cancelled` status, performing any clean-up
    ///   necessary and then ensuring that `completeOperation()` is called; or
    ///   override `cancel` method, calling `super.cancel()` and then cleaning-up
    ///   and ensuring `completeOperation()` is called.
    
    public class AsynchronousOperation: Operation {
        override public var isAsynchronous: Bool { true }
    
        private let stateLock = NSLock()
    
        private var _executing: Bool = false
        override private(set) public var isExecuting: Bool {
            get {
                stateLock.withLock { _executing }
            }
            set {
                willChangeValue(forKey: #keyPath(isExecuting))
                stateLock.withLock { _executing = newValue }
                didChangeValue(forKey: #keyPath(isExecuting))
            }
        }
    
        private var _finished: Bool = false
        override private(set) public var isFinished: Bool {
            get {
                stateLock.withLock { _finished }
            }
            set {
                willChangeValue(forKey: #keyPath(isFinished))
                stateLock.withLock { _finished = newValue }
                didChangeValue(forKey: #keyPath(isFinished))
            }
        }
    
        /// Complete the operation
        ///
        /// This will result in the appropriate KVN of isFinished and isExecuting
    
        public func completeOperation() {
            if isExecuting {
                isExecuting = false
            }
    
            if !isFinished {
                isFinished = true
            }
        }
    
        override public func start() {
            if isCancelled {
                isFinished = true
                return
            }
    
            isExecuting = true
    
            main()
        }
    
        override public func main() {
            fatalError("subclasses must override `main`")
        }
    }
    

    Once you have that, making an operation that performs a network request:

    class DataOperation: AsynchronousOperation {
        private var dataTask: URLSessionTask?
        
        init(session: URLSession, request: URLRequest, networkCompletionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
            super.init()
            
            dataTask = session.dataTask(with: request) { data, response, error in
                networkCompletionHandler(data, response, error)
                self.completeOperation()
            }
        }
    
        override func main() {
           dataTask?.resume()
        }
    
        override func cancel() {
            super.cancel()
    
            dataTask?.cancel()
        }
    
        override func completeOperation() {
            dataTask = nil
    
            super.completeOperation()
        }
    }
    

    And you can write one that performs the DataOperation, but delays the completion of the operation for two seconds:

    class DataOperationWithDelay: DataOperation {
        let delay: TimeInterval
        
        init(session: URLSession, request: URLRequest, delay: TimeInterval, networkCompletionHandler: 
             @escaping (Data?, URLResponse?, Error?) -> Void) {
            self.delay = delay
            super.init(session: session, request: request, networkCompletionHandler: networkCompletionHandler)
        }
        
        override func completeOperation() {
            DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
                super.completeOperation()
            }
        }
    }
    

    Then you can add operations to your queue:

    let queue = OperationQueue()
    queue.maxConcurrentOperationCount = 1
    
    func start(request: URLRequest, completion: (() -> Void)?) {
        let operation = DataOperationWithDelay(session: .shared, request: request, delay: 2) { data, response, error in
            // ... do whatever you want with the response here
            completion?()
        }
        queue.addOperation(operation)
    }
    

    That would ensure that operations that you add to the queue will pause 2 seconds between network requests.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search