Изучение данного блока предполагает предварительное знание синтаксиса языка Swift. Для успешного освоения этого материала, необходимо иметь базовое понимание синтаксиса языка Swift. Это включает в себя знание основных структур данных, операторов, циклов, функций, абстракций и других ключевых элементов языка. Без этих фундаментальных знаний будет сложно понять более сложные концепции и примеры, которые будут рассматриваться в данном блоке.
Мы уже познакомились с определением задач, примерно понимаем чем и как они отличаются, теперь начнем погружаться уже в технические детали и будем на примерах изучать Task.
Для тех, кто знаком с GCD: Не стоит воспринимать создание задачи как аналог DispatchQueue.global().async - это совсем не так. Это не просто перенос работы на бекграунд поток - здесь наследуется контекст и не всегда происходит переход на бекграунд поток, поэтому иногда тяжёлые задачи могут попасть на главный поток и привести к фризам.
В Structured Concurrency eсть 2 способа создать задачу:
/// Создаётся задача верхнего уровня, которая выкидывается из текущей иерархии задач,
/// Ответственность за задачу (отмену, локальное хранилище) будет лежать на разработчике
/// - Parameters:
/// - priority: приоритет задачи, по умолчанию - medium
/// - operation: код, который будет выполняться в таске
Task.detached(priority: TaskPriority?, operation: () async -> _)
Рассмотрим пример: Обработка нажатия на кнопку в контроллере
final class ViewController: UIViewController {
@objc
private func handleTapOnButton(_ sender: UIButton) {
/// Здесь главный поток, на котором мы можем например показать спиннер
showLoadingIndicator()
/// Здесь всё также главной поток и мы начинаем скачивать картинку
fetchImage(by: 1)
}
func fetchImage(by id: Int) {
/// Создание родительской задачи, необходимо для того, чтобы уйти
/// с главного исполнителя на Глобальный, чтобы не перегружать его
let task = Task.detached {
/// Внутри уже будет не главный исполнитель - а другой
}
}
}
/// Создаётся задача, которая укладывается в текущую иерархию задач.
/// Так как задача будет находится в иерархии задач, значит она будет наследовать
/// исполнителя, его контекст, локальное хранилище.
/// - Parameters:
/// - priority: приоритет задачи, по умолчанию - medium
/// - operation: код, который будет выполняться в таске
Task(priority: TaskPriority?, operation: () async -> _)
Рассмотрим аналогичный пример:
final class ViewController: UIViewController {
@objc
private func handleTapOnButton(_ sender: UIButton) {
/// Здесь главный поток, на котором мы можем например показать спиннер
showLoadingIndicator()
/// Здесь всё также главной поток и мы начинаем скачивать картинку
fetchImage(by: 1)
}
func fetchImage(by id: Int) {
/// Создание родительской задачи, необходимо для того, чтобы уйти
/// с главного исполнителя на Глобальный, чтобы не перегружать его
let task = Task {
/// Здесь останется главный исполнитель, потому что задача наследует контекст
}
}
}

Следующая задача предлагается на собеседованиях уровня Junior-Middle.
Есть два экрана. На одном контроллере есть кнопка, которая презентует TaskExampleViewController. В методе viewDidLoad, которого выполняется очень долгая задача. Не дожидаясь завершения задачи, пользователь смахивает экран вниз.
import UIKit
final class TaskExampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
print("TaskExampleViewController", #function)
doHardWork()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("TaskExampleViewController", #function)
}
deinit {
print("TaskExampleViewController", #function)
}
private func doHardWork() {
Task {
var array = [Int]()
(0...100_000_000).forEach {
array.append($0)
}
print(array.count)
}
}
}
После данной задачи возникает вопрос. О каком главном исполнителе идёт речь? У нас нет помеченных функций и нигде явно не указан исполнитель.
Главный исполнитель
Иногда у нас существуют классы, структуры, протоколы - все методы которых должны выполняться на главном потоке, и помечать каждый метод с помощью @MainActor не хочется, если методов много - то это долго. Поэтому мы можем взять и перед сущностью написать @MainActor - что будет означать, что ВСЕ методы класса будут иметь контекст главного исполнителя.
import UIKit
@MainActor
final class MyClass { ... }
@MainActor
struct ScreenState { ... }
@MainActor
protocol ScreenDisplayLogic { ... } /// методы, содержащиеся в данном протоколе - работают с отрисовкой, поэтому им нужен главный поток.
Глобальный исполнитель
Если же мы просто создаём класс - то он по умолчанию имеет @globalActor. Но писать его не имеет никакого смысла, поэтому он скрыт от глаз разработчика Поэтому ещё одним из вариантов решения задачи из собеседования будет создание какого-то сервиса. Опишем логику ниже:
/// Сервис, который будет выполнять сложные задачи
final class HardWorkService {
func doHardWork() {
Task {
var array = [Int]()
(0...100_000_000).forEach {
array.append($0)
}
print(array.count)
}
}
}
final class TaskExampleViewController: UIViewController {
let service = HardWorkService()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
print("TaskExampleViewController", #function)
doHardWork()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("TaskExampleViewController", #function)
}
deinit {
print("TaskExampleViewController", #function)
}
private func doHardWork() {
/// Здесь главный контекст, но когда мы обращаемся к сервису
/// и переходим в его для исполнения кода, то код метода уже будет
/// иметь исполнителя класса
service.doHardWork()
}
}
/// В этом случае на экран выведуться все 3 принта