Как стать автором
Обновить

SwiftUI уроки (часть 9)

Время на прочтение11 мин
Количество просмотров973

ссылка на 8ю часть

Разбираемся с List, ForEach and Identifiable 

В UIKit для iOS один из наиболее часто используемых элементов управления пользовательским интерфейсом - это UITableView. Если вы имеете опыт разработки приложений с использованием UIKit, то знаете, что table view предназначен для отображения списков данных. Этот элемент управления пользовательским интерфейсом широко используется в приложениях ориентированных на контент, например, в новостных приложениях или в принципе в любых популярных приложениях, таких как Instagram, Twitter, Reddit и другие.

В SwiftUI для этой цели используется List, а не UITableView. Если вы раньше занимались созданием табличных представлений с помощью UIKit, вы знаете, что это может потребовать некоторого времени и усилий, особенно если вам нужна кастомная ячейка. SwiftUI упрощает этот процесс и позволяет создавать табличные представления всего лишь в несколько строк кода. Даже если вам нужны кастомные ячейки, это не потребует много времени и усилий. Не волнуйтесь, если сейчас это кажется сложным. Вы поймете, как это работает, увидев примеры. В этой части мы начнем с простого списка и постепенно перейдем к более сложным макетам.

Создание простого списка

Давайте начнем с создания простого списка. Откройте Xcode и создайте новый проект, используя шаблон «App». Задайте имя продукта "SwiftUIList" (или любое другое имя, которое вы предпочитаете), и заполните все необходимые значения. 

Важно, чтобы опция "SwiftUI" была выбрана для интерфейса. После создания проекта Xcode сгенерирует стартовый код  в файле ContentView.swift. Найдите объект текста "Hello World" и замените его следующим кодом:

struct ContentView: View {
	var body: some View {
		List {
			Text("Первый")
			Text("Второй")
			Text("Третий")
			Text("Четвертый")
		}
	}
}

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

Но на самом деле и это можно упросить, давайте заменим код на следующий с применением ForEach

struct ContentView: View {
	var body: some View {
		List {
			ForEach(1...4, id: \.self) { index in
				Text("Ячейка \(index)")
			}
		}
	}
}

Так как наша вью 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)")
			}
		}
	}
}

А теперь мы упростим этот код еще раз и избавимся от ForEach, потому как List предоставляет удобный инициализатор который может сам сделать все тоже самое под капотом

struct ContentView: View {
	var body: some View {
		List(1...4, id: \.self) {
			Text("Ячейка \($0)")
		}
	}
}

Создаем List с текстом и картинками

Отлично, с базовыми вещами мы разобрались, теперь мы будем создавать нормальную таблицу в которой будут изображения и текст, если вы делали это на UIKit то наверное знаете что вам бы потребовалось подписаться под TableViewDelegate и TableViewDataSource, еще нужно было бы создать и зарегистрировать свою собственную ячейку, еще нужно было написать кучу кода вокруг всего этого чтобы таблица взлетела с теми данными которые вы в нее загрузите, но давайте попробуем сделать тоже самое на SUI

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

ссылка на картинки

Когда скачаете и разархивируете, добавьте их в ассеты в нашем проекте.

Далее в контент вью добавьте следующий код

	// Очень плохая практика, никогда так не делайте - здесь это в качестве
	// быстрого сопоставления с предоставленными картинками и для показа работы таблицы,
	// так как тема урока именно они, а не кодстайл и архитектура
	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])
		}
	}

Отлично, таблица взлетела и уже даже скролится, ради интереса - попробуйте создать тоже самое в 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)
	}

Поздравляю, вы создали таблицу один в один как в UIKit и достаточно быстро, без подписок на различные протоколы и прочих кувырков с вращениями. Обратите внимание на следующий параметр который я применил 32 строке .listStyle(.plain)

Рекомендую поиграть с этими стилями, чтобы понять как это может внешне повлиять на отображение вашего контента.

Прежде чем приступить к следующей части урока давайте я объясню этот код.

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
}

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

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, но зачастую имя картинки не совпадает с именем группы или чего либо еще что вы будете использовать в проекте, а нам с вами нужно научиться использоваться кастомные данные в своих проектах с таблицами.

Теперь давайте заменим код в нашем контентвью

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 также как изображено на скриншоте.

Не переключайтесь на контентвью и подумайте, что там случилось с нашей таблицей? Как она сейчас должна отобразить данные? Кажется что по логике вещей она должна вычеркнуть эту группу из List потому что она не уникальна. Но давайте проверим

 Итак, теперь у нас две группы Битлз, причем даже картинка одинаковая, как вы понимаете да - id может стать проблемой если идентификатор не будет уникальным.

Верните пока Rolling Stones на свое место обратно в нашем массиве данных а я расскажу вам про id

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

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

Свойство id позволяет указать, какое значение будет использоваться в качестве уникального идентификатора для каждого элемента списка. В качестве идентификатора можно использовать любое значение, которое уникально идентифицирует элемент. 

Например, это может быть индекс элемента в массиве, как использовали мы ранее в нашем проекте или это может быть какое-либо свойство элемента, которое гарантированно! уникально для каждого элемента.

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

С id и важностью его уникальности разобрались, но как сделать так чтобы наши данные действительно были уникальными? Все достаточно просто, мы можем использовать тип данных UUID

Он позволит создать уникальное значение для вашего объекта, конечно нужно понимать, что раз в год и хэш функция выдает не уникальное значение, но все же это лучший выход из положения, с UUID вы с практически 100% вероятностью получите уникальное значение.

struct RockGroup: Identifiable {
	var id = UUID()
	var groupName: String
	var groupImageName: String
}

Вы наверное уже обратили внимание на то что я дополнительно подписал RockGroup под Identifiable, это буквально необходимо нам для того чтобы наша таблица увидела, что наш тип данных который мы ей скармливаем имеет уникальный id, собственно этот протокол и требует реализовать свойство id

Теперь давайте вернемся в наш 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)
	}
}

Обратите внимание, мы воспользовались инициализатором принимающим только данные, в нашем случае это (rockGrups) и все это благодаря тому что мы подписались на Identifiable

Прежде чем двигаться дальше, давайте сделаем рефакторинг кода ведь всегда лучше одна строчка кода вместо 2ух. Наведите курсор мыши на HStack и выберите ExtractSubview, а затем переименуйте новую вью в RockCell

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)
	}
}

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

Визуальные модификаторы List

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

struct ContentView: View {
	var rockGroups = RockGroupData.data

	var body: some View {
		List(rockGroups) { group in
			RockCell(group: group)
				.listRowSeparatorTint(.red)
		}
		
		.listStyle(.plain)
	}
}

А если вы вообще хотите избавиться от разделителей? Это тоже возможно.

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)
	}
}

Также можно более гибко настроить отступы в рамках нашей ячейки и сделать это тоже можно с помощью модификатора - .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()
		}
	}
}

На этом статья подошла к концу, надеюсь вам понравилось работать с таблицами, разумеется я предлагаю вам попробовать самостоятельно воссоздать таблицу из какого нибудь приложения которым пользуетесь самостоятельно. Например вы бы могли зайти в AppStore на iPhone и попробовать воссоздать страницу «Сегодня»

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

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

Как и прежде подписывайтесь на мой телеграм канал - https://t.me/swiftexplorer

Буду рад вашим комментариям и лайкам!

Спасибо за прочтение!

Теги:
Хабы:
+5
Комментарии0

Публикации

Истории

Работа

Swift разработчик
36 вакансий
iOS разработчик
29 вакансий

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург