Русский неавторизованный конспект лекций Стэнфордского университета " Разработка iOS приложений" 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 и 2023 гг., сопровождаемый решениями заданий и дополнениями, связанными с адаптацией курсов к новым версиям Swift, Objective-C и iOS.
Ниже представлен небольшой фрагмент Лекции 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:
Ниже представлен фрагмент Лекции 8 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 8 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступныздесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
. . . . . . . . . . . . . .
Итак, я говорил вам в начале семестра, что я постараюсь научить вас всему на этом курсе как минимум три раза. Один раз я расскажу вам об этом на слайдах, потом я покажу это вам на демонстрационном примере, а потом вы сами сделаете это в своем Домашнем Задании. И это в большой степени верно для анимации. Итак, вот та часть, где я рассказываю вам про анимацию, то есть какие бывают анимации, как это работает и так далее. Начнем с фундаментальных основ анимации: что нужно знать, чтобы действительно понять, что такое анимация. Главное, что нужно понять, это то, что анимация просто показывает изменения в вашей модели Model с течением времени. Вот и все.
Анимация показывает вам ИЗМЕНЕНИЯ с течением времени, и эти изменения отражаются через аргументы модификаторов ViewModifier и, очевидно, что геометрические фигуры Shape могут меняться.
И еще, я выделил это в отдельную вещь “Переходы” (transitions), но вы увидите, что на самом деле это всего лишь первый вариант, то есть когда Views приходят на экран и уходят с экрана, то работает пара модификаторов ViewModifier. Когда View появляется на экране, вы хотите, чтобы он “проявлялся” постепенно. Когда View уходит за пределы экрана, вы хотите, чтобы он «улетал» или постепенно «растворялся» или что-то вроде этого. Итак, это три вида изменений, которые происходят, и которые мы пытаемся анимировать. И теперь, пожалуй, самая важная строка на этом слайде.
Анимация показывает вам изменения, которые уже произошли, и анимация просто показывает их вам растянутыми во времени.
Людям, которые не занимались асинхронным программированием или чем-то подобным, нужно привыкнуть к этому. Вам хочется думать, если у вас есть анимация, длящаяся в течение 5 секунд, например, анимация непрозрачности opacity или что-то в этом роде, что переменная var, которая устанавливает непрозрачность opacity как-то меняется со временем, но это не так. Когда вы устанавливаете непрозрачность opacity для View на 1 от 0, то View постепенно «проявляется» (fade in) в течение 5 секунд, но непрозрачность opacity для вашего Viewмгновенно изменилась на 1для всего вашего View. Просто система анимация показала пользователю это изменение в течение 5 секунд.
Итак, все изменения, которые стоят за анимациями, которые вы видите, произошли мгновенно, a вы их видите растянутыми во времени. Как только вы поймете это и отложите в своей голове, то сможете вложить гораздо больше смысла в написание кода анимации, который будет правильно работать.
Таким образом, модификаторы ViewModifiers — это основные агенты изменений в пользовательском интерфейсе (UI). Большинство вещей, которые меняются, например непрозрачность opacity, соотношение сторон aspectRatio, даже Views, летающие вокруг, это просто модификатор ViewModifier position. Есть такой модификатор ViewModifier, который я вам еще не показывал, он называется position, и такие контейнеры Views, как HStack и LazyVGrid, используют этот модификатор position для размещения своих Views. Когда Views “летают” вокруг, это просто потому, что аргумент в их модификаторе позиция position меняется. Итак, модификаторы ViewModifiers — это действительно то, что заставляет всё двигаться и анимировать, и вы увидите это в демонстрационном примере, который мы сделаем.
Ниже представлен фрагмент Лекции 7 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 7 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны здесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
Что мы делаем сегодня?
. . . . . . . . . .
Одна из анимаций, которые мы собираемся сделать в ближайшее время, это небольшой таймер обратного отсчета. И это действительно воодушевляет вас быстрее выбирал карты, потому что вы получаете больше очков. Чем больше отсчитывается времени до выбора вами карты, тем меньше очков вы получите к моменту совпадения карт. Итак, давайте посмотрим, как это происходит. Обратите внимание на таймер обратного отсчета. Если я позволю этому таймеру идти до конца, то я не получу много баллов, даже если произойдет совпадение карт. Если я переверну карту обратно, то таймер останавливается.
Вы видели, как взлетела маленькая цифра +2? Это говорит о том, сколько я получил баллов. Позвольте мне еще раз попытаться получить больше очков. Теперь хорошо, количество очков +13.
Демонстрационный пример Shape
Итак, мы собираемся создать этот маленький круглый “пирог” Pie, который ведет обратный отсчет. Мы не будем анимировать его сегодня, но мы создадим свою собственную геометрическую фигуру Shape в виде маленького “пирога”.
Для того, чтобы мы могли увидеть, что здесь происходит, я собираюсь измениться количество карт, которые у меня есть в игре, до четырех карт:
И мы положим все наши карты “лицом” вверх, потому что мы собираемся разместить там “пирог” Pie:
Давайте начнем с нашего CardView, который вы видите здесь на экране. Я размещу там круг Circle ( ) вместо “пирога” Pie:
И это ZStack. Они сгруппированы с помощью Group, но это все еще ZStack И я просто помещаю Circle() позади текста Text. Мы почти закончили, правда? Это не “пирог” Pie, а круг Circle(), и он имеет ярко-оранжевый цвет, пожалуй, слишком яркий, ведь таймер не так уж и важен. Давайте к нашему кругу Circle() добавим немного непрозрачности .opacity ( 0.5 ):
Можно было бы снизить до .opacity ( 0.4 ):
Видите, круг стал немного более прозрачным. Еще одна вещь, которая мне не нравится, это то, что мой “пирог” Pie подходит прямо к краю карты. Это не так уж хорошо.
Ниже представлен фрагмент Лекции 6 Стэнфордского курса CS193P Весна 2023«Разработка iOS приложений с помощью SwiftUI«. Полный русскоязычный неавторизованный конспект Лекции 6 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны здесь. Код находится на GitHub.
С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.
На сегодняшней Лекции 6, a, по сути, на всей этой неделе мы подберем несколько тем и попытаемся понять больше о том, как то, что мы делали, действительно работает.
Первая тема — это Layout, то есть расположение Views на экране. Как место на экране распределяется между всеми Views с помощью HStack, LazyVGrid, ScrollView и всех других подобных вещей? Как они решают, кому какое место достанется? Затем у нас будет небольшая демонстрация.
Аналогичная ситуация с @ViewBuilder. У нас есть есть такая классная штука как @ViewBuilder. Мы знаем, что это просто функция, которая возвращает someView. Но мы также знаем, что это круто и что это список Views, там есть if else, if let, switch и локальные переменные. Так как же работает @ViewBuilder? Я не буду говорить о том, как на самом деле реализован @ViewBuilder, но я собираюсь поговорить о том, как вы его используете и как он работает.
Расположение на экране: Layout.
Как пространство на экране назначается и распределяется между всеми Views, которые там появляются? И это на самом деле это невероятно элегантная маленькая система и очень-очень простая, но в то же время с очень строгими правилами. Это делает расположение Views настолько невероятно предсказуемым, почти 100% предсказуемым.
Работа системы Layout состоит из 3-х шагов:
Первый шаг состоит, конечно, в том, что Views предоставляется некоторое пространство. Например, вашему ContentView, который находится в самом верху вашего приложения, доступен весь экран. Это отправная точка. Но как только вы начнете создавать HStacks и VGrids, вам предлагают все меньше и меньше места по мере того, как вы спускаетесь вниз по иерархии Views.Контейнерам Views, таким как HStack или VGrid или другим подобным им контейнерам, предлагается определенное пространство, a они в свою очередь предлагают его своим “детям”-Views с помощью определенных алгоритмов.
Затем выполняется второй шаг, крайне важный и все дело именно в нём, он заставляет всю систему Layout действительно работать, потому что после того, как Views предложат пространство, даже если это обычный текст Text или изображение Image или что-то еще, ОНИ ВЫБИРАЮТ, какого размера они хотят быть. Единственный, кто может выбрать размер View — это само View. Невозможно принудительно указать размер View, ОНИ сами выбирают свой размер в зависимости от предложенного им пространства. Мы собираемся поговорить о том, как они это делают, но это НЕРУШИМОЕ ПРАВИЛО: Views ВЫБИРАЮТ свой собственный размер. И именно это во многом делает систему Layout полностью предсказуемой, у вас не бывает странных случаев, когда вы действительно не понимаете, что произойдет. Никто никому ничего не навязывает.
Затем, на третьем шаге, как только Views выбрали свои размеры, они хотят вернуться в контейнер Views, который предлагал им пространство, и быть размещенными внутри него. Теперь, когда контейнеры Views знают все размеры, они могут размещать все вещи внутри себя, Например, HStack размещает Views, сдвигая их вправо или влево. То есть позиционирование Views полностью зависит от HStack. Таким образом, размеры Views выбираются самими Views, а позиционирование осуществляется контейнерами Views.
Так работает система Layout. И это всегда работает таким образом, и у этого есть действительно хороший маленький протокол protocolLayout для исполнения этого «танца». Я покажу, как выглядит этот протокол, когда мы доберемся до демонстрационного примера.