![](https://habrastorage.org/getpro/habr/upload_files/b66/c82/cc1/b66c82cc1101eeb0fe2f20af6dfdbfb5.png)
Разбираемся с List, ForEach and Identifiable
В UIKit для iOS один из наиболее часто используемых элементов управления пользовательским интерфейсом - это UITableView. Если вы имеете опыт разработки приложений с использованием UIKit, то знаете, что table view предназначен для отображения списков данных. Этот элемент управления пользовательским интерфейсом широко используется в приложениях ориентированных на контент, например, в новостных приложениях или в принципе в любых популярных приложениях, таких как Instagram, Twitter, Reddit и другие.
В SwiftUI для этой цели используется List, а не UITableView. Если вы раньше занимались созданием табличных представлений с помощью UIKit, вы знаете, что это может потребовать некоторого времени и усилий, особенно если вам нужна кастомная ячейка. SwiftUI упрощает этот процесс и позволяет создавать табличные представления всего лишь в несколько строк кода. Даже если вам нужны кастомные ячейки, это не потребует много времени и усилий. Не волнуйтесь, если сейчас это кажется сложным. Вы поймете, как это работает, увидев примеры. В этой части мы начнем с простого списка и постепенно перейдем к более сложным макетам.
Создание простого списка
Давайте начнем с создания простого списка. Откройте Xcode и создайте новый проект, используя шаблон «App». Задайте имя продукта "SwiftUIList" (или любое другое имя, которое вы предпочитаете), и заполните все необходимые значения.
![](https://habrastorage.org/getpro/habr/upload_files/676/a0b/133/676a0b133bee23735c195a586c3c6353.png)
Важно, чтобы опция "SwiftUI" была выбрана для интерфейса. После создания проекта Xcode сгенерирует стартовый код в файле ContentView.swift. Найдите объект текста "Hello World" и замените его следующим кодом:
struct ContentView: View {
var body: some View {
List {
Text("Первый")
Text("Второй")
Text("Третий")
Text("Четвертый")
}
}
}
![](https://habrastorage.org/getpro/habr/upload_files/25e/9fa/12f/25e9fa12f3ae9d55429e49cafcee4898.png)
Вот и все что вам нужно для создания простого списка или таблицы. Если поместить текстовые представления внутри List, то представление списка будет отображать данные в виде строк. В данном случае каждая строка содержит текстовое представление с уникальным описанием.
Но на самом деле и это можно упросить, давайте заменим код на следующий с применением ForEach
struct ContentView: View {
var body: some View {
List {
ForEach(1...4, id: \.self) { index in
Text("Ячейка \(index)")
}
}
}
}
![](https://habrastorage.org/getpro/habr/upload_files/297/b14/614/297b14614eac6d9e4734122856b0c670.png)
Так как наша вью Text() одинакова во всех случаях мы просто использовали ForEach для того чтобы пробежаться по циклу от 1 до 4 и создать четыре таких тестовых вью.
Внутри List используется цикл ForEach для создания диапазона чисел от 1 до 4. Каждому числу присваивается уникальный идентификатор (id), который в данном случае равен самому числу (self).
Для каждого числа из диапазона создается текстовое представление с помощью Text, которое отображает строку "Ячейка" с номером текущего числа. Эти текстовые представления добавляются в список, который отображается на экране.
Таким образом, данный код создает простой список из четырех ячеек, каждая из которых содержит текстовое представление с номером ячейки.
На самом деле и это можно упростить, убрав «index in»
struct ContentView: View {
var body: some View {
List {
ForEach(1...4, id: \.self) {
Text("Ячейка \($0)")
}
}
}
}
![](https://habrastorage.org/getpro/habr/upload_files/ac8/349/760/ac8349760d15b751a06598808b0d28f2.png)
А теперь мы упростим этот код еще раз и избавимся от ForEach, потому как List предоставляет удобный инициализатор который может сам сделать все тоже самое под капотом
struct ContentView: View {
var body: some View {
List(1...4, id: \.self) {
Text("Ячейка \($0)")
}
}
}
![](https://habrastorage.org/getpro/habr/upload_files/718/106/6ff/7181066ff45348ba76110cb27a790c46.png)
Создаем List с текстом и картинками
Отлично, с базовыми вещами мы разобрались, теперь мы будем создавать нормальную таблицу в которой будут изображения и текст, если вы делали это на UIKit то наверное знаете что вам бы потребовалось подписаться под TableViewDelegate и TableViewDataSource, еще нужно было бы создать и зарегистрировать свою собственную ячейку, еще нужно было написать кучу кода вокруг всего этого чтобы таблица взлетела с теми данными которые вы в нее загрузите, но давайте попробуем сделать тоже самое на SUI
Первое что нужно сделать это скачать картинки которые я для вас подготовил, мы будем использовать их в нашей таблице.
Когда скачаете и разархивируете, добавьте их в ассеты в нашем проекте.
![](https://habrastorage.org/getpro/habr/upload_files/43d/ec6/d64/43dec6d6493a8a7c14b9703f7bf6e36c.png)
Далее в контент вью добавьте следующий код
// Очень плохая практика, никогда так не делайте - здесь это в качестве
// быстрого сопоставления с предоставленными картинками и для показа работы таблицы,
// так как тема урока именно они, а не кодстайл и архитектура
var rockGroups = ["The Beatles", "Rolling Stones", "Prince & The Revolution", "Queen",
"Guns N' Roses", "AC:DC", "The Jimi Hendrix", "Led Zeppelin", "Bob Dylan",
"Joan Jett and the Blackhearts", "Pink Floyd", "Grateful Dead", "The Traveling Wilburys",
"Bruce Springsteen and The E Street Band", "Little Richard and The Upsetters",
"The Kinks", "Creedence Clearwater Revival", "The Band", "The Cure",
"Allman Brothers Band"]
Для вашего удобства я собрал все наименования групп, которые соответствуют именам картинок, но прошу вас - никогда не делайте так в рабочих проектах, ваши данные должны храниться в других местах, подавать во view нужно только подготовленные типы данных которые уже и содержат данные для отображения, но так как это не тема этого урока - более подробно это разбирать мы не будем. Наша задача разобраться с таблицами.
Итак, теперь наша с вами цель передать в наш List некий диапазон, сделать это довольно просто, мы можем передать саму переменную rockGroups и диапазон ее индексов с помощью indices, давайте попробуем также сразу хотя бы отобразить картинки, чтобы убедиться что плюс минус таблица работает.
var body: some View {
List(rockGroups.indices, id: \.self) {
Image(rockGroups[$0])
}
}
![](https://habrastorage.org/getpro/habr/upload_files/895/0d4/afc/8950d4afc8b18139f25831744b4327e1.png)
Отлично, таблица взлетела и уже даже скролится, ради интереса - попробуйте создать тоже самое в UIKit и засеките время, сколько вам на это потребуется особенно если вы не используете снипеты.
Ладно, давайте все же приведем в порядок наш UI, ведь задача была отображать картинки и текст.
var body: some View {
List(rockGroups.indices, id: \.self) { index in
HStack {
Image(rockGroups[index])
.resizable()
.frame(width: 80, height: 80)
Text("\(rockGroups[index])")
.font(.system(.title3))
.bold()
}
}
.listStyle(.plain)
}
![](https://habrastorage.org/getpro/habr/upload_files/3aa/e86/244/3aae86244a24334cf7f4aa61316d19ed.png)
Поздравляю, вы создали таблицу один в один как в UIKit и достаточно быстро, без подписок на различные протоколы и прочих кувырков с вращениями. Обратите внимание на следующий параметр который я применил 32 строке .listStyle(.plain)
![](https://habrastorage.org/getpro/habr/upload_files/f2f/b4b/0a5/f2fb4b0a5daaf1a24af5bbb911f642f9.png)
Рекомендую поиграть с этими стилями, чтобы понять как это может внешне повлиять на отображение вашего контента.
Прежде чем приступить к следующей части урока давайте я объясню этот код.
List(rockGroups.indices, id: \.self) { index in ... } - создает список (List), где каждый элемент списка соответствует индексу в массиве rockGroups. rockGroups.indices генерирует диапазон индексов для массива rockGroups, а id: \.self указывает, что уникальный идентификатор для каждого элемента списка будет самим индексом.
HStack { ... } - это горизонтальный стек (HStack), который располагает свои дочерние элементы горизонтально.
Image(rockGroups[index]) ... - создает изображение (Image) из текущего элемента массива rockGroups по индексу index, .resizable() делает изображение масштабируемым, а .frame(width: 80, height: 80) устанавливает размеры изображения.
Text("\(rockGroups[index])") ... - создает текст (Text) из текущего элемента массива rockGroups по индексу index. .font(.system(.title3)) устанавливает шрифт текста, а .bold() делает текст жирным.
.listStyle(.plain) - устанавливает стиль списка (List) в "plain", что означает, что список будет простым, без дополнительных визуальных эффектов.
Давайте попробуем избавиться от этого массива и создадим свой тип данных RockGroup, создайте одноименный файл и внесите в него следующий код
struct RockGroup {
var groupName: String
var groupImageName: String
}
![](https://habrastorage.org/getpro/habr/upload_files/1a5/f8a/9e5/1a5f8a9e5dc25808649e8191545b7db1.png)
Давайте прямо в этом файле создадим свойство которое будет хранить наши данные и в последствии будем пользоваться именно им.
struct RockGroupData {
static let data = [
RockGroup(groupName: "The Beatles", groupImageName: "The Beatles"),
RockGroup(groupName: "Rolling Stones", groupImageName: "Rolling Stones"),
RockGroup(groupName: "Prince & The Revolution", groupImageName: "Prince & The Revolution"),
RockGroup(groupName: "Queen", groupImageName: "Queen"),
RockGroup(groupName: "Guns N' Roses", groupImageName: "Guns N' Roses"),
RockGroup(groupName: "AC/DC", groupImageName: "AC:DC"),
RockGroup(groupName: "The Jimi Hendrix", groupImageName: "The Jimi Hendrix"),
RockGroup(groupName: "Led Zeppelin", groupImageName: "Led Zeppelin"),
RockGroup(groupName: "Bob Dylan", groupImageName: "Bob Dylan"),
RockGroup(groupName: "Joan Jett and the Blackhearts", groupImageName: "Joan Jett and the Blackhearts"),
RockGroup(groupName: "Pink Floyd", groupImageName: "Pink Floyd"),
RockGroup(groupName: "Grateful Dead", groupImageName: "Grateful Dead"),
RockGroup(groupName: "The Traveling Wilburys", groupImageName: "The Traveling Wilburys"),
RockGroup(groupName: "Bruce Springsteen and The E Street Band", groupImageName: "Bruce Springsteen and The E Street Band"),
RockGroup(groupName: "Little Richard and The Upsetters", groupImageName: "Little Richard and The Upsetters"),
RockGroup(groupName: "The Kinks", groupImageName: "The Kinks"),
RockGroup(groupName: "Creedence Clearwater Revival", groupImageName: "Creedence Clearwater Revival"),
RockGroup(groupName: "The Band", groupImageName: "The Band"),
RockGroup(groupName: "The Cure", groupImageName: "The Cure"),
RockGroup(groupName: "Allman Brothers Band", groupImageName: "Allman Brothers Band")
]
}
Опять таки я упростил для вас задачу и уже добавил все имена и названия картинок, я понимаю что ситуация не супер репрезентативная учитывая что имена групп у нас совпадают с именами картинок, поэтому может показаться что - к чему нам вообще тип данных RockGroup, но зачастую имя картинки не совпадает с именем группы или чего либо еще что вы будете использовать в проекте, а нам с вами нужно научиться использоваться кастомные данные в своих проектах с таблицами.
![](https://habrastorage.org/getpro/habr/upload_files/7f2/8c6/5cb/7f28c65cbfe42c2bbdb04fa06e192ccd.png)
Теперь давайте заменим код в нашем контентвью
var rockGroups = RockGroupData.data
Обратите внимание как наш компилятор сразу стал ругаться, оно и понятно, ведь теперь мы подаем ему не сырые данные, а целый массив типа данных со своими свойствами, давайте спасать ситуацию.
var body: some View {
List(rockGroups, id: \.groupName) { group in
HStack {
Image(group.groupImageName)
.resizable()
.frame(width: 80, height: 80)
Text("\(group.groupName)")
.font(.system(.title3))
.bold()
}
}
.listStyle(.plain)
}
Обратите внимание, теперь в инициализатор List мы передали непосредственно наш массив с рок-группами (именами и картинками) а в качестве уникального идентификатора каждого из них указала .groupName, чтобы наша таблица смогла отрисовать каждую из групп.
Самые внимательные из вас наверное уже задались вопросом, а что вообще с этим id? Зачем он нам вообще нужен и не могут ли возникнуть какие-то проблемы из-за него, ну например в нашем случае если мы создадим две группы с одинаковым именем, то что случится?
Давайте проведем эксперимент, а потом я расскажу вам про этот самый id
Замените имя группы Rolling Stones на The Beatles также как изображено на скриншоте.
![](https://habrastorage.org/getpro/habr/upload_files/298/f9f/0cf/298f9f0cfac9b44b1164ad44e6539981.png)
Не переключайтесь на контентвью и подумайте, что там случилось с нашей таблицей? Как она сейчас должна отобразить данные? Кажется что по логике вещей она должна вычеркнуть эту группу из List потому что она не уникальна. Но давайте проверим
![](https://habrastorage.org/getpro/habr/upload_files/2c7/c65/722/2c7c65722231de3e63af59afc1469c32.png)
Итак, теперь у нас две группы Битлз, причем даже картинка одинаковая, как вы понимаете да - id может стать проблемой если идентификатор не будет уникальным.
Верните пока Rolling Stones на свое место обратно в нашем массиве данных а я расскажу вам про id
Когда вы создаете список, SwiftUI должен отслеживать каждый элемент в списке, чтобы обновлять их правильно при изменении данных.
Например, если вы добавите новый элемент в массив, из которого создается список, SwiftUI должен понять, что нужно добавить новый элемент в список, а не обновить существующий. Для этого SwiftUI использует идентификаторы элементов.
Свойство id позволяет указать, какое значение будет использоваться в качестве уникального идентификатора для каждого элемента списка. В качестве идентификатора можно использовать любое значение, которое уникально идентифицирует элемент.
Например, это может быть индекс элемента в массиве, как использовали мы ранее в нашем проекте или это может быть какое-либо свойство элемента, которое гарантированно! уникально для каждого элемента.
Если не указать свойство id явно, SwiftUI будет использовать значения элементов списка в качестве идентификаторов. Однако, это может привести к ошибкам, если значения элементов не являются уникальными. Поэтому рекомендуется всегда указывать явно свойство id для каждого элемента списка.
С id и важностью его уникальности разобрались, но как сделать так чтобы наши данные действительно были уникальными? Все достаточно просто, мы можем использовать тип данных UUID
![](https://habrastorage.org/getpro/habr/upload_files/631/a0d/b0a/631a0db0a83d5960697c546bfa829869.png)
Он позволит создать уникальное значение для вашего объекта, конечно нужно понимать, что раз в год и хэш функция выдает не уникальное значение, но все же это лучший выход из положения, с UUID вы с практически 100% вероятностью получите уникальное значение.
struct RockGroup: Identifiable {
var id = UUID()
var groupName: String
var groupImageName: String
}
Вы наверное уже обратили внимание на то что я дополнительно подписал RockGroup под Identifiable, это буквально необходимо нам для того чтобы наша таблица увидела, что наш тип данных который мы ей скармливаем имеет уникальный id, собственно этот протокол и требует реализовать свойство id
![](https://habrastorage.org/getpro/habr/upload_files/20c/3a5/d71/20c3a5d710101a32e3486f1f816b7422.png)
Теперь давайте вернемся в наш ContentView и заменим код на следующий.
struct ContentView: View {
var rockGroups = RockGroupData.data
var body: some View {
List(rockGroups) { group in
HStack {
Image(group.groupImageName)
.resizable()
.frame(width: 80, height: 80)
Text("\(group.groupName)")
.font(.system(.title3))
.bold()
}
}
.listStyle(.plain)
}
}
![](https://habrastorage.org/getpro/habr/upload_files/5d8/7f3/fb4/5d87f3fb44f2e757017b10ebfaff73cb.png)
Обратите внимание, мы воспользовались инициализатором принимающим только данные, в нашем случае это (rockGrups) и все это благодаря тому что мы подписались на Identifiable
Прежде чем двигаться дальше, давайте сделаем рефакторинг кода ведь всегда лучше одна строчка кода вместо 2ух. Наведите курсор мыши на HStack и выберите ExtractSubview, а затем переименуйте новую вью в RockCell
![](https://habrastorage.org/getpro/habr/upload_files/9e8/e97/afc/9e8e97afc04c63994c83d26527e6c8c3.png)
struct RockCell: View {
var body: some View {
HStack {
Image(group.groupImageName)
.resizable()
.frame(width: 80, height: 80)
Text("\(group.groupName)")
.font(.system(.title3))
.bold()
}
}
}
Сейчас ваша ячейка ругается на то, что она не понимает о какой такой группе идет речь. Давайте добавим такое свойство в нашу ячейку.
struct RockCell: View {
var group: RockGroup
var body: some View {
HStack {
Image(group.groupImageName)
.resizable()
.frame(width: 80, height: 80)
Text("\(group.groupName)")
.font(.system(.title3))
.bold()
}
}
}
И соответственно к ContentView заменим код на следующий
struct ContentView: View {
var rockGroups = RockGroupData.data
var body: some View {
List(rockGroups) { group in
RockCell(group: group)
}
.listStyle(.plain)
}
}
![](https://habrastorage.org/getpro/habr/upload_files/007/1e0/c71/0071e0c71da0c4d1731ad9bc6ff83e97.png)
Все также отлично работает, а ячейка теперь может быть использована в любом другом месте приложения.
Визуальные модификаторы List
Представим что вы хотите заменить цвет разделителя с серого на любой другой, сделать это достаточно просто, все что нам потребуется это использовать модификатор .listRowSeparatorTint
struct ContentView: View {
var rockGroups = RockGroupData.data
var body: some View {
List(rockGroups) { group in
RockCell(group: group)
.listRowSeparatorTint(.red)
}
.listStyle(.plain)
}
}
![](https://habrastorage.org/getpro/habr/upload_files/179/ab6/77d/179ab677d9d3fe5ff1b2c75d30cf0b98.png)
А если вы вообще хотите избавиться от разделителей? Это тоже возможно.
struct ContentView: View {
var rockGroups = RockGroupData.data
var body: some View {
List(rockGroups) { group in
RockCell(group: group)
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
}
Также мы можем и заменить цвет таблицы, фактически заменив background нашей ячейки и тоже с помощью лишь одного модификатора. .listRowBackground
struct ContentView: View {
var rockGroups = RockGroupData.data
var body: some View {
List(rockGroups) { group in
RockCell(group: group)
.listRowBackground(Color.red)
.listRowSeparatorTint(.blue)
}
.listStyle(.plain)
}
}
![](https://habrastorage.org/getpro/habr/upload_files/8a4/42b/6a1/8a442b6a1ffeec93dba193bbb00f8cee.png)
Также можно более гибко настроить отступы в рамках нашей ячейки и сделать это тоже можно с помощью модификатора - .listRowInsets
struct ContentView: View {
var rockGroups = RockGroupData.data
var body: some View {
List(rockGroups) { group in
RockCell(group: group)
.listRowBackground(Color.red)
.listRowSeparatorTint(.blue)
.listRowInsets(EdgeInsets(top: 90, leading: 50, bottom: 90, trailing: 90))
}
.listStyle(.plain)
}
}
Не обращайте внимание на то что у нас получились не самые красивые ячейки, нам это было необходимо просто чтобы показать - отступы могут быть разных размеров. Единственное с чем лично я пока никак не могу смириться это с тем что статусбар белого цвета разрушает всю картинку. Добавьте List модификатор ignoresSafeArea() и проблема будет решена.
Мой итоговый код выглядит следующим образом
struct ContentView: View {
var rockGroups = RockGroupData.data
var body: some View {
List(rockGroups) { group in
RockCell(group: group)
.listRowBackground(Color.red)
.listRowSeparatorTint(.blue)
.listRowInsets(EdgeInsets(top: 20, leading: 50, bottom: 20, trailing: 50))
}
.ignoresSafeArea()
.listStyle(.plain)
}
}
#Preview {
ContentView()
}
struct RockCell: View {
var group: RockGroup
var body: some View {
HStack {
Image(group.groupImageName)
.resizable()
.frame(width: 180, height: 180)
Text("\(group.groupName)")
.font(.system(.title3))
.bold()
}
}
}
![](https://habrastorage.org/getpro/habr/upload_files/0dd/19a/635/0dd19a635f8d6cb08b77c85967aa2cc1.png)
На этом статья подошла к концу, надеюсь вам понравилось работать с таблицами, разумеется я предлагаю вам попробовать самостоятельно воссоздать таблицу из какого нибудь приложения которым пользуетесь самостоятельно. Например вы бы могли зайти в AppStore на iPhone и попробовать воссоздать страницу «Сегодня»
![](https://habrastorage.org/getpro/habr/upload_files/d4d/7dd/e31/d4d7dde31ddcf803e9fad3ab0995e416.jpg)
Она как раз содержит только вертикальные элементы, значит здесь используется таблица или в нашем случае List.
Кстати рекомендую не удалять приложение которое мы сделали с вами в ходе этого урока, так как мы будем использовать его для следующего.
Как и прежде подписывайтесь на мой телеграм канал - https://t.me/swiftexplorer
Буду рад вашим комментариям и лайкам!
Спасибо за прочтение!