Search code examples
swiftuitableviewnsoperationqueuensoperation

How to reload UITableView when OperationsQueue is done with all operations


I am trying to update the cells in a UITableView by calling .reloadData() but it reloads the data before the process finishes therefore not really updating anything since the array that it gets data from is still empty. The way I have the code structured is that it makes a bunch of API calls that are each a Swift Operation. A OperationQueue executes these operations. What needs to happen now is when all the tasks are completed it such call self.tableView.reloadData() . I've tried putting it in a operation's completion block however it is not the main thread and therefore does not execute.

Any help would be appreciated.

EDIT: I've tried changing the thread of the OperationQueue to the main thread using OperationQueue.main and adding a final operation that is run after all other operations are done that contains self.tableView.reloadData(). Although this allows me to reload the UITable it causes the UI to lag. (I have a pull to refresh in the UITableView in which makes the main thread use obvious.)

Sample Code:

let operation1 = BlockOperation {
//Gets Data from server & waits until task is complete before ending
}

let operation2 = BlockOperation {
//Parse JSON
}

let updateOperation = BlockOperation {
    self.tableView.reloadData()
    //Throws a "UITableView.reloadData() must be used from main thread only"
}

operation2.addDependency(operation1)
updateOperation.addDependency(operation2)

let queue = OperationQueue()
queue.addOperation(operation1)
queue.addOperation(operation2)
queue.addOperation(completionOperation)

Solution

  • There are two possible approaches:

    1. Create a new operation that you want to execute when all the others are done, which I'll call the "completion operation". Then every time you create one of the existing individual operations, add that operation as a dependency to your "completion operation". Then, when you're done adding all of your individual operations (and adding each as a dependency to your "completion operation), then add your completion operation to the main queue. It won't fire until all of the other, individual operations are finished.

      For example:

      let queue = OperationQueue()
      queue.maxConcurrentOperationCount = 4
      
      let completionOperation = BlockOperation {
          print("done")
      }
      
      for _ in 0 ..< 10 {
          let operation = ...
          completionOperation.addDependency(operation)
          queue.addOperation(operation)
      }
      
      OperationQueue.main.addOperation(completionOperation)
      

      Note, this assumes that you've created your operations carefully so that the operations don't complete until the task inside the individual operations are done. Notably, if your operation is starting a task that is, itself, an asynchronous task (e.g. a network request), make sure that you define the Operation subclass to be an "asynchronous" operation (isAsynchronous returns true) and make sure that it does the isFinished/isExecuting KVO correctly upon completion.

    2. The other approach is to use a dispatch group. Every time you create one of your individual operations, enter the group (not in the operation itself, but as you create those operations and add them to your operation queue). Then, as the last task in your individual operations, leave the group. Then, when you're done adding all of your individual operations, you can make a dispatch group notify, which you can schedule on the main queue. Then, when all of the individual operations, your dispatch group notify block will fire.

      For example:

      let queue = OperationQueue()
      queue.maxConcurrentOperationCount = 4
      
      let group = DispatchGroup()
      
      for _ in 0 ..< 10 {
          group.enter()
          let operation = BlockOperation {
              ...
              group.leave()
          }
          queue.addOperation(operation)
      }
      
      group.notify(queue: .main) {
          print("done")
      }
      

    I'd lean towards option 1 (staying within the operation queue paradigm), but both approaches would work.