Лекция 14. Многопоточность, Обработка ошибок. CS193P Spring 2023.

Ниже представлен небольшой фрагмент Лекции 14 Стэнфордского курса CS193P Весна 2023 «Разработка iOS приложений с помощью SwiftUI«.
Полный русскоязычный неавторизованный конспект Лекции 14 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны на платной основе.
Код находится на GitHub.

С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.

. . . . . . . . . . . . . .

Демонстрационный пример: конечный автомат Background

Итак, давайте поговорим о том, как я собираюсь загружать фоновое изображение background концептуально. Я собираюсь использовать конечный автомат (state machine).
Сколько человек реально написали код, который использует конечный автомат (state machine)?
Ну, не так уж и много. Интересно.
Итак идея программирования конечных автоматов заключается в том, что я собираюсь подумать обо всех состояниях (states), в которых я могу оказаться, и выполняю некоторый процесс. На самом деле я собираюсь закодировать их и делать каким-то образом пометки в моем коде для каждого из этих шагов. И отличный ТИП данных для конечных автоматов — это перечисление enum. Потому что по определению вы перемещаетесь по этим различимым состояниях (states), a перечисление enum как раз и представляет различимые состояния (states).
Вот что представляет собой мой конечный автомат (state machine). Я размещу весь этот код отдельно, в разделе // MARK: — Background Image. Это будет перечисление enum с именем Background:

И каковы состояния (states) выборки чего-либо из Интернета?
Вы можете быть в состоянии none, то есть вы ничего не делаете. Нет, фонового изображения  background, которое Drag & Drop (перетаскивается и сбрасывается), вы просто нигде:

Возможно, я сейчас выбираю данные из Интернета, так что я мог бы назвать состояние fetching. Но когда я выбираю, возможно, я хочу знать URL, по которому идет выборка, и я могу получить URL в качестве ассоциированных данных:

Итак, я сделал выборку по этому URL, и либо у меня есть изображение, либо мне не удается этого сделать из-за какого-то сетевого сбоя или чего-то в этом роде.
В результате у меня действительно добавляются еще два состояния в моем конечном автомате.
В случае успешного завершения выборки я нахожусь в состоянии found с изображением UIImage, в противном случае я нахожусь в состоянии failed, и в этом случае я мог бы сохранить ошибку, которую я получил:

Для простоты я сохраняю ошибку в виде String, которая описывает, вероятно, localizedDescription полученной ошибки.
Итак, это состояния моего конечного автомата.
Я собираюсь пройти через эти состояния шаг за шагом и выполнить все необходимые действия.
Теперь, поскольку у меня есть этот маленький конечный автомат enum Background, я также создал небольшие удобные функции.
Давайте посмотрим на них:

Это маленькие переменные var, которые просто возвращают ассоциированные данные определенного состояния моего конечный автомат enum Background, если я нахожусь в этом состоянии.
Итак, вы видите вычисляемую переменную var uiImage:

Внутри мы переключаемся switch по self, и если я нахожусь в состоянии found с ассоциированным значение uiImage в виде выбранного изображения, то возвращает это изображение uiImage. В противном случае он просто возвращает nil.
То же самое происходит с получением URL-адреса urlBeingFetched, по которому осуществляется выборка:

Внутри мы переключаемся switch по self, и если я нахожусь в состоянии fetching с ассоциированным значение url, то возвращает этот url, по которому идет выборка изображения. В противном случае он просто возвращает nil.
То же самое с причиной неудачной выборки failureReason:

Если я нахожусь в состоянии failed, дайте мне причину reason моей неудачи.
Это всего лишь удобные переменные.
Так что я могу просто спросить, какое у меня фоновое изображение uiImage прямо сейчас?
И это будет nil, если у меня его просто нет. Если я не в состоянии found, когда я получаю изображение uiImage, a в любом другом состоянии: none или fetching или failed, я также получу nil.
Я разместил также маленькую Bool переменную var isFetching:

Эта переменная равна true только в том случае, когда мой urlBeingFetched не равен nil, то есть когда я действительно выбираю изображение из Интернета. Потому что когда я нахожусь; в состоянии fetching, у меня есть URL-адрес, и это единственное состояние, когда у меня есть этот URL-адрес.
Теперь, когда у меня есть мой компактный конечный автомат enum Background, я собираюсь избавиться от переменной var background, я закомментирую это, чтобы запомнить, как это было:

Вместо этого я собираюсь создать новую @Published переменную var, которую назову background, её ТИП будет Background и её начальное значение будет .none:

Я делаю её @Published, потому что именно так мой UI будет видеть фоновое изображение background. UI всегда имеет возможность увидеть, в каком состоянии я нахожусь, потому что смотрите — я даю ему это перечисление enum Background, в котором вы также можете использовать удобные функции.

Например. По мере того, как я прохожу через эти состояния, так как URL-адрес в моем Emoji Art меняется, мой UI должен иметь возможность увидеть все это.
Я в процессе выборки? Для этого существует состояние fetching.
Есть ли у меня изображение, которое я могу показать?
Я получил ошибку? UI даже может выдать оповещение alert об ошибке, потому что он может видеть, где мы находимся.

Давай сделаем выборку изображения, просто чтобы посмотреть, что я имею в виду под этим.
Вот наш EmojiArtDocumentView, a это AsyncImage:

Помните, у нас там был этот phase? Этот phase представляет собой перечисление enum, и в какой-то степени  отражает процесс выборки изображения. Он действительно не позволяет вам посмотреть на него, когда вам захочется. Итак, мы собираемся полностью избавится от нашего AsyncImage и заменить это нашим конечным автоматом (state machine).
Как будет работать наш конечный автомат?
Я просто могу написать следующий код:

Если я в состоянии found с изображением document.backgrounds.uiImage, отличным от nil, только тогда я смогу показать это изображение на экране Image ( uiImage: uiImage) и задать ему такое же местоположение .position, как это было с AsyncImage. Вот и все.
Посмотрите, насколько прост мой UI код. Нет никаких ошибок.
Почему здесь так легко все получилось?
Потому что я выбрал хорошую абстракцию для поиска изображение, и мой UI просто наблюдает за этим. И поскольку мой background — это @Published, то как только появится UIImage, он тут же покажет его.

Теперь нам придется зайдите в нашу ViewModel и заставить наш конечный автомат пройти через свои состояния. Но где это начинается?
Начинается это, когда кто-то устанавливает URL-адрес нашего фонового изображения. Как только кто-то устанавливает наше фоновое изображение, нам нужно начать выбирать это изображение из Интернета. 
Итак, где мы можем узнать что это произошло?
Есть очень круто место, чтобы сделать это прямо здесь:

Это наша модель emojiArt. И мы уже что-то делаем, когда она меняется, то есть мы выполняет автосохранение autosave(). Теперь мы сделаем ещё одну вещь, когда она изменится, a именно, мы хотим посмотреть, изменилось ли наше фоновое изображение background. И если это так, то мы начнем выборку его из Интернета.
Это очень просто.
Я могу сказать, что если мое фоновое изображение emojiArt.background не равно старому oldValue.background, другими словами, это произойдет, когда модель emojiArt изменится, но сейчас я интересуюсь конкретно тем, когда изменяется background:

Что я собираюсь в этом случае делать? 
Я просто вызову функцию fetchBackgroundImage (), и она будет управлять нашим конечным автоматом (state machine):

Вот что собирается делать fetchBackgroundImage ().

Выборка изображения с помощью URLSession.shared 

Давайте спустимся сюда, в нашу милую маленькую область фонового изображения Background image и сверху разместим нашу private функцию func fetchBackgroundImage ( ) :

И вы, ребята, знаете, как я люблю программировать, мне нравится просто печатать, как будто бы это работает, а потом исправлять все проблемы. Потому что когда я просто печатаю, я хотя бы получаю представление о том, чего добиваюсь. Возможно, я ошибаюсь, и это может просто не работать, но, по крайней мере, я могу напечатать так, как я хочу.
Так вот что я хочу здесь. 
Если URL-адрес url существует, то есть у модели emojiArt есть фоновое изображение background, тогда я хочу запустить свою машину состояний, сказав, что я нахожусь в состоянии выборки fetching (url) по этому url:

Затем я хочу перейти на следующий уровень, надеюсь, это .found:

И как я собираюсь установить найденный UIImage?
Для этого мне понадобится функция, я назову её fetchUIImage (from: url):

ИЛИ . . . .

Мы можем потерпеть неудачу при выборе изображения, в этом случае я хочу перейти к background = .failed с каким-то сообщением об ошибке:

И если URL-адрес равен nil, тогда background снова возвращается в .none:

Вы видите, как этот код перемещает меня по всем состояниям моего конечного автомата (state machine)?
Очевидно, у меня здесь проблема.
У меня нет функции func fetchUIImage (from url: URL). И, возможно, у нас будут еще какие-то изменения в процессе совершенствования кода.
Но давайте продолжим и создадим эту private функцию func fetchUIImage(from url: URL), она возвращает UIImage:

Как выглядит эта функция? 

Мне нужно получить данные Data из этого URL-адреса. Надеюсь, это типа JPEG, PNG, TIFF файлов или что-то в этом роде, извлекаем эти данные и создаем из них UIImage.
Помните? Я говорил вам, что UIImage — это та вещь, которую мы создаем, чтобы как бы удержать изображение в своих руках.
Давайте сделаем это. Мы действительно знаем, как получить данные Data из URL
Мы делали это раньше:

