Русский неавторизованный конспект лекций Стэнфордского университета " Разработка iOS приложений" 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 и 2023 гг., сопровождаемый решениями заданий и дополнениями, связанными с адаптацией курсов к новым версиям Swift, Objective-C и iOS.
В двух предыдущих постах мы рассмотрели создание логики игры 2048 и разработку UI с анимацией. В этом посте мы добавим ИИ (искусственный интеллект ) для игры 2048 в виде алгоритмов Expectimax и Monte Carlo. Код находится на Github.
ШАГ 16. Добавление AI в игру 2048
Добавление ИИ в игру 2048 подразумевает реализацию логики, которая может автоматически выбирать лучший ход на каждом шаге. ИИ будет, например, использовать функцию bestMoveDirection(), которую мы ранее обсуждали, чтобы определить, какой ход выполнить, основываясь на максимальном увеличении счета. В этом случае ИИ может автоматически играть в игру 2048, делая оптимальные ходы.
Таким образом, нам понадобится метод выполнения хода ИИ, возможность запуска его автоматически с определенной периодичностью, и, переключатель для переключения между ручным режимом со swipe жестом и воспроизведением ИИ.
Но давайте сначала поймем, какие в SwiftUI есть средства запуска определенного кода автоматически через равные промежутки времени:
В прошлом посте «iOS приложение игры 2048 в SwiftUI с ChatGPT 4-o. Часть 1. Логика игры» показано, как реализовать логику игры 2048 c помощью ChatGPT. В этом посте мы рассмотрим проектирование UI игры 2048 с помощью ChatGPT и особое внимание уделим анимации перемещения плиток на игровой доске. Код находится на Github.
Я хочу поделиться с вами опытом создания «с нуля» iOS приложения известной игры 2048 с элементами ИИ (искусственного интеллекта) в SwiftUI с помощью ChatGPT .Код находится на Github.
В своем классическом варианте, когда играет пользователь с помощью жестов (вверх, вниз, вправо, влево), это довольно простая игра и создать полноценное iOS приложение для такой игры 2048 можно за короткое время, при этом код будет понятен каждому. Но простые правила игры только подталкивают к созданию оптимальных алгоритмов решения игры 2048, то есть к созданию ИИ, который мог бы играть в эту игру автоматически и максимизировать счет игры в разумные сроки.
Мне хотелось написать игру 2048 именно на SwiftUI, пользуясь его прекрасной и мощной анимацией и приличным быстродействием , a также предоставить в распоряжения пользователя не только “ручной” способ игры, когда Вы руководите тем, каким должен быть следующий ход: вверх, вниз, влево и вправо, но и ряд алгоритмов с оптимальной стратегией (метода Монте-Карло, стратегий поиска по деревьям (Minimax, Expectimax) ), позволяющих АВТОМАТИЧЕСКИ выполнять ходы — вверх, вниз, влево и вправо — и добиться плитки с числом 2048 и более (эти алгоритмы и называют алгоритмами “искусственного интеллекта” (ИИ)). Необходимым элементом ИИ является алгоритм поиска, который позволяет смотреть вперед на возможные будущие позиции, прежде чем решить, какой ход он хочет сделать в текущей позиции.
2048 — это очень известная игра, и мне не нужно было объяснять ChatGPT ее правила, он сам всё про неё знает. Кроме того, оказалось, что ChatGPT прекрасно осведомлен об ИИ алгоритмах для игры 2048, так что мне вообще не пришлось описывать ChatGPT контекст решаемой задачи. И он предлагал мне множество таких неординарных решений, которые мне пришлось бы долго выискивать в научных журналах.
Ниже представлен небольшой фрагмент Лекции 15 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 15 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны здесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
. . . . . . . . . . . . . .
DocumentGroup в демонстрационном примере
Вот так обычно выглядит наше приложение.
И куда нам двигаться дальше? Что ж, чтобы наше приложение перестало быть тем, чем оно является сейчас, давайте, кстати, посмотрим, на что наше приложение способно сейчас, пока еще не добавили поддержку документов. Вот мое приложение, и я могу сказать: “Add another window” (“Добавить еще одно окно”), чтобы получить красивое фоновое изображение. Можно добавить сюда еще эмодзи (смайлики). Я еще раз могу сказать: “Add another window” и посмотрите, что произойдет, если я кликну на иконке нашего приложения. Я получаю второй Emoji Art. Итак, теперь у меня фактически есть два окна, смотрящих на один и тот же документ. Вы видите здесь два документа. Это то же самое. И у обоих есть “грузовик” 🚚.
Это потому, что оба этих окна смотрят на один и тот же @StateObject.
Это документ EmojiArt по умолчанию — defaultDocument. Поскольку они видят одну и ту же ViewModel, они показывают вам один и тот же документ. У каждого из них есть собственное масштабирование zoom и смещение pan, поскольку это отдельные View, они немного отличаются, но оба они просматривают один и тот же документ. Так что это в некотором смысле довольно удобно, что вы можете иметь несколько окон, просматривающих один и тот же документ, но это не так удобно, как иметь возможность просматривать множество документов. Итак, первое, что мы собираемся сделать, это заменить WindowGroup на DocumentGroup. Это то, что лежит в основе поведения всех документов. И вы помните, что у DocumentGroup был аргумент config, который является его конфигурацией. И вместо того, чтобы использовать наш документ по умолчанию defaultDocument, который у нас есть, мы просто возьмем нашу ViewModel из этой конфигурации — config.document:
Ниже представлен небольшой фрагмент Лекции 14 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 14 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны здесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
Итак, давайте поговорим о том, как я собираюсь загружать фоновое изображение 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 полученной ошибки. Итак, это состояния моего конечного автомата. Я собираюсь пройти через эти состояния шаг за шагом и выполнить все необходимые действия. Теперь, поскольку у меня есть этот маленький конечный автомат enumBackground, я также создал небольшие удобные функции. Давайте посмотрим на них:
Это маленькие переменные var, которые просто возвращают ассоциированные данные определенного состояния моего конечный автомат enumBackground, если я нахожусь в этом состоянии. Итак, вы видите вычисляемую переменную varuiImage:
Внутри мы переключаемся 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 переменную varisFetching:
Эта переменная равна true только в том случае, когда мой urlBeingFetched не равен nil, то есть когда я действительно выбираю изображение из Интернета. Потому что когда я нахожусь; в состоянии fetching, у меня есть URL-адрес, и это единственное состояние, когда у меня есть этот URL-адрес. Теперь, когда у меня есть мой компактный конечный автомат enumBackground, я собираюсь избавиться от переменной varbackground, я закомментирую это, чтобы запомнить, как это было:
Вместо этого я собираюсь создать новую @Published переменную var, которую назову background, её ТИП будет Background и её начальное значение будет .none:
Я делаю её @Published, потому что именно так мой UI будет видеть фоновое изображение background. UI всегда имеет возможность увидеть, в каком состоянии я нахожусь, потому что смотрите — я даю ему это перечисление enumBackground, в котором вы также можете использовать удобные функции.
Ниже представлен небольшой фрагмент Лекции 13 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 13 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступныздесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
. . . . . . . . . . . . . .
TextField и @Binding в действии
Следующее, самое важное — я хочу иметь возможность редактировать название палитры, а также добавлять эмодзи (смайлики). Как мне сделать это редактируемым текстовым полем? Редактируемые текстовые поля в Swift — это View отличные от обычного текста Text. Вместо Text они называются TextField.
TextField имеет два аргумента. Первый аргумент — это то, что мы называем Placeholder (Заполнитель) текста или слово, которое можно использовать, чтобы помочь пользователю понять, о чем мы здесь просим. Второй аргумент, он называется text — это тот текст, который мы редактируем внутри этого текстового поля TextField. И еще этот второй аргумент является привязкой Binding.
Если подумать о том, что здесь происходит, то у нас есть текстовое поле TextField, в котором текстом text является “Vehicles”. Мы хотим иметь возможность передать текстовому полю “Vehicles” как начальное значение, и всякий раз, когда что-то меняется в нем, мы хотим знать об этом. И то, как мы собираемся это сделать, заключается в создании единственного “источника истины” (single source of truth) для этого текста text. Мы будем делать это с помощью привязки Binding. В частности, этот второй аргумент text текстового поля TextField является привязкой Binding. TextField знает, что он не хочет поддерживать копию того, что редактируется, он хочет редактировать эту вещь напрямую. Поэтому он просит вас дать ему привязка Binding к этому “источнику истины” (source of truth). Ну a что является “источником истины” (source of truth) для этого palette.name? Он находится в нашей ViewModel, в нашем PaletteStore. Следовательно, нам нужно дать здесь нашему TextField обратную привязку Binding к нашей ViewModel. Для этого нам нужна привязка Binding к палитре palette, которую нам дали отредактировать в верхней части нашего PaletteEditor:
Сделав эту переменную var@Binding, мы заставляем того, кто создаст этот PaletteEditor, дать нам привязку Binding к “источнику истины” (source of truth) для этой палитры palette. Теперь каждый раз, когда мы ссылаемся на эту палитру palette где угодно в нашем коде здесь, в редакторе PaletteEditor, на самом деле мы будем ссылаться в обратном порядке на палитру в нашей ViewModel. И мы также можем использовать эту привязку Binding для передачи привязки Binding к имени палитры palette.name в нашем TextField:
Это потому, что для $, то есть projectedValue, для @Binding — это еще одна привязка Binding к той привязке @Binding. Итак, $palette.name здесь означает привязку Binding к этой @Bindingvarpalette, которая будет привязана в обратный порядке в конечном итоге к нашей ViewModel. Теперь наше текстовое поле TextField будет редактировать имя name палитры palette напрямую в ViewModel. Вы видите, что эти $, эти привязки Binding, проходят через всю нашу систему Views.
Ниже представлен небольшой фрагмент Лекции 12 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 12 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны здесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
. . . . . . . . . . . . . .
UserDefaults для сохранения палитр palettes
Давайте займемся нашими палитрами. Вы видите палитры прямо здесь?
Если я зайду в свои палитры и скажу New (новая), то добавляется математическая палитра Math. Если я заново перезапущу приложения, то мы обнаружим, что математическая палитра Math исчезла.
Приложение не помнит, что я добавил палитру Math (Математика). Или, если я удалю что-то, например, Sports, ну, нам не нравятся виды спорта. Если мы вернемся и перезапустим наше приложение, то палитра Sports возвращается.
Итак, мы хотим сделать так, чтобы все, что мы здесь делаем с палитрами, новые и удаленные палитры запоминались. Мы запомним это в UserDefaults, главным образом потому, что я уже показал вам, как это сделать в файловой системе, теперь я хочу показать, как это делать в UserDefaults. Вероятно, как мы говорили ранее, это не совсем уместно делать это в UserDefaults. Но учитывая довольно маленькое количество данных, мы собираемся сделать это в UserDefaults. Мы сделаем это очень крутым способом. Видите мои @Published палитры palettes, которые находятся в моем PaletteStore?
Позвольте мне избавиться от этого кода и превратить palettes в вычисляемое свойство:
Ниже представлен небольшой фрагмент Лекции 11 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 11 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны здесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
. . . . . . . . . . . . . .
Демо: масштабирование и перемещение по экрану EmojiArt
Мы хотим иметь возможность масштабировать и перемещать по экрану наш документ EmojiArtDocument с помощью движения пальцев. Вот как это выглядит, это немного устаревшая версия приложения Emoji Art, но я собираюсь выполнить жест pinch. Кстати, когда вы используете такой симулятор, вы можете выполнить жест pinch, удерживая клавишу option. И видите, у меня сразу на симуляторе появляются тут два пальца в виде серых кружков. Удерживаю option — и они появляются. Можно также перемещать с помощью жеста drag документ по экрану. Итак, я увеличиваю масштаб. Видите? Я выполняю жест pinch, чтобы увеличить масштаб. И, конечно, я могу уменьшить масштаб. Возможно, я захочу переместить мой документ и я выполняю жест drag.
Мы хотим перемещать документ с помощью жеста drag и изменять его масштаб с помощью жеста pinch, я собираюсь реализовать эти две вещи. Что мы должны перетаскивать drag и масштабировать pinch.? Содержимое нашего документа, это фоновое изображение и все эти эмодзи (смайлики). Все это увеличивается и перемещается, вы видите это представлено синим цветом:
Я собираюсь взять этот код и разместить его в отдельной переменной var с именем documentContents, и это то, что я собираюсь масштабировать и перемещать по экрану.
Ниже представлен небольшой фрагмент Лекции 10 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 10 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны здесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
. . . . . . . . . . . . . .
Демо. “Сброс” Drop фонового изображения background
Давай сделаем “сброс” (drop) URL-адреса фонового изображения background. Это действительно очень очень легко. Куда мы хотим это “сбросить”? По сути, мы хотим “сбросить” его поверх нашего ZStack, это наш документ. Итак, модификатор .dropDestination. Что мы сбрасываем? URL-адрес:
Обратите внимание на этот URL.self, когда вы пишите .self на нижнем регистре, то это означает сам ТИП. Таким образом, я передаю .dropDestination ТИП URL.self в качестве аргумента, так что мой документ знает, что мы ожидаем при “сбросе”. В замыкании у нас есть массив URL-адресов urls и место “сброса” location. Для фонового изображения background нам неважно место “сброса” location, но это нас явно интересует при “сбросе” эмодзи (смайликов), и мы должны поработать с этим location. Это замыкание должно вернуть информацию о том, был ли “сброс” успешным, потому что некоторые “сбросы” на этот View могут вас не устраивать. Тогда вам придется сказать:”НЕТ, лети обратно, туда, откуда пришел”. Я собираюсь вернуть return функцию funcdrop (urls, at: location, in: geometry), которую я собираюсь написать через секунду:
Ниже представлен небольшой фрагмент Лекции 9 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 9 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны здесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
. . . . . . . . . . . . . .
Колоду карт довольно просто реализовать.
Это может быть private переменной var, которая является someView и представляет собой ZStack несданных карт undealtСards. Не сданные карты undealtСards находятся прямо вверху и я создал их ранее. Мы будем рисовать несданных карт undealtСards с помощью того же CardView, что мы нарисовали любые другие карты:
Но есть кое-что, что я хочу сделать с этим — я хочу явно указать размер колоды карт с помощью маленького ViewModifier, который я покажу сегодня — это .frame, им очень легко злоупотребить, будьте очень осторожны при его использовании. О чем .frame? Я говорил вам, что Views сами решают, какого размера они хотят быть, помните это? Они могут это сделать с помощью .frame. Но сам по себе .frame в реальности не будет определять размер самого View. Это больше похоже на создание контейнера этого размера и предложения View этого пространства.
Помните, как при размещении Views на экране (Layout) вам предлагается пространство, a View сам определяет свой размер, чтобы там поместиться? Таким образом, .frame предоставит вам пространство. Теперь CardView использует всё предоставленное ему пространство. Итак, я могу написать .frame(width: deckWidth, height: deckWidth / aspectRatio), где deckWidth — это ширина моей колоды карт, и я позже сделаю для нее константу. Высота колоды, конечно же, будет равна ширине колоды деленной на соотношение сторон aspectRatio:
Написав этот код, мы получили небольшой контейнер, в котором нарисовались все наши карты. Это именно то, что я хочу. Но есть и другие вещи, которые вы можете делать с помощью .frame, a не только выбирать конкретные ширину и высоту. Я хочу, чтобы вы посмотрели в документации его возможности. Например, вы можете указать желаемую минимальную ширину, или желаемые максимальную ширину и высоту.
Теперь причина, по которой я сказал, что этим легко злоупотребить. Посмотрите, как мы программируем, мы никогда не указываем точные размеры вещей, мы просто размещаем их в VStacks и HStacks или располагаем в AspectVGrid и все такое. И мы даже использовали GeometryReader, чтобы узнать, a сколько у меня места? Потом мы разделяли полученное пространство между нужными нам Views, которые как бы отреагировали на то, сколько места им дали.. Единственный раз, когда мы выбрали фиксированный размер — это наш теперешний случай, когда мы создали пространство фиксированного размера, чтобы наш View (колода карт) в нем разместился. И мы даже использовали GeometryReader, чтобы узнать, a сколько у меня места? Потом мы разделяли полученное пространство между нужными нам Views, которые как бы отреагировали на то, сколько места им дали. Здесь я выбираю конкретную ширину колоды deckWidth равную 50: