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
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
SwiftUI dev
Protect mutable state with Swift actors https://developer.apple.com/videos/play/wwdc2021/10133/ 1️⃣ Обзор на сессию выложу в виде последовательных постов (номер по порядку смотрите в начале поста), так как инфы оч много, а тема достаточно интересная и нетрививальная.…
2️⃣ Продолжим пример:

extension Counter {
func resetSlowly(to newValue: Int) {
value = 0
for _ in 0..<newValue {
increment()
}
assert(value == newValue)
}
}


Обратим внимание, что функция resetSlowly() определена в расширении Counter и имеет прямой доступ к value актора и синхронно вызывает его методы. Таким образом await для increment() не нужен, так как resetSlowly() уже внутри actor.
❗️Это важное свойство actor: синхронный код, запущенный внутри actor, не прерывается!

Тем не менее, race condition может случиться в actors, рассмотрим пример:

actor ImageDownloader {
private var cache: [URL: Image] = [:]

func image(from url:) async throws -> Image? {
if let cached = cache[url] {
return cached
}
let image = try await downloadImage(from url)

cache[url] = image

return cache[url]
}
}

Представим ситуацию, при которой какая-то таска 1 пытается загрузить картинку по url, которой нет в кэше, в итоге функция suspend-ится в await, в это время таска 2, пытается также загрузить картинку по тому же url (так как кэша из таски 1 еще нет) и также suspend-ится в await, в это время таска 1 загружает картинку, resume-ится и кладет картинку в кэш, а на сервере вместе с этим меняется картинка и таска 2 загружает уже эту новую картинку, resume-ится, и перезатирает кэш с картинкой от таски 1. Фиксом этой ситуации будет следующий код после await:

actor ImageDownloader {
private var cache: [URL: Image] = [:]

func image(from url:) async throws -> Image? {
if let cached = cache[url] {
return cached
}
let image = try await downloadImage(from url)

cache[url] = cache[url, default: image]

return cache[url]
}
}


То есть мы отбрасываем новую картинку, если она отличается от той, что в кэше, или же сохраняем ее в кэш, если картинки там не было.
👨🏻‍💻Рекомендации при работе с Actor:
1. Стараться изменять state в синхронном коде внутри actor
2. Если синхронно сделать нельзя, то всегда проверять состояние/ожидаемый результат state после await (чтоб не было ситуации как в коде выше)!
❗️Важно: на actor не может быть deadlock-ов.

#readthis
SwiftUI dev
Protect mutable state with Swift actors https://developer.apple.com/videos/play/wwdc2021/10133/ 1️⃣ Обзор на сессию выложу в виде последовательных постов (номер по порядку смотрите в начале поста), так как инфы оч много, а тема достаточно интересная и нетрививальная.…
Рассмотрим как акторы взаимодействуют с протоколами, замыканиями и классами.
3️⃣ Рассмотрим про взаимодействие с протоколами.
Как и классы, акторы могут подписываться под протоколы, рассмотрим следующий код:

actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}

extension LibraryAccount: Equatable {
static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
lhs.idNumber == rhs.idNumber
}
}


Обратим внимание, что актор подписан под Equatable, а значит, должен соответстовать требованиями Equatable. Реализуем функцию ==. Так как метод статический, то есть нет собственного инстанса, то метод не изолирован (изолирован - когда нет прямых ссылок) от актора (вместо этого у нас есть два параметра типа LibraryAccount, и этот статический метод находится вне их обоих). Тут все ОК, потому что имплементация метода получает доступ только к неизменяемому (idNumber) состоянию актора.
Рассмотрим другой пример:

actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}

extension LibraryAccount: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(idNumber)
}
}

Компилятор выдаст ошибку. Почему? Соответствие Hashable таким образом означает, что эта функция может быть вызвана снаружи актора, но hash(into) не является асинхронным (методы актора снаружи можно вызывать через только через await - это правило), поэтому нет способа поддерживать изоляцию актора.
Чтобы исправить это, мы можем сделать этот метод неизолированным.

actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}

extension LibraryAccount: Hashable {
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(idNumber)
}
}


Неизолированный означает, что этот метод рассматривается как находящийся вне актора, даже если он и описан в акторе.
Поскольку неизолированные методы рассматриваются как находящиеся вне актора, они не могут ссылаться на изменяемое состояние актера (например, на booksOnLoan), в данном примере все ОК, так как ссылка на немутабельное свойство idNumber. Если бы мы попытались написать hasher.combine(booksOnLoan) компилятор выдал бы ошибку, потому что доступ к изменяемому состоянию извне привел бы к гонке потоков.

#readthis
SwiftUI dev
Рассмотрим как акторы взаимодействуют с протоколами, замыканиями и классами. 3️⃣ Рассмотрим про взаимодействие с протоколами. Как и классы, акторы могут подписываться под протоколы, рассмотрим следующий код: actor LibraryAccount { let idNumber: Int…
4️⃣ Поговорим про взаимодействие акторов с замыканиями.
Рассмотрим пример:

actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}

extension LibraryAccount {
func readSome(_ book: Book) -> Int { ... }

func read() -> Int {
booksOnLoan.reduce(0) { book in
readSome(book)
}
}
}


Как и функции, замыкания могут быть изолированы и нет. Функция reduce включает в себя замыкание, в котором вызывается readSome(book). Обратим внимание, что readSome(book) вызывается без await. Так как сама функция read изолирована от актора (по определению), а значит и замыкание внутри изолированной функции также изолировано. Таким образом, работа безопасна, так как метод reduce выполняется синхронно.
Усложним пример:

actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}

extension LibraryAccount {
func readSome(_ book: Book) -> Int { ... }

func readLater() -> Int {
Task.detached {
await self.read()
}
}
}


Task.detached здесь выполняет замыкание параллельно с другой работой, которую делает актор. Таким образом, здесь замыкание не может быть выполнено на акторе, иначе получим race condition. Таким образом, это замыкание не изолировано от актора.

Теперь предположим, что тип книги в акторе - это value тип - структура.

actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
func selectRandomBook() -> Book? { ... }
}

struct Book {
var title: String
var authors: [Author]
}

func visit(_ account: LibraryAccount) async {
guard var book = await account.selectRandomBook() else {
return
}
book.title = "\(book.title)!!!”
}

Если мы вызовем функцию visit, чтобы выбрать случайную книгу selectRandomBook(), мы получим копию Book. И при этом изменения в нашу копию Book не повлияют на актора и наоборот. Однако, если Book сделать class:

actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
func selectRandomBook() -> Book? { ... }
}

class Book {
var title: String
var authors: [Author]
}

func visit(_ account: LibraryAccount) async {
guard var book = await account.selectRandomBook() else {
return
}
book.title = "\(book.title)"
}


Теперь же при вызове функции visit, в методе selectRandomBook(), мы получим ссылку на изменяемое состояние актора, которое является общим ресурсом за пределами актора. А значит возможен data races из-за изменения title, поскольку метод visit не находится на акторе. Таким образом, если value типы и акторы безопасны для параллельного использования, то классы - нет.

#readthis
SwiftUI dev
4️⃣ Поговорим про взаимодействие акторов с замыканиями. Рассмотрим пример: actor LibraryAccount { let idNumber: Int var booksOnLoan: [Book] = [] } extension LibraryAccount { func readSome(_ book: Book) -> Int { ... } func read() -> Int…
5️⃣ Как устроен актор, Sendable

Типы, которые безопасны для общего параллельного(одновремнного) использования между акторами, называются Sendable.
Тип может быть Sendable, если его значение копируется из одного места в другое, и оба места могут безопасно изменять свои собственные копии этого значения, не мешая друг другу.
Таким образом, по определению value-типы и акторы являются Sendable, а классы могут быть Sendable, если реализованы тщательным образом, например, если класс и все его подклассы содержат только неизменяемые данные или если класс выполняет синхронизацию с блокировкой для обеспечения параллельного доступа, в остальных случаях классы нельзя отнести к Sendable. Функции необязательно являются Sendable, но для случаев, когда являются, вводится новый тип функций с аннотацией @Sendable.
На самом деле, акторы и вообще параллельный код должен общаться посредством Sendable типов, которые защищают код от date race.
Swift в конечном итоге предотвратит совместное использование не-Sendable данных: компилятор выдаст ошибку. Как компилятор это проверяет? Так как Sendable это протокол, то компилятор смотрит удовлетворяет ли тип протоколу. Например, рассмотрим код:

struct Book: Sendable {
var title: String
var authors: [Author]
}


Структура Book может быть Sendable тогда, когда все ее свойства удовлетворяют Sendable, и если Author является, например, классом, то [Author] не является Sendable, а значит и Book таким не является.
Дженерики, при определении являются ли они Sendable, все зависит от их параметров:

struct Pair<T, U> {
var first: T
var second: U
}

extension Pair: Sendable where T: Sendable, U: Sendable {
}


Pair будет Sendable, когда оба аргумента Sendable. Таким образом, и массив типов Sendable является Sendable.

Функции могут быть @Sendable. Это означает, что безопасно передавать значение функции между акторами. Это особенно важно для замыканий, где Sendable ограничивает то, что может сделать замыкание, чтобы предотвратить data race (например, замыкание не может захватить локальную переменную, потому что это позволит data race по этой переменной). И, наконец, синхронное замыкание Sendable не может быть изолировано от актора, потому что это позволило бы запустить код на акторе снаружи.

detached у Task определена следующим образом:
static func detached(operation: @Sendable () async -> Success) -> Task<Success, Never>

Потому и в примере выше:

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())
}


