Начало работы со списками SwiftUI после WWDC 21

Списки - один из наиболее широко используемых компонентов в современных приложениях. Они позволяют приложениям отображать несколько строк данных в столбце. В UIKit списки можно разрабатывать с использованием UITableView, и разработчикам приходится писать много кода для реализации списков. Однако в SwiftUI списки можно легко реализовать с помощью List view. Благодаря List разработчики теперь могут:

  • Реализуйте списки с более коротким и понятным кодом.
  • Придайте своим спискам лучший вид и удобство с меньшими усилиями.
  • Настройте каждую строку так, чтобы она содержала различные виды представлений, например Image, Text, Button и т. Д.
  • Используйте тот же код для расширения приложения на другие платформы Apple, такие как iPadOS и macOS.

И что самое интересное, WWDC21 объявила о множестве функций, интегрированных в List, что делает его не только более удобным для пользователя, но и более удобным для разработчиков. По мере продвижения по этому руководству вы познакомитесь с такими функциями, как:

  • Поддержка Markdown в Text представлении.
  • Модификатор .formatted, простой и понятный способ форматирования даты и времени.
  • Модификатор .refreshable для обновления списка.
  • .searchablemodifier для включения поиска.
  • .swipeActions in List.
  • Использование .headerProminence, чтобы сделать заголовки List разделов более заметными.
  • Использование .listRowSeparator и .listRowSeparatorTint для изменения видимости и цвета разделителей строк List соответственно.

Примечание. Вам понадобится Xcode 13. Это руководство было написано с использованием бета-версии 5. Чтобы запустить этот проект на устройстве iOS, оно должно работать под управлением бета-версии iOS 15.

Начиная

Загрузите стартовый проект из этого репозитория GitHub.

Скомпилируйте и запустите стартовый проект. Вы увидите экран приложения FavMovies, на котором отображаются мои любимые фильмы, как показано на скриншоте ниже:

Расстроенный? Не волнуйтесь, впереди много изменений.

Теперь посмотрим на структуру папок. Помимо стандартных файлов и папок, вы увидите пять папок: Extensions, Helpers, JSON Files, Models и Views.

Extensions содержит расширение Bundle, которое обрабатывает декодирование JSON. Вы будете использовать его для декодирования файлов JSON JSON Files.

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

Точно так же Models содержит Movie.swift и Genre.swift, которые вы будете использовать в качестве моделей для фильмов, которые будут перечислены в FavMovies.

Кроме того, Views содержит представления приложения. Сейчас он содержит только ContentView.swift.

Составьте свой список

Пришло время составить список фильмов и отобразить его в FavMovies. Откройте Movie.swift в Models, чтобы найти модель Movie. Он соответствует Identifiable, поскольку вы будете использовать его для создания списка, в котором каждому Movie значению присваивается уникальный идентификатор с использованием своего id. Он также соответствует Codable, поскольку вы будете использовать эту модель для декодирования JSON.

Наряду с id вы увидите другие свойства, такие как name, desc, releaseDate и genre в Movie.

genre относится к типу Genre, который является enum, соответствующим String, CaseIterable и Codable, и имеет несколько вариантов.

Настройка источника данных для вашего списка

Если вас беспокоит мысль о создании данных фильма, у начинающего есть эта проблема. В JSON Files вы увидите movies.json, который вы будете использовать для создания данных о фильмах.

Откройте ContentView.swift в Views. Создайте состояние с именем movies в ContentView, используя строку, приведенную ниже:

Приведенный выше код создает переменную состояния movies и сохраняет данные фильма, сгенерированные путем декодирования данных JSON в movies.json с использованием decode(_:from:) Bundle. На этом вы завершили настройку источника данных для списка.

Отображение данных в вашем списке

Пришло время отобразить сведения о фильмах в списке, используя только что настроенный источник данных.

Откройте ContentView.swift и замените строку Text(“My Favorite Movies”) в body из ContentView следующим текстом:

Приведенный выше код создает List путем перебора элементов в movies. Параметр movie фиксирует значение каждой итерации, а представление Text в закрытии List обращается к name из movie для отображения имени фильма.

Выполните сборку и запустите, чтобы увидеть экран, подобный приведенному ниже снимку экрана:

FavMovies теперь отображает список, содержащий названия некоторых фильмов.

Примечание. До WWDC21 List нуждался в аргументе id, чтобы иметь возможность перебирать элементы массива.

Но в приведенном выше коде вы использовали синтаксис, введенный после WWDC21, который устранил необходимость передавать аргумент в id.

Дополнительная информация о фильме, пожалуйста.

Вы успешно отобразили названия фильмов в FavMovies. Почему бы вам не сделать список более информативным, указав описание и дату выхода фильмов?

Отображение дополнительной информации в каждом элементе списка

Поскольку вам потребуется больше представлений для отображения дополнительной информации в строках, неплохо было бы создать отдельное представление, чтобы весь код не загромождался только ContentView.

Итак, создайте новое представление SwiftUI, MovieListView.swift в Views. В этом представлении будет отображаться список, содержащий имя, описание и дату выпуска каждого фильма. Для этого требуются данные, и вы должны настроить MovieListView для получения данных из ContentView.

В MovieListView добавьте переменную привязки, используя приведенный ниже код:

Как только вы добавите строку, указанную выше, MovieListView_Previews покажет вам сообщение об ошибке:

В вызове отсутствует аргумент для параметра "фильмы".

Замените MovieListView() следующей строкой кода:

Поскольку MovieListView имеет привязку, вы задали ему постоянное значение [Movie], вызвав PreviewMovieGenerator.getPreviewMovie() и сохранив его в массиве. Теперь ошибка должна исчезнуть.

Необходимая настройка почти завершена. Замените Text(“Hello, World!”) в MovieListView следующим кодом:

Данный код создает List, каждая итерация которого содержит VStack, содержащий Text представления, отображающие имя, описание и дату выпуска каждого фильма. Кроме того, пара Spacer представлений используется для увеличения промежутка между Text представлениями.

Теперь замените представление List и его содержимое в ContentView следующим:

Код вызывает инициализатор MovieListView и передает ему привязку $movies, так что ContentView действует как единственный источник истины для MovieListView. Таким образом, если данные переменной привязки movies MovieListView изменяются, они также отражаются в переменной состояния movies ContentView.

Теперь тело ContentView должно выглядеть следующим образом:

Строй и беги. Вы увидите экран со списком, отображающим название, описание и дату выпуска различных фильмов, как на скриншоте ниже:

Украшение элементов списка с помощью Markdown и форматирования даты

Хотя в FavMovies теперь отображаются названия фильмов, описания и даты выпуска, из-за отсутствия надлежащего форматирования текста список не выглядит приятным для глаз. Кроме того, поскольку нет правильного форматирования даты, вы можете увидеть ненужные детали в дате выпуска. Здесь на помощь приходит поддержка Markdown в Text представлениях и модификатор .formatted.

WWDC21 объявила о встроенной поддержке SwiftUI для рендеринга Markdown. Он включает поддержку полужирного шрифта, курсива, ссылок, зачеркивания и моноширинного форматирования, а также прост в использовании. Вы можете просто включить синтаксис Markdown в свое Text представление и отформатировать текст соответствующим образом.

Например:

Приведенный выше код создает VStack с Text представлениями, содержащими тексты с синтаксисом Markdown. Результат, который он дает, показан на скриншоте ниже:

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

Теперь приступим к кодированию. Откройте MovieListView.swift и замените VStack и его содержимое следующим кодом:

Приведенный выше код создает VStack, содержащий Text представлений с синтаксисом Markdown. В первом Text виде название фильма выделено жирным шрифтом, а во втором - его описание выделено курсивом.

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

Поддержка Markdown в Text представлениях упростила и ускорила форматирование текста. Однако есть проблема - дата выхода. Когда дело доходит до даты выпуска, достаточно только отображения месяца, дня и года. Итак, вы должны избавиться от ненужного с помощью .formatted (а не DateFormatter, что является более длительным процессом).

Использовать .formatted просто. Просто рассмотрите приведенный ниже код:

В приведенной выше строке представление Text отображает текущую дату, отформатировав ее с помощью модификатора .formatted Date. .formatted принимает FormatStyle в качестве аргумента, а в приведенном выше коде FormatStyle равно dateTime. Затем осуществляется доступ к year(), day() и month() из dateTime, и, следовательно, результат, который вы получаете, показан ниже:

Отлично, вы отформатировали дату в одну строку кода. Прошли те времена, когда мы использовали DateFormatter и писали несколько строк кода, чтобы получить желаемый формат даты.

В настоящее время название месяца указано в краткой форме, но что, если вы хотите отобразить его полную форму?

Просто передайте аргумент .wide в month(). month() имеет параметр format типа Date.FormatStyle.Symbol.Month, и значение этого параметра по умолчанию - .abbreviated. Передавая .wide, вы явно заявляете, что хотите получить широкую версию месяца. Попробуйте код, приведенный ниже:

В данном коде аргумент .wide передается в month(). Таким образом, результат будет выглядеть как на скриншоте ниже:

Таким образом вы можете отобразить полное название месяца.

Чтобы реализовать это в FavMovies, откройте MovieListView.swift и замените Text представление, отображающее дату выпуска, следующим кодом:

Данный код форматирует дату с использованием .formatted и отображает только год, день и месяц (в полной форме).

Строй и беги. Вы увидите результат, аналогичный приведенному ниже снимку экрана:

Следовательно, FavMovies отображает список фильмов с улучшенным текстом и датой.

Добавление "крутого" обновления по запросу

Если вам нравится функция "потянуть для обновления", то вам понравится этот раздел, потому что вы узнаете, как ее реализовать. Кроме того, простота его реализации может поразить вас, потому что все, что вам нужно, - это модификатор .refreshable(action:) в вашем List.

Параметр action - это асинхронное действие, которое SwiftUI выполняет, когда пользователь вытягивает список вниз в своем приложении iOS или iPadOS. Вы используете этот параметр для загрузки новых данных, и во время загрузки новых данных SwiftUI отображает индикатор обновления вверху списка. Легко, правда?

Первое, что нужно настроить - это механизм загрузки новых данных. Если вы откроете JSON Files, вы увидите другой файл JSON с именем more_movies.json, содержащий массив данных о новом фильме. Вы расшифруете этот файл, чтобы сгенерировать новые дополнительные данные.

Перейдите к ContentView.swift и создайте refreshMovieList(), используя приведенный ниже код:

Данный код создает refreshMovieList(), который добавляет два массива, сгенерированных декодированием more_movies.json и movies.json, и сохраняет результат в movies.

После этого в ContentView ниже .navigationTitle(“My Favorite Movies”) добавьте следующий код:

Вы применили модификатор .refreshable(action:) к MovieListView, поскольку его body возвращает List, который вы хотите обновить. Таким образом, refreshMovieList() вызывается из action закрытия .refreshable(action:), когда выполняется обновление по запросу.

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

Вперед! Вы реализовали функцию "потяните для обновления" в FavMovies.

Время реализовать поиск

Поиск - одна из самых важных функций в приложениях, отображающих большой объем информации, и SwiftUI значительно упростил ее реализацию.

Использование .searchable(text:placement:prompt:suggestions:) позволяет легко отображать и выполнять поиск. .searchable(text:placement:prompt:suggestions:) имеет параметр text, который является String привязкой, которая получает поисковый запрос, вводимый пользователем.

Точно так же вы можете использовать параметр placement, значение которого по умолчанию - .automatic, чтобы явно указать, где вы хотите отображать поле поиска. Вы можете отобразить его на панели навигации, боковой панели или панели инструментов явно, используя .navigationBarDrawer, .sidebar и .toolbar соответственно. Не стесняйтесь экспериментировать, запуская код на разных платформах и добавляя модификатор .searchable в различные представления в вашем приложении.

Однако установки .automatic достаточно в большинстве случаев, поскольку платформа, на которой работает приложение, автоматически определяет его размещение. По умолчанию в iOS, iPadOS и macOS поле поиска размещается на панели инструментов, а в tvOS и watchOS поле поиска размещается внутри его содержимого.

Есть еще один параметр prompt в .searchable(text:placement:prompt:suggestions:), который отображает текст подсказки для пользователя. Он принимает значение String и не является обязательным.

Наконец, вы можете дополнительно использовать параметр suggestions для отображения поисковых предложений, когда пользователь начинает поиск.

Отображение панели поиска

Показать строку поиска очень просто. Добавьте модификатор .searchable к любому виду, который вы хотите отобразить.

Вам необходимо обязательно передать привязку к параметру text .searchable. Итак, откройте ContentView.swift и создайте переменную состояния searchText, используя приведенный ниже код:

Приведенный выше код инициализирует searchText пустым String. Затем поместите .searchable в NavigationView из ContentView, добавив следующую строку кода после закрывающей фигурной скобки NavigationView:

Таким образом, вы добавили поле поиска с привязкой к searchText в ContentView. Когда пользователь начинает вводить текст в поле поиска, введенный текст сохраняется в searchText.

Создайте и запустите, чтобы найти панель поиска, добавленную в FavMovies.

На показанной записи экрана вы можете увидеть интерактивную панель поиска, добавленную в приложение, просто поместив модификатор .searchable.