Помните? У нас была эта функция? Тогда, возможно, я мог бы просто вернуть UIImage с этими данными data:

Итак, это идея. Это не очень хорошая идея, но это — идея.
Какая проблема с этой штукой?
Дело не в этих ошибках. Дело в том, что это не async функция, поэтому она блокирует наш UI.
Так что, если я вызову эту функцию прямо здесь, то она заблокирует мое приложение. Пока я буду выбирать эти данные, приложение не будет реагировать на прикосновения, ни на жесты Swipe, ни на что. Оно просто застрянет, потому что эта функция блокирует UI.
Data (contentsOf: url) работает с HTTP URL-адресами, но на самом деле это не предназначено для такого использования. Это для URL-адресов файлов, где не происходит блокировки UI. URL-адрес файла соответствуют определенному файлу, который вы считываете очень быстро.
Так что это не то, что мы хотим использовать.

К счастью, конечно, есть что-то, что можно использовать для получения этих данных, и это URLSession. URLSession — это структура struct, у которой есть всевозможные механизмы для загрузки различных вещей с URL-адреса. И это действительно сложно и мощно, но у него есть хороший shared, глобальный общий ресурс, который мы можем использовать, чтобы создать его, и у него есть некоторые конфигурационные параметры. Кстати, у этого URLSession.shared экземпляра есть все наиболее распространенные конфигурации.
И одна из функций, которая есть в URLSession — это data (from: url):

У нас ошибка:
(“‘async’ call in a function that does not support concurrency.”)
( “Асинхронный вызов в функции, которая не поддерживает многопоточность.”)

Итак, эта функция URLSession.shared.data (from: url) является асинхронно, она помечена как async. Она знает, как вести всю эту игру с отложенным (suspended) кодом, a именно этого мы и хотим. Через секунду мы разберемся с этой ошибкой.
Теперь у меня есть еще одна проблема, а именно:

Она говорит:
(“Cannot convert value of type ‘(Data, URLResponse)’ to expected argument type ‘Data’.”)
(“Не могу преобразовать значение ТИПа ‘(Data, URLResponse)’ в ТИП ‘Data’.”)

На самом деле URLSession.shared.data (from: url) возвращает не только данные Data. Она возвращает кортеж, в котором помимо данных Data, есть и ответ URLResponse:

Поэтому, когда вы делаете HTTP-запрос, вы получить некоторую информацию о том, что произошло с этим запросом, а не только данные, которые были в запрошенном вами файле, но некоторую дополнительную информацию. В данный момент нам не нужно ничего дополнительного, так что использую символ “подчеркивания” “_”, чтобы просто убрать это с моего пути, потому что все, что мне нужно, это фактические данные data, а не делать там какие-то специфичные для HTTP вещи:

Теперь у нас возникла еще одна проблема:

Value of optional type ‘UIImage?’ must be unwrapped to the value of type ‘UIImage’.
(“Значение Optional ТИПа ‘UIImage?’ необходимо развернуть в значение ТИПа ‘UIImage’.)

Интересно. В данный момент я просто принудительно разверну (force unwrap) его, чтобы эта ошибка исчезла:

Но у нас будет возможность поработать с этим фактом, потому что функция UIImage (data:data) возвращает nil всякий раз, когда ей не удается увидеть файл JPEG файл или TIFF или PNG, в этом случае я не могу получить изображение UIImage.

. . . . . . . . . . . . . .

Смотрите код на Github EmojiArt L14 и EmojiArt L!4 Observation palettes.
На Лекции 14 рассматриваются следующие вопросы:

  • Цвета Colors и Изображения Images
  • Многопоточное программирование
  • Task
  • Ловушка многопоточности
  • ТИП actor, async функции
  • await, async функции
  • await внутри Task
  • async замыкания
  • Main Actor
  • Обработка ошибок
  • Демонстрационный пример: конечный автомат Background
  • Выборка изображения с помощью URLSession.shared
  • try await
  • “Выбрасываем” собственную ошибку enum FetchError
  • Обработка ошибок с помощью do { } catch { }
  • Фиолетовый треугольник смерти. @MainActor
  • ProgressView
  • .alert
  • zoomToFit
  • Двойной клик, чтобы увидеть всё. bbox

Полный русскоязычный неавторизованный конспект Лекции 14 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны на платной основе.

С полным перечнем Лекций и Домашних Заданий Стэнфордского курса CS193P Весна 2023 «Разработка iOS приложений с помощью SwiftUI» на русском языке можно познакомиться здесь.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *