Впечатление от Стэнфордских курсов CS193P Весна 2020 г.: Разработка iOS приложений с помощью SwiftUI



    Стэнфордский университет, США — один из лучших в мире в области информатики (Computer Science). Он щедро делится своими курсами, и одним из самых популярных и успешных курсов является курс CS193P по разработке приложений на iOS, который читает профессор Пол Хэгерти.
    Предложенные в весеннем семестре 2020 года лекции Стэнфордского курса CS193P «Developing  Application for iOS with SwiftUI» («Разработка приложений для iOS с использованием SwiftUI») были прочитаны студентам Стэнфорда с ориентацией на новый продукт, предоставленный Apple разработчикам в 2019 г, — фреймворк SwiftUI для разработки реактивного пользовательского интерфейса (UI). На сайте курса вы найдете материалы, которые были предоставлены студентам Стэнфорда в течение весеннего семестра 2020 г.: ссылки на видео, слайды, домашние задания и код демонстрационных примеров. Русскоязычный конспект курса представлен здесь.

    SwiftUI — это совершенно новая вещь, которой было всего несколько месяцев от роду на момент прочтения лекций. Но это самый передовой край технологий, которые, наконец, добрались до разработки приложений на iOS. В июне 2020 г. состоялась международная конференция разработчиков Apple WWDC и там была представлена следующая версия SwiftUI 2.0. Изменения в версии SwiftUI 2.0 отражены на сайте курса на закладке WWDC.

    Если вы уже программировали на iOS ранее с использованием UIKit, готовьтесь полностью перевернуть своё мировоззрение на разработку iOS приложений, этот Стэнфордский курс реально «сносит голову» и это здорово, потому что вы попадаете совсем в другой Мир. Даже если вы в ближайшее время не планируете разрабатывать приложения на SwiftUI — посмотрите этот курс, там куча оригинальных идей и это будущее.

    Да, SwiftUI имеет множество фантастических анимаций и очень крутые возможности проектирования UI, но для разработчика реальных iOS приложений фундаментальные основы функционирования SwiftUI связаны с потоком данных между различными Views, между View и Model, между View и «внешним Миром» (пользователем или интернетом). Поэтому профессор в своем курсе уделяет особое внимание именно «потоку данных» в приложениях, построенных на основе SwiftUI, и уже на Лекции 2 мы обсуждаем MVVM.

    Далее я приведу лишь небольшой кусок из лекций 2 и 3 профессора Пола Хэгерти, объясняющий реактивную природу SwiftUI и MVVM, чтобы вы понимали, на каком уровне идет обучение в этом курсе.

    Используемая в SwiftUI концепция создания UI называется Reactive. Она имеет декларативную природу в отличие от императивной, которая использовалась в предыдущем фреймворке проектирования UIUIKIt.

    View в SwiftUI имеет декларативный характер, то есть вы просто декларируете, как выглядит ваш View, и что действительно будет меняться на экране, если изменится Model или «внешний Мир». View в SwiftUI воспринимает эти изменения через специальные «реактивные» переменные @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject. В отличие от UIKIt, в SwiftUI View нет ни делегата delegate, ни data source, ни каких-то других «паттернов» управления состоянием UIView. В SwiftUI View могут присутствовать только указанные выше Property Wrapper («Обертки Свойства»), маркируемые знаком @, они «улавливают» нужные им изменения и «перерисовывают» этот View.

    Изменения исходят от ViewModel, которая ставится между Model и View или между «внешним Миром» (интернетом) и View. Именно она интерпретирует все изменения Model или «внешнего Мира» для View и запускает тот самый «реактивый» механизм, который помогает автоматическому обновлению View при любых изменениях.
    ViewModel НИКОГДА не «говорит» с View напрямую. Когда что-то изменилось в Model или во внешнем Мире, ViewModel публикует сообщение: “Что-то изменилось...” А View “подписывается” на эту публикацию, и если View видит, что “Что-то изменилось...”, то обращается к ViewModel и запрашивает: “Какое текущее состояние (state) в этом Мире? Я собираюсь нарисовать себя в соответствии с этим текущим состоянием!”
    У ViewModel есть свой арсенал «реактивности» — объекты, реализующие протокол ObservableObjectи способные посылать сообщение objectWillChange, @Published переменные, модификатор .environment. Чаще всего ViewModel — это класс class, так как услугами ViewModel хотят одновременно воспользоваться множество Views.

    Так что вы не можете работать в SwiftUI без MVVM.
    MVVM — это целая система. Но существует и нечто другое, также относящееся к архитектуре приложения, и называется это «нечто» Model-View-Intent (Модель — Изображение — Намерение — MVI). Что делает более понятной архитектуру приложения, когда пользователь что-то хочет сделать и проходит через это “намерение”.

    В настоящий момент дизайн конструирования UI для iOS приложений с помощью фреймворка SwiftUI не реализует Intent систему, так что профессор рассказывает об Intent системе, как о концепции.
    Intent — это некоторое “намерение” пользователя.

    Классическим примером “намерения” Intent в нашей карточной игре на запоминание является “намерение” пользователя выбрать карту. Это и есть “намерение” Intent. Обработка этих “намерений” Intent остается на усмотрение ViewModel.



    Конечно, подобные теоретические концепции конкретизируются в демонстрационных примерах. Каждый символ и каждая стрелка на этом рисунке будут объяснены и показаны профессором в коде.

    В основной части курса, которая состоит из первых 10 Лекций для обязательного изучения, рассматриваются 2 демонстрационных примера — карточная игра на совпадение Memorize и создание «картин» путем «перетягивания» (Drag)эмоджи из разного рода палитр на фоновое изображение в приложении EmojiArt. В точности эти же демонстрационные примеры были и в предыдущей версии курса CS193P «Developing  iOS 11 Apps with Swift» Весна 2017.

    Но ни одной строчки кода из предыдущего курса профессор не заимствует, потому что на этот раз использует не объектно-ориентированное программирование (ООП) как в предыдущем курсе, а Функциональное программирование (ФП) или Протокол — Ориентированное Программирование (ПОП).

    В ПОП вместо superclass, который используется в ООП, вы используете протокол protocol для описания «поведения» объекта, вместо классов class (Reference Type) используются структуры struct и перечисления enum (Value Type), а для реализации полиморфизма без наследования и Reference семантики — Generic или «Не важно, какие» ТИПы, как называет их профессор в своих лекциях, хотя реальное общепринятое имя такого ТИПа — это ПАРАМЕТР ТИПА (type parameter). Сочетание protocol + Generic и является основой функционального программирования в Swift. Для своих студентов профессор Пол Хэгерти называет этот механизм Constraints and Gains (Ограничения и Выгоды), когда вы объявляете Generic структуру struct, а затем заставляете её делать определенные вещи согласно протоколу protocol, а, по существу, ограничиваете, но взамен она приобретает новые «поведенческие» возможности. Профессору нравится рифма выражения Constraints and Gains (Ограничения и Выгоды), но и на самом деле это так и работает.

    Вот пример использования механизма Constraints and Gains (Ограничения и Выгоды) для Model карточной игры на совпадение Memorize:



    На протяжении всего курса, начиная с Лекции 2 и где только можно, профессор будет демонстрировать на конкретных примерах этот механизм Constraints and Gains (Ограничения и Выгоды). Например, при создании своей собственной “сетки” Grid, которая повторяет семантические возможности ForEach в комбинацией с 2D HStack для размещения каждого отдельного View в нужном месте, определяемым строкой и столбцом:



    Далее Пол Хэгерти посвящает ряд Лекций тому, что вы можете разместить в декларативном описании View в SwiftUI, то есть своеобразному SwiftUI DSL (domain-specific language).

    Прежде всего это:

    1. анимация (явная и неявная, протокол Animatable, «переходы» Transition, модификатор AnimatableModifier ),
    2. элегантная система Layout,
    3. Shape,
    4. жесты (@GestureState),
    5. ViewModifier,
    6. @ViewBuilder,
    7. модальные Views .sheet, .popover,
    8. навигацию NavigationView, NavigationLink

    Особое внимание и демонстрационные усилия направлены, конечно же, на анимацию, там много всего интересного, но неожиданно подробно описываются «переходы» Transition , о которых мало что можно найти и в документации Apple, и в интернете, но Полу Хэгерти пришлось это сделать, потому что ему нужно «красиво» «перевернуть» карту в карточной игре на совпадение Memorize. Кроме того там рассматривается конструирование «анимации того, что должно произойти в ближайшее время», хотя все анимации отражают то, что уже произошло. Я до конца ещё не разгадала эту анимацию, но это — действительно круто.

    Самой важной для понимания реактивной природы SwiftUI является Лекция 9 «Data Flow», на которой Пол Хэгерти очень подробно рассматривает «реактивные» Property Wrapper («Обертки Свойства») @Published, State, @Binding, StateObject, @ObservedObject, @EnvironmentObject. Вы не сможете полноценно программировать на SwiftUI, если не поймете их смысл, их нестандартную инициализацию и т.д.

    Ещё одна приятная особенность этого курса состоит в том, что по сравнению с более ранними версиями курса CS193P профессор рассказывает не только о какой-то одной системе постоянного хранения (persistence), а представляет в действии практически все системы постоянного хранения (persistence) в iOS:

    UserDefaults. Профессор адаптировал древний UserDefaults к «реактивности» в плане автосохранения.
    DocumentGroup (UIDocument аналог). Интегрирует приложение Files и данные, воспринимаемые пользователем как «документы” в ваше приложение. Появился в iOS 14 в SwiftUI 2.0.
    Core DataМощная объектно-ориентированная база данных. Элегантная интеграция со SwiftUI.
    Cloud Kit. База данных, расположенной в „облаке“ (то есть в интернете), работающая полностью асинхронно. Следовательно, данные появляются на любых устройствах пользователя.
    UFileManager/URL/Data. Запоминание данных в файловой системе iOS.

    Дело в том, что условно весь Стэнфордский курс CS193P в этом году можно разделить на две части: обязательную для изучения часть (Лекции 1-10) и дополнительную (Лекции 11-14), которая состоит из 4-х последних Лекций, призванных поддержать разработку студентами своих финальных проектов. В дополнительную часть включены:

    Лекция 11 рассматривает такую важную тему как Picker (средство выбора).
    Лекция 12 посвящена Core Data.
    Лекция 13 содержит обзор всех систем постоянного хранения (Persistence) на iOS, там демонстрируется использование файловой системы iOS.
    Лекция 14 — интернация UIKit в SwiftUI.

    Лекции 11 и 12 из этого списка рассматриваются в контекст нового приложения под названием Enroute. Приложение Enroute, по сути, использует API, которое доступно в интернете от трекера рейсов компании FlightAware, и вы можете видеть на своем UI текущую информацию о рейсах, определяемых некоторым фильтром, в котором задается аэропорт прибытия destination, аэропорт отправления origin, авиакомпания airline и находятся ли рейсы уже в воздухе или еще ожидают вылета на земле inTheAir.

    Особенно впечатлила Лекция 12, посвященная Core Data, куда мы загружаем оперативную информацию о рейсах Flight, аэропортах Airport и авиакомпаниях Airline с трекера полетов FlightAware, а затем выводим её на наш UI.
    Core Data превосходно научилась „играть“ на поле „реактивности“ SwiftUI.

    Есть две основные точки интеграции SwiftUI с базой данных Core Data.

    Одна из этих точек — @ObservedObject. Объекты, которые мы создаем в базе данных, являются @ObservableObject. По существу, это миниатюрные ViewModel,  которые мы можем использовать для индивидуальных объектов наподобие Flight, Airline на случай, если они изменились, так как мы посылаем objectWillChange.send() при их изменении.  Но iOS 14 и SwiftUI 2.0 пошли ещё дальше — они сделали объекты Core Data Identifiable, что позволяет использовать их без каких-либо затруднений в таких SwiftUI конструкциях, как ForEcEach и List.

    Второй точкой интеграции SwiftUI и Core Data является @FetchRequest, который динамически всегда показывает то, что находится в базе данных, это действительно “сердце” интеграции SwiftUI и Core Data. Это маленький „шедевр“, изобретенный Apple для работы своей эффективной базой данных Core Data с декларативным SwiftUI. Это НЕ одноразовая выборка данных, когда выбрал и получил определенный результат. @FetchRequest постоянно выполняет выборку, так что ваш UI всегда будет показывать то, что в данный момент находится в базе данных, а она действительно постоянно меняется, так как самолеты летят и их время прибытия в аэропорт назначения все время меняется. 
    Всё это часть декларативной “природы” SwiftUI.



    По ходу дела Пол Хэгерти продемонстрировал очень комфортную работу c Core Data ещё и за счет того, что придал объектам Core Data дополнительная функциональность с помощью „синтаксического сахара“ в их расширении extension. НЕ-Optional переменные vars сделаны вычисляемыми, и „взаимосвязи“ объектов типа „one to many“ (»один-ко многим") или «many to many»(«многие-ко многим») представлены в виде Swift множеств типа множества рейсов Set.

    Отдельного упоминания требует способ хранения DocumentGroup данных, воспринимаемых пользователями как «документы», а именно таким и является наше приложение EmojiArt. Профессор является большим поклонником приложений, основанных на документах (Document Based App) и сильно сожалел о том, что в iOS 13 в SwiftUI не было возможности работать с UIDocument .
    И, о чудо! В  iOS 14 и SwiftUI 2.0 появляется «обертка» UIDocument в виде DocumentGroup , которая способна превратить EmojiArt в много-документное приложение, которое работает как “родное” приложение в среде Files как на iOS, так и на Mac. И профессор предоставляет в наше распоряжение уже после окончания курса великолепный вариант приложения EmojiArt на основе DocumentGroup , который работает в Xcode 12. Надо сказать, что в интернете практически отсутствует информация  о полноценном применении DocumentGroup, так что это просто подарок тем, кто создает приложения, основанные на «документах».

    Если вы — уже действующий разработчик iOS приложений и у вас есть куча UIKit кода, то, конечно, вы хотели бы использовать его при переходе на SwiftUI, и специальный API позволит вам произвести интеграцию UIKit компоненты в SwiftUI. Об этом рассказывается на заключительной Лекции 14. Там рассматриваются два демонстрационных примера.

    Один демонстрационный пример усовершенствует приложение Enroute так, что позволяет в фильтре FilterFlights выбирать аэропорт назначения destination прямо с карты Map, UIKit карты с именем MKMapView.
    Второй демонстрационный пример в приложении EmojiArt к таким уже существующим способам получения фонового изображения, как “перетягивание” (dragging) или “копирование (copy) и вставка (paste), добавляет еще один способ — получение “картинки” непосредственно с фотокамеры (camera) устройства или извлечение фото из библиотеки фотографий (photo library).
    В  iOS 14 и SwiftUI 2.0 появился «родной» SwiftUI Map, но пока он значительно уступает по функциональности MKMapView, так что остаемся при своих.

    В Xcode 12.2 ( Swift 5.3) существенно улучшился декларативный язык представления Views в SwiftUI (DSL):

    1. нет необходимости в явном .self, если нет семантики «захвата»,
    2. так как Views напрямую наследуют атрибут @ViewBuilder из самого протокола View, то можно не добавлять @ViewBuilder к var body (как это делал Пол Хэгерти) при использовании условий,
    3. внутри body можно использовать не только if else, но и if let и switch,
    4. можно использовать множество «хвостовых» замыканий,
    5.появился main атрибут для точки входа в приложение

    Это упростит код SwiftUI и сделает его более понятным.

    В заключение хочу отметить, что Стэнфордский курс CS193P Весна 2020 достаточно сложный, с огромным количеством кода и нюансами, информацию о которых вы вряд ли сможете где-нибудь ещё найти. Но если вы «прорвётесь», то вы безусловно оцените «красоту» демонстрационного кода Пола Хэгерти. Это обязательно получится, если вы вслед за профессором будете программировать в Xcode, а не только смотреть на то, как это делают другие.

    Материалы курса на английском можно посмотреть на сайте CS193p — «Developing  Application for iOS» . Русскоязычный конспект находится здесь.

    P.S. Комментарий от Пола Хэгерти на сайте курса.

    Материал, представленный в этом курсе, не разрабатывался с участием и не проверялся кем-либо из Apple, поэтому его не следует воспринимать как «истину в последней инстанции» по поводу того, как разрабатывать приложения с использованием SwiftUI. Наша команда сделала все возможное, чтобы самим понять эту технологию за короткое время ее появления, а затем поделиться тем, что узнали, с вами.

    Комментарии 2

      0
      Интересно, как обертка (кажется @Published?) под капотом обеспечивает постоянную актуальность модели. Не может же она непрерывно дергать APIs и отслеживать изменения? Или если так, то как быть в условиях ограничений на количество запросов / возрастет ли нагрузка на сервера?
        0
        Если вы имеете ввиду запросы к трекеру полетов FlightAware, то там работает таймер:
        fetchTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false, block: { [weak self] timer in
                        if (self?.fetchInterval ?? 0) > 0 || (self?.fetchSequenceCount ?? 0) > 0 {
                            self?.fetch()
                        }
                    })

        Вы сами задаете интервал обновления.
        А в fetch() уже работает @Published, а точнее CurrentValueSubject<Set, Never>([]), но это близко к @Published, и это воздействует на View SwiftUI.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое