Access variable from inside a completionHandler

I am using URLSession.shared.dataTask(with:completionHandler:) to access an API which, on certain circumstances, won't respond immediately. I also need to cancel pending requests on certain conditions.

My current implementation uses the completionHandler approach and multiple requests can happen in parallel. This makes it very inconvenient to change the pattern to a delegate.

What I am trying is to store every pending task in a set, and then remove the task from the set once they are done. The problem is accessing the return of the dataTask from inside the completion handler seems very cumbersome, to a point I am not sure this is even correct.

class API {
  private var tasks: Set<URLSessionTask> = []

  func sendRequest(path: String, body: [String: Any], completionHandler: @escaping @Sendable (APIResponse) -> Void) {
    var request = // ... create request

    let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
      // ... handle response

      tasks.remove(task)
    }

    tasks.insert(task)
    task.resume()
  }

  func cancelAllTasks() {
    for task in tasks {
      task.cancel()
      tasks.remove(task)
    }
  }
}

I either get a compiler error Closure captures 'task' before it is declared or a warning 'task' mutated after capture by sendable closure.

Is there a better way of doing this?

Try this:

  var task: URLSessionDataTask!
  task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
      // ... handle response
      tasks.remove(task)
  }
  tasks.insert(task)
  task.resume()

Also, have a look at URLSession's invalidateAndCancel - looks it's doing what you are trying to do. Although: "Calling this method on the session returned by the shared method has no effect" - so you'll need to create your own URLSession and manage its lifetime.

Doing that triggered the warning I mentioned: 'task' mutated after capture by sendable closure

I ended up going with a suggestion from a response in StackOverflow: store the task indexed by the request in a dictionary, and use that to cancel the task.

class API {
  private var requestTasks: [URLRequest: URLSessionDataTask] = [:]

  func sendRequest(path: String, body: [String: Any], completionHandler: @escaping @Sendable (APIResponse) -> Void) {
    var request = // ... create request

    let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
      requestComplete(request: request)
      // ... handle response
    }

    task.resume()
    requestTasks[request] = task
  }

  func cancelAllTasks() {
    for (session, task) in requestTasks {
      task.cancel()
      requestTasks.removeValue(forKey: session)
    }
  }
}

If all you're doing is implementing a cancelAll()method, you don't really need to track them yourself. You can call URLSession.getAllTasks and cancel the tasks there.