Swift 5.5 Task { ... } weak self

When using Task { ... } in Swift 5.5, I'm unclear if retain cycles might be an issue or not. For example:

class ViewController: UIViewController {
   private var rows = [String]()
   private var tableView: UITableView?

   func populateRows() {
      Task {
         rows = (try? await getRowsFromNetwork()) ?? []
         tableView?.reloadData()
      }
   }
}

Will populateRows retain the view controller for the life of the task because it references rows and tableView?

If the view controller goes out of scope, I'd like it to have the freedom to deallocate without populateRows retaining it. This syntax compiles, but I don't know if it makes sense:

class ViewController: UIViewController {
   private var rows = [String]()
   private var tableView: UITableView?

   func populateRows() {
      Task { [weak self] in
         self?.rows = (try? await self?.getRowsFromNetwork()) ?? []
         self?.tableView?.reloadData()
      }
   }
}

I don't mind if the task continues to run. (I'm not worried about canceling it.) I just want to understand if there is a way to avoid retaining the view controller in the task, and if that happens automatically or if something like [weak self] is needed.

Replies

Your assumption is correct, the Task will retain the view controller while it's running. The view controller will only be deallocated once the task finishes. This shouldn't be an issue if the task is guaranteed to finish at some point, which is the case if getRowsFromNetwork is internally using the async/await version of the URLSession APIs.

If you really don't want to wait until the Task is complete, using weak self as you did solves the issue. You could also capture [rows, tableView] instead of self, which would also allow ViewController to be deallocated as soon as it's dismissed.

If you change your mind and decide it's best to cancel the running Task if the view controller is dismissed, you can do so by keeping a reference to the Task, like this:

private var task: Task<Void, Error>?

func populateRows() {
    task = Task { ... }
}

and then cancelling it on deinit:

deinit {
    task?.cancel()
}