компилятор ругался на захват var counter.

Рассмотрим другой пример:

static func detached(operation: @Sendable () async -> Success) -> Task<Success, Never>

extension LibraryAccount {
func read() -> Int { ... }

func readLater() {
Task.detached {
self.read()
}
}
}


Поскольку замыкание для Task.detached является Sendable, то замыкание не должно быть изолировано от актора. Таким образом, вызов метода self.read() должен быть асинхронным. Преобразуем:

static func detached(operation: @Sendable () async -> Success) -> Task<Success, Never>

extension LibraryAccount {
func read() -> Int { ... }

func readLater() {
Task.detached {
await self.read()
}
}
}


Sendable типы и замыкания помогают поддерживать изоляцию акторов, проверяя, что изменяемое состояние не является общим для акторов и не может быть изменено одновременно.

#readthis
SwiftUI dev
5️⃣ Как устроен актор, Sendable Типы, которые безопасны для общего параллельного(одновремнного) использования между акторами, называются Sendable. Тип может быть Sendable, если его значение копируется из одного места в другое, и оба места могут безопасно…
6️⃣ Main actor
Когда мы работаем над приложением, нам нужно постоянно думать об основном потоке. Именно здесь происходит рендеринг UI, а также где обрабатываются события взаимодействия с пользователем. Тем не менее, если мы постоянно работаем в главном потоке, скажем, загружаем данные, то наш UI начинает фризить, особенно в случаях плохой связи. Такие операции нужно выносить из главного потока, а потом вызывать DispatchQueue.main.async всякий раз, когда есть конкретная операция, которая должна быть выполнена в основном потоке, например, присвоение в @Published.

func checkedOut(_ booksOnLoan: [Book]) {
booksView.checkedOutBooks = booksOnLoan
}

DispatchQueue.main.async {
checkedOut(booksOnLoan)
}


На самом деле, взаимодействие с основным потоком очень похоже на взаимодействие с актором.
И если мы работаем в основном потоке, то можем безопасно получить доступ и обновить state у UI, а если не на главном, то работать с ним нужно асинхронно. Именно так и работают акторы. Main actor - специальный актор, который представляет главный поток.
Код выше преобразовывается:

@MainActor func checkedOut(_ booksOnLoan: [Book]) {
booksView.checkedOutBooks = booksOnLoan
}

await checkedOut(booksOnLoan)

Главный актор отличается от обычного двумя свойствами:
1. Всю свою синхронизация выполняет через main disptach queue. Это означает, что с точки зрения выполнения на главном акторе логику можно заменить на DispatchQueue.main
2. Так как код и данные, которые нужно выполнять на главном потоке, разбросаны повсюду: SwiftUI, UIKit, фрэймворки, то достаточно указать для этого специальную аннотацию @MainActor (и код выполнить асинхронно на главном потоке).
У типов, помеченных @MainActor, члены и подклассы также будут @MainActor. Это удобно тех частей кода, которые должны взаимодействовать с пользовательским интерфейсом, где большинство все должно выполняться в основном потоке.

#readthis
SwiftUI dev
Protect mutable state with Swift actors https://developer.apple.com/videos/play/wwdc2021/10133/ 1️⃣ Обзор на сессию выложу в виде последовательных постов (номер по порядку смотрите в начале поста), так как инфы оч много, а тема достаточно интересная и нетрививальная.…
👨🏻‍💻Правила изоляции акторов:
1. Актор может читать свои собственные свойства или вызывать свои функции (т.е. используя self) синхронно.
2. Актор может обновлять только свои собственные свойства (и может делать это синхронно). Это означает, что вы можете обновлять свойства только с помощью ключевого слова self.
Попытка обновить свойство другого актора приведет к ошибке компилятора.
3. ☝🏼Считывание свойств между участниками или вызовы функций должны происходить асинхронно с использованием ключевого слова await. 🧐Однако перекрестное чтение неизменяемых свойств может происходить синхронно (тех, что объявлены с помощью let).
👍🏻 Весьма годная статья про акторы
https://apptractor.ru/info/articles/actors-swift-5-5.html

Выдержка:
Одно из лучших объяснений для общения в модели акторов выглядит следующим образом:
Представьте, что каждый актор похож на остров, а наша кодовая база — это мир с островами. Каждый остров может общаться с другим островом, отправляя ему сообщения в бутылке. Каждый остров знает, куда отправить сообщение (то есть адрес другого острова), и именно так работает связь между островами.

#readthis