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
SwiftUI dev
❗️Главное: 1. Не использовать AnyView, строится неоптимальный код 2. 📖 В SwiftUI два типа identity у View: structural и explicit, в UIKit - pointer identiity 3. 👆🏻 If-else - относится к structural типу. If-else разворачивается «под капотом» в _ConditionalContent…
This media is not supported in your browser
VIEW IN TELEGRAM
Пояснение к п.6:

когда объект подписываем под Identifiable, это означает что объект должен иметь что-то уникальное, по чему можно идентифицировать объект. Типичным хаком является использование UUID(), который относится к числу нестабильных explicit identity. Это означает, что каждый раз получая объект, мы будем получать новый - с другим UUID, это убивает reusability и может вызывать проблемы с памятью помимо проблем с просмотром и анимацией.

#readthis
Protect mutable state with Swift actors
https://developer.apple.com/videos/play/wwdc2021/10133/

1️⃣ Обзор на сессию выложу в виде последовательных постов (номер по порядку смотрите в начале поста), так как инфы оч много, а тема достаточно интересная и нетрививальная.

Одна из основных проблем многопоточности - data race/ гонка данных (когда минимум два потока пытаются получить доступ к общему ресурсу и минимум один - доступ на запись).
Рассмотрим код:

class Counter {
var value = 0

func increment() -> Int {
value = value + 1
return value
}
}

let counter: Counter = .init()

Task.detached {
print(counter.increment())
}

Task.detached {
print(counter.increment())
}


В разный момент времени print может вернуть 1, 2; 2,1 и даже 2,2 или 1,1 (если обе таски прочитают 0 или 1 в начальном состоянии).
Проблема гонки потоков относится к классу недетерминированных (которые можно получить строго в определенной последовательности), так как системный планировщик может запускать параллельные таски каждый раз по-разному.

Value семантика позволяет избегать гонки потоков как в следующем примере (Словари и массивы в Swift относятся к value типам):

var array1 = [1, 2]
var array2 = array1
array1.append(3)
array2.append(4)
print(array1) // [1,2,3]
print(array2) // [1,2,4]


Но при этом, если мы изменим код сверху в value-семантике (выбрав не class, а struct):

struct Counter {
var value = 0

mutating func increment() -> Int {
value = value + 1
return value
}
}

Var counter: Counter = .init()

Task.detached {
print(counter.increment())
}

Task.detached {
print(counter.increment())
}


гонка потоков все равно остается, так как на счетчик ссылаются из двух параллельных задач. К тому же, компилятор и вовсе выдаст ошибку. Таким образом, необходим инструмент для синхронизации для одновременного доступа к общему изменяемому state из параллельных задач (до этого момента мы могли юзать Atomic, Locks или серийные очереди).
🧐Actors предоставляют такой инструмент синхронизации. У Actor есть свой state, который изолирован от остальной части программы так, что любой доступ к state происходит через actor, а actor из коробки обеспечивает, что никакой другой код параллельно не получит доступ к state (по сути это инструмент и напоминает lock и серийную очередь).
❗️Actors - новый тип в swift, у них те же возможности, что и у обычных name-типов: у них могут быть свойства, методы, конструкторы, subscripts, могут подписываться под протоколы и расширяться через extensions, но не наследоваться!
❗️Actors относятся к reference типам как классы (потому что главное назначение actors - предоставлять доступ к общему ресурсу)
Таким образом, основной отличительной чертой actors является то, что они изолируют данные от остальной части программы и обеспечивают синхронизированный доступ к этим данным.
Изменив, код выше на:

actor Counter {
var value = 0

func increment() -> Int {
value = value + 1
return value
}
}


Мы гарантируем, что доступ к value не будет предоставляться параллельно.
Но при этом, если мы запустим следующий код:

let counter: Counter = .init()

Task.detached {
print(counter.increment())
}

Task.detached {
print(counter.increment())
}


То print нам вернет или 1,2 , или 2,1 (так как очередность тасок не гарантируется), но при этом НЕ будет значения 1,1 или 2,2!

В случае же одновременного доступа к state, как мы можем гарантировать, чтобы другая таска ждала, пока выполнится первая? Для этого в swift появился механизм из коробки для этого: запускать через await.

Task.detached {
print(await counter.increment())
}

Task.detached {
print(await counter.increment())
}


Так как таска запускается как async, то другая таска пытающаяся получить доступ к actor, видя что он занят, перейдет в режим suspended и CPU будет выполнять другую работу, поток не будет блокирован.

#watchthis #readthis