SwiftUI dev
1.07K subscribers
87 photos
36 videos
1 file
73 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
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
Modern swift API design.pdf
2.3 MB
Modern Swift API Design
https://developer.apple.com/videos/play/wwdc2019/415/
Отличная презентация про современной подход к дизайну API приложения, поднимаются такие вопросы, как какие типы - value или reference - использовать и где, когда лучше вводить протоколы, а когда дженерики (общий посыл: не надо чрезмерно злоупотреблять протоколами (принцип less code is better), а чаще использовать generic типы), рассказывают про PropertyWrappers (ключевая аннотация при объявление @propertyWrapper) и их роль в SwiftUI.

#watchthis
🧭 Быстрая навигация на канале

#readthis - ссылки на статьи, книги и др
#watchthis - ссылки на видео
#howto - воркшопы, обучающие статьи и т п
#getsources - ссылки на проекты с открытым исходным кодом (включая #swiftpm модули)
#trytodo - челенджи, иногда простые, иногда не очень
#groovy - посты с наибольшим количеством шарингов и реакций
#tasty - “посмотри, чтоб вдохновиться”, здесь будут анимации, концепты и т п
🧭 Quick navigation

#readthis - recommended articles, books, etc
#watchthis - recommended videos, clips, etc
#howto - tutorials, rtfm
#getsources - where the hell are sources? open-source repositories (including my own swift packages #swiftpm), projects
#trytodo - “try to do” challenges, sometimes not easy
#groovy - trending high-rated posts based on statistics (private or public sharing and positive reactions)
#tasty - cool creative features (animations, concepts, etc), might be useful for inspiring developers, designers or PMs