SwiftUI dev
1.17K subscribers
87 photos
37 videos
1 file
74 links
Mobile development, SwiftUI, Compose, feel free to reach me: @lexkraev
Download Telegram
Discover concurrency in SwiftUI
https://developer.apple.com/videos/play/wwdc2021/10019/

Рассмотрим следующий код:

class Photos: ObservableObject {
@Published var items: [Photos] = []

func updateItems() {

let fetched = fetchPhotos()
items = fetched
}
}

1. Присвоение значения в переменную items, помеченную как @Published, триггерит событие objectWillChange, сразу после этого в storage @Published записывается новое значение.
2. Как только SwiftUI видит событие objectWillChange, происходит snapshot текущего значения items, в следующем runloop-е происходит сравнение сделанного snapshot с текущим значением items
Именно такая последовательность гарантируется, так как код запускается на Main actor.
Предположим, что загрузка фото, происходит слишком долго вследствие слабого соединения, и в этом случае мы хотим “разгрузить” главный поток, чтобы как минимум не было фриза у приложения, для этого мы выносим загрузку в глобальную очередь:

class Photos: ObservableObject {
@Published var items: [Photos] = []

func updateItems() {
DispatchQueue.global().async {
let fetched = fetchPhotos()
items = fetched
}
}
}

В этом случае, возможна следующая ситуация:
3. Срабатывает событие objectWillChange
4. SwiftUI делает snapshot текущего значения items
5. Само присвоение items происходит уже в новом цикле runloop-а (то есть триггер события objectWillChange и присвоения items происходит в разных циклах)
6. SwiftUI сравнивает значения и не видит изменения (так как новое еще не успело записаться в новом цикле, а snapshot еще равен текущему значению)
Чтобы SwiftUI видел изменения, нужно чтобы изменения произошли в следующем порядке:
objectWillChange - Изменение состояния - Runloop входит в новый цикл
Чтобы гарантировать такой порядок до Swift 5.5 и iOS 15 (ждем поддержки ниже в Xcode 13.2), необходимо было вернуться на Main actor. Начиная с Swift 5.5 и iOS 15 (начиная с Xcode 13.2, ниже), все становится проще и достаточно юзать просто await, который продолжит выполнение работы на Main actor после того, как отработает async. Это называется “to yield main actor”.

class Photos: ObservableObject {
@Published var items: [Photos] = []

func updateItems() async {
let fetched = await fetchPhotos()
items = fetched
}
}

Чтобы гарантировать, что работа происходит на Main actor мы добавляем аннотацию @MainActor в начало описания класса, это означает, что свойства и метода доступны только из Main actor.
Связывать UI теперь можно не через onAppear { Task {…}}, а через модификатор .task { async {…} }, доступный с iOS 15.
Выполнение .task {} привязано к жизненному циклу View и автоматически отменяется, если жизненный цикл View заканчивается.

#watchthis #readthis