Как заставить поиск работать

До сих пор вы добавляли панель поиска в FavMovies, но она еще не работает. Пора заставить это работать.

Откройте MovieListView.swift и добавьте привязку searchText, используя приведенный ниже код:

Вы будете использовать привязку searchText, созданную выше, для привязки к searchText из ContentView. Следовательно, текст, который пользователь вводит во время поиска, также будет передан этому searchText.

Как только вы добавите привязку, MovieListView_Previews попросит вас вставить аргумент параметра searchText. Исправьте это, заменив MovieListView(movies: .constant([PreviewMovieGenerator.getPreviewMovie()])) следующим:

В коде вы передали постоянное пустое значение String параметру searchText инициализатора MovieListView в его предварительном просмотре.

Но вы все равно увидите еще одну ошибку. Создайте проект, если вы его не видите. В ContentView для инициализации MovieListView требуется аргумент в параметре searchText. Замените MovieListView(movies: $movies) следующим:

Вы передали привязку searchText к MovieListView из ContentView в коде, показанном выше. Следовательно, searchText из MovieListView теперь может получать поисковые запросы, которые пользователь вводит в строку поиска.

Приложение должно знать, когда оно должно начать показывать результаты поиска вместо обычного списка фильмов - очевидно, когда пользователь нажимает на строку поиска и начинает печатать. К счастью, SwiftUI позволяет узнать, когда это произойдет, с помощью переменной среды isSearching. Добавьте следующую строку под объявлением searchText в MovieListView:

Написанный вами код позволяет узнать, ищет пользователь или нет. Если пользователь выполняет поиск, вы увидите представление со списком результатов поиска или представление со списком всех фильмов. Таким образом, у вас будет два разных представления.

В MovieListView создайте movieRow(movie:), используя приведенный ниже код:

movieRow(movie:) возвращает VStack, идентичный таковому в List представлении body из MovieListView.

Теперь в MovieListView создайте переменную searchResults, используя следующий код:

searchResults - вычисляемое свойство, которое фильтрует и возвращает все фильмы, имена которых содержат значение searchText.

Теперь, чтобы отобразить результаты, возвращаемые searchResults, вам необходимо настроить MovieListView, заменив его body следующим:

Давайте рассмотрим это шаг за шагом.

  1. Вы создали List, содержащий if-else блок.
  2. Если пользователь коснулся строки поиска (isSearching это true) и если пользователь что-то там набрал (searchText не пусто), код отображает результаты поиска с помощью ForEach итерации по searchResults и отображения представления, возвращенного movieRow(movie:).
  3. Однако, если пользователь не выполняет операцию поиска, вводя какие-либо данные, код отображает представление, возвращенное movieRow(movie:), выполнив от ForEach до movies.

Примечание. Вы использовали ForEach блок в List, потому что использовали одно и то же List представление для отображения результатов поиска, а также всех фильмов. Следовательно, вам нужно ForEach для перебора данных.

Кроме того, без использования ForEach в List функция смахивания не работает. Вы научитесь применять его позже в этой статье.

Собери и запусти, чтобы увидеть, как работают поисковые системы.

Ура, вы реализовали поиск в FavMovies.

Удалить? Просто проведите пальцем влево.

Если вы использовали приложение Apple Notes на iOS или iPadOS, вы, должно быть, заметили, что оно предлагает определенные параметры, такие как закрепление, удаление и т. Д., Когда вы проводите пальцем влево или вправо по заметке в списке.

Итак, не будет ли интересно, если вам удастся реализовать эту функцию в FavMovies?

Для реализации действий смахивания SwiftUI предлагает модификатор .swipeActions. У него три параметра: edge, allowsFullSwipe и content.

Используя параметр edge, значение по умолчанию которого HorizontalEdge.trailing, вы можете определить край представления, с которым вы хотите связать действия смахивания.

Установка для параметра allowsFullSwipe значения true дает возможность выполнить первое действие с полным смахиванием.

А параметр content - это место, где вы пишете внешний вид действия смахивания.

Реализация смахивания для удаления

Теперь вы создадите свайп для удаления функции в FavMovies. Откройте MovieListView.swift и добавьте .swipeActions после закрывающей фигурной скобки VStack из movieRow(movie:), используя следующий код:

Давайте рассмотрим это шаг за шагом.

  1. Вы добавили модификатор .swipeActions с параметром allowsFullSwipe, установленным на true.
  2. В параметр content вы передали Button с role, установленным на .destructive (придавая ему красный цвет), его action настроен на удаление фильма из movies, id которого совпадает с id текущего movie, а его label показывает значок корзины с текст, Delete.

Выполните сборку и запустите, чтобы увидеть, как провести пальцем по экрану для удаления.

Поздравляем, в FavMovies вы реализовали смахивание для удаления. Поскольку вы не передали аргумент параметру edge, действие смахивания выполняется на конце.

Выделение заголовков разделов

SwiftUI позволяет создавать списки с разделами и их заголовками, чтобы ваши списки выглядели четко разбитыми по категориям. Эти заголовки разделов нельзя было изменить, пока WWDC21 не представил модификатор .headerProminence, который может принимать аргумент .increased. Он также может принимать .standard аргумент, но .increased увеличивает размер шрифта и вес заголовков, делая их выделяющимися.

Отображение заголовков разделов

Откройте MovieListView.swift и создайте переменную movieListWithGenres, используя следующий код:

Давайте разбираться в коде поэтапно.

  • Вы создали movieListWithGenres переменную.
  • ForEach проходит через все случаи Genre enum, что стало возможным благодаря его соответствию с CaseIterable.
  • В каждой итерации случаев Genre создается раздел, и его заголовок устанавливается на String rawValue жанра. Следовательно, строка Text(genre.rawValue) устанавливает заголовок раздела.
  • Создается еще один блок ForEach, который проходит через movies. Каждая итерация сохраняется в movie.
  • Если genre из movie совпадает с текущим genre, фильм помещается в раздел этого жанра.

Теперь замените блок ForEach и его содержимое в блоке else из body из MovieListView на movieListWithGenres, чтобы body выглядел как код, приведенный ниже:

Строй и беги. Приложение будет выглядеть так:

FavMovies теперь отображает список фильмов, сгруппированных по жанрам.

Изменение заметности заголовка

В настоящее время заголовок незаметен. Итак, необходимо это изменить. Откройте MovieListView.swift. В его body после переменной movieListWithGenres в блоке else добавьте следующую строку:

Вы добавили модификатор .headerProminence с аргументом .increased в movieListWithGenres.

Выполните сборку и запустите, чтобы увидеть экран, похожий на запись экрана ниже:

FavMovies отображает список, содержащий заголовки с повышенной заметностью. Благодаря этому вы сделали свои заголовки заметными.

Игра с разделителем строк

В FavMovies вы, должно быть, заметили горизонтальные линии между строками разделов в списке фильмов. Они называются разделителями строк, а в WWDC21 введены .listRowSeparator() и .listRowSeparatorTint(), чтобы разработчики могли отображать или скрывать их и изменять их цвета соответственно.

Если вы любите играть с цветами, сейчас ваше время. Вы воспользуетесь .listRowSeparatorTint(), чтобы изменить цвет разделителей строк. Откройте MovieListView.swift и после закрывающей фигурной скобки .swipeActions в movieRow(movie:) добавьте следующую строку, чтобы придать красный цвет разделителям строк:

Вы закрасили разделители строк в красный цвет. Скомпилируйте и запустите, чтобы увидеть результат:

Вы также можете использовать другие Color значения в .listRowSeparatorTint().

Но если вы не большой поклонник этих разделителей строк, вы можете скрыть их, используя .listRowSeparator(.hidden), где .hidden - один из случаев Visibility enum. Другие случаи включают .visible и .automatic.

Чтобы скрыть разделители строк в FavMovies, откройте MovieListView.swift и после закрывающей скобки .swipeActions в movieRow(movie:) добавьте следующую строку:

Вы просто спрятали разделители строк. Выполните сборку и запустите, чтобы увидеть, что разделители строк исчезли.

Таким образом, вы экспериментировали с видимостью и цветом разделителей строк вашего списка в FavMovies.

Подводить итоги

Из этого руководства вы узнали основы списков SwiftUI. Попутно вы реализовали:

  • Добавление поддержки Markdown в Text view.
  • Использование .formatted для простого и удобного форматирования даты и времени.
  • Обновление списка с использованием .refreshable.
  • Реализация поиска с использованием .searchable.
  • Добавление .swipeActions в List.
  • Использование .headerProminence, чтобы List заголовки разделов привлекали внимание.
  • Играем с .listRowSeparator и .listRowSeparatorTint, чтобы изменить видимость и цвет разделителей строк для List соответственно.

Если вы хотите скачать готовые файлы проекта, пройдите, пожалуйста, в этот репозиторий.

Удачного кодирования!