Списки — фундаментальный элемент мобильных интерфейсов. От списка контактов до ленты новостей, от настроек до каталога товаров — трудно представить приложение, которое не использовало бы этот компонент в той или иной форме. Неудивительно что в Swift особое внимание уделяется простоте и гибкости реализации таких интерфейсов. В эпоху UIKit разработчики сталкивались с целым рядом технических нюансов при работе с TableView. Регистрация ячеек, делегаты, источники данных, переиспользование ячеек — весь этот императивный подход требовал написания значительного объема шаблонного кода. Опытные разработчики помнят, как создание обычного списка с несколькими типами ячеек превращалось в многостраничное решение со сложной логикой.
SwiftUI перевернул это представление. С появлением декларативного подхода к построению интерфейсов разработка списков стала невероятно лаконичной. Вместо описания *как* должен формироваться список, мы просто указываем *что* в нем должно отображаться. SwiftUI берет на себя всю тяжелую работу по эффективному отображению и обновлению данных.
Swift
Скопировано | 1
2
3
4
5
| List {
Text("Первый элемент")
Text("Второй элемент")
Text("Третий элемент")
} |
|
Этот простой пример демонстрирует революционность подхода — вместо десятков строк кода мы описываем список в нескольких строках. И это лишь вершина айсберга возможностей, которые предоставляет SwiftUI.
Создание и стилизация списков в SwiftUI: от базовых примеров до продвинутых решений
Современные тренды мобильного дизайна предъявляют всё более высокие требования к спискам. Пользователи ожидают плавных анимаций, интерактивных элементов, визуального разнообразия и при этом стабильной работы даже с большими объемами данных. SwiftUI предоставляет инструменты для реализации всех этих требований — от простых стилизаций до сложных кастомных решений. Интересно сравнить подходы различных фреймворков. UIKit с его TableView и CollectionView предлагает максимальный контроль над каждым аспектом отображения, но требует значительных усилий для реализации даже базовых сценариев. React Native и Flutter имеют свои аналоги списков с собственными преимуществами и ограничениями. SwiftUI занимает уникальную нишу, балансируя между простотой использования и гибкостью настройки.
За последние годы команда Apple существенно расширила возможности списков в SwiftUI. Появились новые стили, улучшилась поддержка анимаций, добавились механизмы для эффективной работы с большими наборами данных. В iOS 16 и 17 появились дополнительные улучшения, касающиеся производительности и кастомизации. Понимание всех аспектов работы со списками в SwiftUI — от базовых принципов до продвинутых техник — критически важно для создания современных, отзывчивых и визуально привлекательных приложений. В этой статье мы подробно рассмотрим, как эффективно использовать списки, стилизовать их под различные задачи и преодолевать типичные ограничения.
Одной из ключевых особенностей SwiftUI стала возможность представлять данные в виде списков с минимальными усилиями. В отличие от многих других компонентов пользовательского интерфейса списки должны эффективно работать как с небольшими, так и с огромными наборами данных. И если в малых приложениях можно пренебречь оптимизацией, то в сложных проектах вопросы производительности выходят на первый план. Простота базовой реализации списков в SwiftUI иногда создаёт ложное впечатление их ограниченности. Многие разработчики, привыкшие к детальному контролю над каждым аспектом TableView в UIKit, поначалу испытывают сомнения. Разве можно создать действительно сложный и уникальный пользовательский интерфейс с помощью такого лаконичного API? Практика показывает, что можно, и зачастую с меньшими затратами, чем в UIKit. Эволюция списков в SwiftUI продолжается с каждым обновлением iOS. Первая версия SwiftUI в iOS 13 предлагала базовую функциональность, которой не хватало многих возможностей, привычных для UIKit-разработчиков. В iOS 14 появилась поддержка LazyVStack и LazyHStack, что расширило возможности кастомизации списков. iOS 15 принесла обновлённый API для обработки свайпов и улучшенные возможности выделения элементов. А с выходом iOS 16 и 17 появились инструменты для ещё более точной настройки производительности и внешнего вида.
Преимущества декларативного подхода раскрываются в полной мере при работе с динамическими данными. Когда источник данных обновляется, SwiftUI автоматически перерисовывает только те элементы списка, которые действительно изменились. Это не только упрощает код, но и улучшает производительность — особенно в сравнении с ручным управлением обновлениями в UIKit, где легко допустить ошибки, приводящие к мерцанию интерфейса или лишним перерисовкам.
Интересно, что при всей своей простоте списки в SwiftUI способны решать весьма нетривиальные задачи. Секции с заголовками и подвалами, вложенные списки, интерактивные элементы с жестами — всё это реализуется с минимальными затратами кода. При этом SwiftUI не требует от разработчика думать о деталях реализации, таких как переиспользование ячеек или оптимизация прокрутки.
Основы списков в SwiftUI
Работа со списками в SwiftUI начинается с понимания их фундаментальной концепции. В отличие от UIKit, где TableView требует делегатов и источников данных, SwiftUI использует декларативный подход. Здесь список — это просто контейнер, который организует свои элементы в вертикальную прокручиваемую колонку. Самый простой способ создать список — использовать компонент List с статическим содержимым:
Swift
Скопировано | 1
2
3
4
5
6
| List {
Text("Антуан")
Text("Маайке")
Text("Сеп")
Text("Йип")
} |
|
Этот код генерирует вертикальный прокручиваемый список из четырех текстовых элементов. Никаких делегатов, источников данных или переиспользования ячеек — SwiftUI автоматически управляет всем этим за кулисами. Однако на практике редко используются статические списки. Чаще всего данные поступают из массива или другой коллекции. Для таких случаев SwiftUI предоставляет элегантное решение:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| struct DynamicListView: View {
var people: [Person] = [
Person(name: "Антуан", age: 34),
Person(name: "Маайке", age: 6),
Person(name: "Сеп", age: 3),
Person(name: "Йип", age: 1)
]
var body: some View {
List(people, id: \.name) { person in
Text(person.name)
}
}
} |
|
В этом примере мы передаем коллекцию people вместе с ключом для уникальной идентификации каждого элемента (id: \.name ). SwiftUI использует этот идентификатор для эффективного обновления интерфейса при изменении данных. Здесь важно отметить потенциальную проблему: если два человека имеют одинаковое имя, могут возникнуть непредсказуемые эффекты. Поэтому рекомендуется использовать действительно уникальные идентификаторы или, что еще лучше, реализовать протокол Identifiable :
Swift
Скопировано | 1
2
3
4
5
| struct Person: Identifiable {
let id = UUID()
let name: String
let age: Int
} |
|
После реализации этого протокола код списка упрощается:
Swift
Скопировано | 1
2
3
| List(people) { person in
Text(person.name)
} |
|
SwiftUI автоматически использует свойство id для идентификации элементов, что делает код чище и безопаснее.
Жизненный цикл элементов списка в SwiftUI значительно отличается от UIKit. В традиционном подходе разработчик должен вручную управлять переиспользованием ячеек, что часто приводит к ошибкам, таким как неправильное отображение данных при прокрутке. В SwiftUI этой проблемы не существует — фреймворк создает и обновляет представления на основе состояния данных. Когда данные в источнике меняются, SwiftUI автоматически обновляет только те элементы, которые действительно изменились. Это возможно благодаря системе идентификации, которую мы рассмотрели выше. Важно понимать, что представления в SwiftUI — это не объекты, которые создаются один раз и затем изменяются, а скорее описания того, что должно быть отображено на экране при определенном состоянии данных.
При работе с обновлениями данных в списках SwiftUI предлагает несколько подходов. Модификатор .id() указывает SwiftUI, что все представление должно быть пересоздано, если изменяется переданное значение. Это радикальный подход, который следует использовать с осторожностью:
Swift
Скопировано | 1
2
3
| List(people) { person in
Text(person.name)
}.id(refreshID) // Полная перерисовка при изменении refreshID |
|
ForEach и List работают похожим образом, но есть важное отличие: ForEach часто используется внутри других контейнеров или даже внутри List для создания сегментов с разной логикой:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
| List {
Text("Заголовок секции")
ForEach(activeUsers) { user in
ActiveUserRow(user: user)
}
Text("Неактивные пользователи")
ForEach(inactiveUsers) { user in
InactiveUserRow(user: user)
}
} |
|
Одно из ключевых отличий SwiftUI от UIKit заключается в том, как обрабатываются изменения данных. В UIKit требуется явно сообщать таблице о вставке, удалении или обновлении строк с помощью методов вроде insertRows(at:with:) . В SwiftUI достаточно изменить исходные данные, и интерфейс обновится автоматически, благодаря системе отслеживания зависимостей.
Рассмотрим пример с использованием @State для управления списком элементов:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| struct TaskListView: View {
@State private var tasks = [
Task(title: "Изучить SwiftUI"),
Task(title: "Создать прототип"),
Task(title: "Провести тестирование")
]
var body: some View {
List {
ForEach(tasks) { task in
Text(task.title)
}
.onDelete(perform: deleteTasks)
Button("Добавить задачу") {
tasks.append(Task(title: "Новая задача \(Date())"))
}
}
}
func deleteTasks(at offsets: IndexSet) {
tasks.remove(atOffsets: offsets)
}
}
struct Task: Identifiable {
let id = UUID()
var title: String
} |
|
В этом примере мы видим, как легко реализуется добавление и удаление элементов списка. Модификатор .onDelete позволяет обрабатывать стандартные свайпы для удаления. Когда пользователь нажимает кнопку "Добавить задачу", новый элемент добавляется в массив tasks , и SwiftUI автоматически обновляет список, не требуя дополнительных действий.
Другой важный аспект — обработка нажатий на элементы списка. В отличие от UIKit, где используется метод didSelectRowAt , в SwiftUI мы просто добавляем модификатор .onTapGesture к элементу:
Swift
Скопировано | 1
2
3
4
5
6
| ForEach(tasks) { task in
Text(task.title)
.onTapGesture {
print("Выбрана задача: \(task.title)")
}
} |
|
При работе со сложными данными нередко требуется создавать иерархические структуры. SwiftUI обрабатывает вложенность с удивительной легкостью:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| struct Department: Identifiable {
let id = UUID()
var name: String
var employees: [Employee]
}
struct Employee: Identifiable {
let id = UUID()
var name: String
}
struct NestedListView: View {
var departments = [
Department(name: "Разработка", employees: [
Employee(name: "Алексей"),
Employee(name: "Мария")
]),
Department(name: "Дизайн", employees: [
Employee(name: "Сергей"),
Employee(name: "Анна")
])
]
var body: some View {
List {
ForEach(departments) { department in
Section(header: Text(department.name)) {
ForEach(department.employees) { employee in
Text(employee.name)
}
}
}
}
}
} |
|
Здесь мы создаем список с секциями, где каждая секция представляет отдел, а элементы внутри — сотрудников. SwiftUI автоматически обрабатывает все уровни вложенности, создавая визуально структурированный список.
Для интеграции списков с навигацией SwiftUI предлагает компонент NavigationLink . Это позволяет создавать иерархические интерфейсы с минимальнми усилиями:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
| NavigationView {
List(articles) { article in
NavigationLink(destination: ArticleDetailView(article: article)) {
VStack(alignment: .leading) {
Text(article.title)
.font(.headline)
Text(article.subtitle)
.font(.subheadline)
}
}
}
.navigationTitle("Статьи")
} |
|
В этом примере каждый элемент списка становится ссылкой на детальное представление статьи. SwiftUI автоматически обрабатывает переходы, анимации и историю навигации.
Еще одна мощная возможность Lists в SwiftUI — это группировка содержимого в секции. Секции позволяют логически организовать данные и визуально отделить различные группы элементов:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| List {
Section(header: Text("Приоритетные задачи")) {
ForEach(priorityTasks) { task in
TaskRow(task: task)
}
}
Section(header: Text("Обычные задачи"), footer: Text("Всего: \(regularTasks.count)")) {
ForEach(regularTasks) { task in
TaskRow(task: task)
}
}
}
.listStyle(GroupedListStyle()) |
|
Секции могут иметь заголовки и подвалы, что позволяет добавить контекстную информацию к группам данных. В примере выше мы также применяем стиль GroupedListStyle , который визуально выделяет секции.
При работе с динамическими списками часто требуется обновлять только определенные элементы, не затрагивая остальные. Модификатор .id() позволяет явно указать идентификатор для представления, что влияет на то, как SwiftUI определяет, должно ли представление быть обновлено:
Swift
Скопировано | 1
2
3
4
5
6
| List {
ForEach(messages) { message in
MessageRow(message: message)
.id(message.id)
}
} |
|
В этом случае, если изменяется только содержимое сообщения, но не его идентификатор, SwiftUI обновит только соответствующую строку, а не весь список.
Стилизация списков
Списки в SwiftUI не только функциональны, но и гибко настраиваются под различные дизайнерские требования. Система предлагает несколько встроенных стилей, которые влияют на внешний вид и поведение списка, сохраняя при этом единообразие с системным дизайном. Базовый стиль, применяемый по умолчанию — это DefaultListStyle , который адаптируется к контейнеру, в котором размещен список. SwiftUI автоматически выбирает наиболее подходящий внешний вид в зависимости от контекста:
Стилизация списков
SwiftUI предлагает разнообразные способы стилизации списков для адаптации их внешнего вида к требованиям дизайна приложения. По умолчанию список использует системный стиль, который автоматически подстраивается под контекст использования.
Swift
Скопировано | 1
2
3
| List(people) { person in
Text(person.name)
}.listStyle(.automatic) |
|
Модификатор .listStyle() — основной инструмент для изменения внешнего вида списка. SwiftUI предоставляет широкий набор предопределенных стилей:
Automatic (DefaultListStyle) — применяет стандартный стиль в зависимости от платформы и контекста,
Plain (PlainListStyle) — минималистичный стиль без визуальных групп или разделителей,
Grouped (GroupedListStyle) — группирует элементы и секции с фоновым оформлением,
Inset (InsetListStyle) — добавляет отступы по краям элементов,
Inset Grouped (InsetGroupedListStyle) — комбинирует отступы с группировкой,
Bordered (BorderedListStyle) — добавляет стандартную рамку,
Carousel (CarouselListStyle) — создает карусель из элементов,
Elliptical (EllipticalListStyle) — размещает элементы по эллиптической траектории.
Выбор подходящего стиля зависит от контекста использования списка и общего дизайна приложения. Например, GroupedListStyle отлично подходит для экранов настроек:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| List {
Section {
Toggle("Авиарежим", isOn: $airplaneMode)
Toggle("Wi-Fi", isOn: $wifiEnabled)
Toggle("Bluetooth", isOn: $bluetoothEnabled)
} header: {
Text("Соединения")
}
Section {
Toggle("Уведомления", isOn: $notificationsEnabled)
Toggle("Звуки", isOn: $soundEnabled)
} header: {
Text("Системные настройки")
}
}.listStyle(.grouped) |
|
InsetGroupedListStyle, введенный в iOS 14, создает более современный вид с закругленными углами секций и дополнительными отступами:
Swift
Скопировано | 1
2
3
| List {
// Содержимое с секциями
}.listStyle(.insetGrouped) |
|
Для создания боковой панели навигации, характерной для iPad и Mac приложений, используется SidebarListStyle:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
| NavigationView {
List {
NavigationLink("Входящие", destination: InboxView())
NavigationLink("Отправленные", destination: SentView())
NavigationLink("Архив", destination: ArchiveView())
}
.listStyle(.sidebar)
Text("Выберите папку")
} |
|
Особенность SidebarListStyle в том, что он поддерживает раскрываемые секции, которые пользователь может сворачивать и разворачивать:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| List {
Section(isExpanded: $workExpanded) {
ForEach(workProjects) { project in
ProjectRow(project: project)
}
} header: {
Label("Рабочие проекты", systemImage: "briefcase")
}
Section(isExpanded: $personalExpanded) {
ForEach(personalProjects) { project in
ProjectRow(project: project)
}
} header: {
Label("Личные проекты", systemImage: "person")
}
}.listStyle(.sidebar) |
|
Одним из частых требований дизайна является изменение фонового цвета списка. В SwiftUI это не так прямолинейно, как могло бы показаться. Простое применение модификатора .background() не даст ожидаемого результата. Необходимо дополнительно скрыть стандартный фон прокрутки:
Swift
Скопировано | 1
2
3
4
5
6
7
| List {
ForEach(items) { item in
Text(item.name)
}
}
.background(Color.mint.opacity(0.2))
.scrollContentBackground(.hidden) |
|
Модификатор .scrollContentBackground(.hidden) появился в iOS 16 и является ключевым для настройки фона списка. В более ранних версиях iOS приходилось использовать обходные пути через UIKit.
Помимо изменения фона, важным аспектом стилизации является кастомизация отдельных ячеек списка. В SwiftUI каждая ячейка представляет собой обычное представление, которое можно свободно настраивать:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| List(people) { person in
HStack {
Image(systemName: "person.circle.fill")
.foregroundColor(.blue)
.font(.system(size: 32))
VStack(alignment: .leading) {
Text(person.name)
.font(.headline)
Text("Возраст: \(person.age)")
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding(.vertical, 8)
} |
|
Такой подход позволяет создавать сложные и информативные ячейки с минимальными усилиями. Важно отметить, что SwiftUI автоматически обрабатывает поведение при нажатии, добавляя стандартный эффект выделения.
Для пользовательского оформления выделения можно использовать модификатор .listRowBackground :
Swift
Скопировано | 1
2
3
4
| List(people) { person in
Text(person.name)
.listRowBackground(person.isActive ? Color.green.opacity(0.2) : Color.clear)
} |
|
Этот модификатор позволяет задать фоновый цвет для конкретной строки, что особенно полезно при создании списков с условным форматированием.
Стилизация разделителей между элементами также является важной частью дизайна списка. SwiftUI предлагает несколько способов управления их внешним видом:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| List {
ForEach(items) { item in
Text(item.name)
}
.listRowSeparator(.hidden) // Скрыть разделители
}
// Или для конкретных строк
List {
ForEach(items) { item in
Text(item.name)
.listRowSeparator(.visible, edges: .bottom) // Показать разделитель только снизу
}
} |
|
Модификатор .listRowInsets позволяет настроить внутренние отступы ячейки:
Swift
Скопировано | 1
2
| Text("Кастомные отступы")
.listRowInsets(EdgeInsets(top: 8, leading: 20, bottom: 8, trailing: 20)) |
|
Для более специфических случаев может потребоваться создание полностью кастомного стиля списка. Это можно сделать, определив структуру, соответствующую протоколу ListStyle :
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| struct CustomBorderedListStyle: ListStyle {
func makeBody(configuration: Configuration) -> some View {
VStack(spacing: 12) {
ForEach(configuration.content) { item in
item
.padding(12)
.background(RoundedRectangle(cornerRadius: 8)
.stroke(Color.blue, lineWidth: 1))
}
}
.padding(.horizontal)
}
}
// Применение
List {
// Содержимое
}.listStyle(CustomBorderedListStyle()) |
|
Такой подход даёт полный контроль над внешним видом списка, но требует самостоятельной реализации всех аспектов, включая обработку прокрутки и интерактивности.
При разработке приложений, работающих на разных устройствах, часто требуется адаптировать стиль списка под конкретную платформу или размер экрана. SwiftUI предлагает элегантное решение с помощью модификаторов вроде .phoneOnlyStackNavigationView() и условных конструкций:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @ViewBuilder
func adaptiveList() -> some View {
if UIDevice.current.userInterfaceIdiom == .pad {
List {
// Содержимое для iPad
}
.listStyle(.sidebar)
} else {
List {
// Содержимое для iPhone
}
.listStyle(.insetGrouped)
}
} |
|
Для поддержки темной и светлой тем без дополнительного кода достаточно использовать системные цвета и адаптивные изображения:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| List {
ForEach(items) { item in
HStack {
Image(systemName: item.iconName)
.foregroundColor(Color(.systemBlue)) // Автоматически адаптируется к теме
Text(item.name)
.foregroundColor(Color(.label)) // Черный в светлой теме, белый в темной
}
.padding()
.background(Color(.secondarySystemBackground)) // Адаптивный фоновый цвет
.cornerRadius(8)
}
} |
|
Для создания анимированных переходов между разными состояниями списка можно использовать модификатор .animation() в комбинации с условными значениями:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| @State private var isCompact = false
var body: some View {
VStack {
Toggle("Компактный вид", isOn: $isCompact)
.padding()
List {
ForEach(items) { item in
HStack {
Text(item.name)
Spacer()
if !isCompact {
Text(item.description)
.foregroundColor(.gray)
}
}
}
}
.animation(.spring(), value: isCompact)
}
} |
|
При работе с доступностью SwiftUI списки предлагают встроенную поддержку VoiceOver и других ассистивных технологий. При этом можно дополнительно улучшить опыт пользователей с особыми потребностями:
Swift
Скопировано | 1
2
3
4
5
6
7
8
| List(items) { item in
Text(item.title)
.accessibilityLabel("Элемент: \(item.title)")
.accessibilityHint("Нажмите для просмотра деталей")
.accessibilityAction(named: "Отметить") {
item.isMarked.toggle()
}
} |
|
Модификаторы доступности позволяют предоставить дополнительный контекст и действия для ассистивных технологий, не влияя на визуальное оформление. Динамические шрифты — еще один аспект доступности, который SwiftUI поддерживает "из коробки". При использовании системных шрифтов ячейки списка автоматически адаптируются к предпочтениям пользователя:
Swift
Скопировано | 1
2
3
4
5
6
7
8
| List(items) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.subtitle)
.font(.subheadline)
}
} |
|
Пользователи, установившие увеличенный размер шрифта в настройках системы, автоматически получат соответствующий размер текста в приложении.
Для создания действительно уникального внешнего вида можно комбинировать различные модификаторы. Например, добавление градиентного фона к элементам списка:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| List {
ForEach(items) { item in
Text(item.name)
.frame(maxWidth: .infinity, minHeight: 60)
.listRowBackground(
LinearGradient(
gradient: Gradient(colors: [.blue.opacity(0.2), .purple.opacity(0.3)]),
startPoint: .leading,
endPoint: .trailing
)
)
}
}
.scrollContentBackground(.hidden) |
|
Для создания эффекта глубины и выделения активных элементов можно использовать тени и масштабирование:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| List {
ForEach(items) { item in
Text(item.name)
.padding()
.background(Color(.systemBackground))
.cornerRadius(8)
.shadow(color: item.isSelected ? .blue.opacity(0.3) : .gray.opacity(0.1),
radius: item.isSelected ? 5 : 2,
x: 0, y: item.isSelected ? 3 : 1)
.scaleEffect(item.isSelected ? 1.02 : 1)
.animation(.spring(), value: item.isSelected)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
.scrollContentBackground(.hidden) |
|
При работе с сезонными или тематическими приложениями часто требуется переключать стили списков на основе контента или времени года:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @State private var season: Season = .current()
var body: some View {
List {
ForEach(items) { item in
Text(item.name)
}
}
.listStyle(season == .winter ? .insetGrouped : .plain)
.tint(season.accentColor)
.onChange(of: season) { newSeason in
withAnimation(.spring()) {
// Дополнительные изменения стиля при смене сезона
}
}
} |
|
Контекстные меню — ещё один способ расширить функциональность списков без усложнения интерфейса:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| List {
ForEach(emails) { email in
HStack {
Image(systemName: email.isRead ? "envelope.open" : "envelope")
Text(email.subject)
}
.contextMenu {
Button {
markAsRead(email)
} label: {
Label("Прочитано", systemImage: "envelope.open")
}
Button(role: .destructive) {
deleteEmail(email)
} label: {
Label("Удалить", systemImage: "trash")
}
}
}
} |
|
Использование .refreshable позволяет добавить функцию обновления списка жестом pull-to-refresh:
Swift
Скопировано | 1
2
3
4
5
6
| List(articles) { article in
ArticleRow(article: article)
}
.refreshable {
await loadLatestArticles()
} |
|
SwiftUI 3.0 (iOS 15) и более поздние версии предлагают улучшенный API для стилизации разделителей строк:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
| List {
ForEach(transactions) { transaction in
HStack {
Text(transaction.title)
Spacer()
Text(transaction.amount)
.foregroundColor(transaction.amount.hasPrefix("-") ? .red : .green)
}
.listRowSeparatorTint(transaction.isImportant ? .red : nil)
}
} |
|
Для комбинирования различных стилей визуализации элементов в одном списке можно использовать проверку условий непосредственно в замыкании:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
| List {
ForEach(items) { item in
if item.isPinned {
PinnedItemRow(item: item)
.listRowBackground(Color.yellow.opacity(0.2))
} else if item.isCompleted {
CompletedItemRow(item: item)
.listRowBackground(Color.green.opacity(0.1))
} else {
RegularItemRow(item: item)
}
}
} |
|
Выделение элементов и работа с выбором
Одна из важнейших функций списков в любом приложении — возможность выбора элементов. SwiftUI предлагает элегантные решения как для одиночного, так и для множественного выбора, освобождая разработчиков от необходимости писать сложный код для обработки выделения. Наиболее распространённый способ реализации выбора в списках SwiftUI — использование параметра selection в сочетании с состоянием отслеживающим выбранные элементы:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| struct SelectionListView: View {
var people: [Person] = [
Person(name: "Антуан ван дер Ли", age: 34),
Person(name: "Маайке", age: 6),
Person(name: "Сеп", age: 3),
Person(name: "Йип", age: 1)
]
@State var selectedPeopleIdentifiers: Set<UUID> = []
var body: some View {
NavigationView {
List(people, selection: $selectedPeopleIdentifiers) { person in
Text(person.name)
}
.toolbar { EditButton() }
}
Text("Выбрано \(selectedPeopleIdentifiers.count) элемент(ов)")
}
} |
|
В этом примере для хранения выбранных элементов используется Set<UUID> , что обеспечивает эффективное отслеживание уникальных идентификаторов. Кнопка редактирования (EditButton ) автоматически переключает список в режим выбора, позволяя пользователю отметить несколько элементов.
Если требуется одиночный выбор, можно использовать опциональный тип вместо множества:
Swift
Скопировано | 1
2
3
4
5
| @State private var selectedPersonID: UUID? = nil
List(people, selection: $selectedPersonID) { person in
Text(person.name)
} |
|
Однако стандартный режим редактирования не всегда соответствует дизайнерским требованиям. Часто нужно создать более естественный интерфейс выбора, например, с использованием символов чекбоксов:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| List {
ForEach(people) { person in
HStack {
Image(systemName: selectedPeopleIdentifiers.contains(person.id)
? "checkmark.circle.fill" : "circle")
.foregroundColor(.blue)
Text(person.name)
}
.contentShape(Rectangle())
.onTapGesture {
if selectedPeopleIdentifiers.contains(person.id) {
selectedPeopleIdentifiers.remove(person.id)
} else {
selectedPeopleIdentifiers.insert(person.id)
}
}
}
} |
|
Здесь используется .contentShape(Rectangle()) для обеспечения того, чтобы вся строка была интерактивной, а не только текст и изображение по отдельности.
Для обеспечения визуальной обратной связи можно использовать анимации при изменении состояния выбора:
Swift
Скопировано | 1
2
3
4
5
6
7
8
| HStack {
Image(systemName: selectedPeopleIdentifiers.contains(person.id)
? "checkmark.circle.fill" : "circle")
.foregroundColor(.blue)
.animation(.spring(), value: selectedPeopleIdentifiers.contains(person.id))
Text(person.name)
} |
|
Для создания более сложных интерфейсов выбора можно комбинировать различные состояния. Например, реализация трёхстороннего чекбокса для дерева задач:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| enum CheckState {
case unchecked
case partiallyChecked
case checked
}
struct TaskItem: Identifiable {
let id = UUID()
let title: String
var state: CheckState
var subtasks: [TaskItem]
}
// В представлении:
HStack {
Image(systemName: getCheckboxImageName(for: task.state))
.foregroundColor(getCheckboxColor(for: task.state))
Text(task.title)
}
.onTapGesture {
toggleTaskState(task)
}
// Функция для получения имени изображения:
func getCheckboxImageName(for state: CheckState) -> String {
switch state {
case .unchecked: return "circle"
case .partiallyChecked: return "minus.circle.fill"
case .checked: return "checkmark.circle.fill"
}
} |
|
Тактильная обратная связь значительно улучшает пользовательский опыт. С iOS 13 можно использовать фреймворк UIFeedbackGenerator через UIKit:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import SwiftUI
import UIKit
struct HapticFeedback {
static func selection() {
let generator = UISelectionFeedbackGenerator()
generator.selectionChanged()
}
static func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) {
let generator = UIImpactFeedbackGenerator(style: style)
generator.impactOccurred()
}
}
// Применение в обработчике выбора:
.onTapGesture {
if selectedPeopleIdentifiers.contains(person.id) {
selectedPeopleIdentifiers.remove(person.id)
} else {
selectedPeopleIdentifiers.insert(person.id)
}
HapticFeedback.selection()
} |
|
Для программного управления выделением может потребоваться изменение выбранных элементов в ответ на действия, не связанные напрямую с касанием строки списка:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
| Button("Выбрать всех") {
for person in people {
selectedPeopleIdentifiers.insert(person.id)
}
}
Button("Снять выделение") {
selectedPeopleIdentifiers.removeAll()
} |
|
Важно помнить о сохранении состояния выбора при перемещении между экранами или при перезапуске приложения. Для этого можно использовать UserDefaults, CoreData или другие механизмы сохранения данных:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
| // Сохранение выбора
func saveSelection() {
let selectedIDs = selectedPeopleIdentifiers.map { $0.uuidString }
UserDefaults.standard.set(selectedIDs, forKey: "SelectedPeople")
}
// Восстановление выбора
func restoreSelection() {
if let selectedIDs = UserDefaults.standard.stringArray(forKey: "SelectedPeople") {
selectedPeopleIdentifiers = Set(selectedIDs.compactMap { UUID(uuidString: $0) })
}
} |
|
В SwiftUI существуют различные способы визуализации выбранных элементов. Помимо стандартных чекбоксов, можно использовать более сложные индикаторы состояния:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| ForEach(tasks) { task in
HStack {
// Кастомный индикатор выполнения
Circle()
.stroke(task.isSelected ? Color.blue : Color.gray, lineWidth: 2)
.overlay(
task.isSelected ?
Circle().fill(Color.blue).scaleEffect(0.8) : nil
)
.frame(width: 24, height: 24)
Text(task.title)
.foregroundColor(task.isSelected ? .primary : .secondary)
Spacer()
if task.isSelected {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
.transition(.scale.combined(with: .opacity))
}
}
.padding()
.background(task.isSelected ? Color.blue.opacity(0.1) : Color.clear)
.cornerRadius(8)
.animation(.spring(), value: task.isSelected)
.onTapGesture {
withAnimation {
toggleSelection(for: task)
}
}
} |
|
Для реализации выбора элементов с помощью долгого нажатия, что часто используется в почтовых и файловых приложениях, можно применить модификатор .onLongPressGesture :
Swift
Скопировано | 1
2
3
4
5
| .onLongPressGesture(minimumDuration: 0.5) {
enterSelectionMode()
selectedItems.insert(item.id)
HapticFeedback.impact(style: .medium)
} |
|
Это особенно полезно для интерфейсов, где обычное нажатие выполняет другое действие (например, открывает элемент), а долгое нажатие активирует режим выбора.
Для работы с выделением в более сложных иерархических списках может потребоваться каскадный выбор, когда выбор родительского элемента влияет на все дочерние:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| struct FolderView: View {
@Binding var selection: Set<UUID>
let folder: Folder
var body: some View {
DisclosureGroup {
ForEach(folder.files) { file in
HStack {
Image(systemName: selection.contains(file.id) ?
"checkmark.square.fill" : "square")
.foregroundColor(.blue)
.onTapGesture {
toggleSelection(fileID: file.id)
}
Text(file.name)
}
}
ForEach(folder.subfolders) { subfolder in
FolderView(selection: $selection, folder: subfolder)
}
} label: {
HStack {
Image(systemName: folderSelectionIcon)
.foregroundColor(.blue)
.onTapGesture {
toggleFolderSelection()
}
Text(folder.name)
}
}
}
private var folderSelectionIcon: String {
let filesAndSubfolders = folder.allFilesAndSubfolders
let selectedCount = filesAndSubfolders.filter { selection.contains($0.id) }.count
if selectedCount == 0 {
return "square"
} else if selectedCount == filesAndSubfolders.count {
return "checkmark.square.fill"
} else {
return "minus.square.fill"
}
}
private func toggleFolderSelection() {
let allIDs = folder.allFilesAndSubfolders.map { $0.id }
let allSelected = allIDs.allSatisfy { selection.contains($0) }
if allSelected {
// Снимаем выделение со всех элементов папки
for id in allIDs {
selection.remove(id)
}
} else {
// Выделяем все элементы папки
for id in allIDs {
selection.insert(id)
}
}
}
private func toggleSelection(fileID: UUID) {
if selection.contains(fileID) {
selection.remove(fileID)
} else {
selection.insert(fileID)
}
}
} |
|
Связь выбора элементов с другими действиями в приложении — ещё один важный аспект. Например, можно автоматически показывать панель действий при выборе элементов:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| struct TaskListWithActions: View {
@State private var selectedTasks = Set<UUID>()
@State private var isActionSheetPresented = false
var body: some View {
ZStack(alignment: .bottom) {
List(tasks, selection: $selectedTasks) { task in
Text(task.title)
}
if !selectedTasks.isEmpty {
HStack(spacing: 20) {
Button(action: markAsCompleted) {
Image(systemName: "checkmark.circle")
.font(.title2)
}
Button(action: { isActionSheetPresented = true }) {
Image(systemName: "folder")
.font(.title2)
}
Button(action: deleteTasks) {
Image(systemName: "trash")
.font(.title2)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(radius: 5)
.padding(.bottom)
.transition(.move(edge: .bottom).combined(with: .opacity))
.animation(.spring(), value: selectedTasks.isEmpty)
}
}
.actionSheet(isPresented: $isActionSheetPresented) {
ActionSheet(
title: Text("Переместить \(selectedTasks.count) элемент(ов)"),
buttons: [
.default(Text("В избранное")) { moveToFavorites() },
.default(Text("В архив")) { moveToArchive() },
.cancel()
]
)
}
}
} |
|
Продвинутые техники
При разработке реальных приложений часто возникают сценарии, выходящие за рамки базовых возможностей списков. Умение работать с неоднородными данными и оптимизация производительности становятся критически важными навыками для создания плавных и отзывчивых интерфейсов.
Динамические списки с разными типами данных
Нередко в одном списке требуется отображать элементы различных типов. Например, лента социальной сети может содержать текстовые посты, изображения, опросы и другие виды контента. SwiftUI позволяет элегантно решить эту задачу с помощью паттерна типа-обёртки:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| enum FeedItem: Identifiable {
case textPost(TextPost)
case imagePost(ImagePost)
case pollPost(PollPost)
var id: UUID {
switch self {
case .textPost(let post): return post.id
case .imagePost(let post): return post.id
case .pollPost(let post): return post.id
}
}
}
struct FeedView: View {
var items: [FeedItem] = []
var body: some View {
List(items) { item in
switch item {
case .textPost(let post):
TextPostView(post: post)
case .imagePost(let post):
ImagePostView(post: post)
case .pollPost(let post):
PollPostView(post: post)
}
}
}
} |
|
Такой подход позволяет сохранить строгую типизацию и при этом работать с разнородными данными в одном списке. Каждый тип контента получает свою специализированную ячейку, оптимизированную для отображения именно этого типа данных.
Оптимизация производительности больших списков
При работе с большими объёмами данных производительность обычного List может деградировать. Вместо загрузки всех данных сразу, лучше реализовать постепенную подгрузку контента:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| struct PaginatedListView: View {
@StateObject private var viewModel = ArticlesViewModel()
var body: some View {
List {
ForEach(viewModel.articles) { article in
ArticleRow(article: article)
.onAppear {
if article.id == viewModel.articles.last?.id {
Task {
await viewModel.loadMoreIfNeeded()
}
}
}
}
if viewModel.isLoading {
ProgressView()
.frame(maxWidth: .infinity)
}
}
.refreshable {
await viewModel.refreshContent()
}
}
} |
|
Этот паттерн отслеживает, когда пользователь прокручивает список до последнего элемента, и инициирует загрузку следующей страницы данных, создавая иллюзию бесконечного списка.
Интеграция с Combine
Combine предоставляет мощный инструментарий для работы с асинхронными потоками данных, что идеально подходит для списков, содержимое которых обновляется из различных источников:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| class SearchViewModel: ObservableObject {
@Published var searchTerm = ""
@Published var results: [SearchResult] = []
private var cancellables = Set<AnyCancellable>()
init() {
$searchTerm
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.flatMap { term -> AnyPublisher<[SearchResult], Never> in
return self.search(term)
.catch { _ in Just([]) }
.eraseToAnyPublisher()
}
.receive(on: RunLoop.main)
.assign(to: \.results, on: self)
.store(in: &cancellables)
}
private func search(_ term: String) -> AnyPublisher<[SearchResult], Error> {
// Реализация запроса к API или локальной базе данных
}
} |
|
Такой подход позволяет создать поисковый интерфейс, который реагирует на изменения поискового запроса с задержкой для предотвращения излишних запросов и автоматически обновляет список результатов.
Оптимизация перерисовки списков
Один из ключевых аспектов производительности — минимизация ненужных перерисовок. В SwiftUI каждое изменение состояния может вызвать каскадное обновление представлений. Для оптимизации можно использовать:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| struct OptimizedListItemView: View {
let item: ListItem
var body: some View {
HStack {
Text(item.title)
Spacer()
Text(item.subtitle)
}
.equatable() // Кастомный модификатор
}
}
extension View {
func equatable() -> some View {
return EquatableView(content: self)
}
}
struct EquatableView<Content: View>: View {
let content: Content
var body: some View {
content
}
}
extension EquatableView: Equatable {
static func == (lhs: EquatableView<Content>, rhs: EquatableView<Content>) -> Bool {
return true // Логика сравнения в реальном приложении будет сложнее
}
} |
|
Другая важная техника — разделение представлений на меньшие компоненты, каждый из которых отвечает за свою часть данных и обновляется независимо:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| struct ComplexListItem: View {
let item: Item
@ObservedObject var likesViewModel: LikesViewModel
var body: some View {
VStack {
ItemHeaderView(title: item.title, date: item.date)
ItemContentView(content: item.content)
LikesCountView(viewModel: likesViewModel)
.equatable() // Обновляется только при изменении числа лайков
}
}
} |
|
Такой подход позволяет локализовать обновления только в тех частях интерфейса, где данные действительно изменились.
Профилирование производительности
Для выявления узких мест в производительности списков SwiftUI можно использовать стандартные инструменты профилирования Xcode, но также полезно добавить собственные метрики:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| struct ProfilingListView: View {
@State private var lastRenderTime = Date()
@State private var renderDurations: [TimeInterval] = []
var body: some View {
List {
ForEach(items) { item in
ItemView(item: item)
.background(RenderProfiler())
}
}
.overlay(
VStack {
Text("Среднее время: \(averageRenderTime) мс")
Text("Макс. время: \(maxRenderTime) мс")
}
.padding()
.background(Color.black.opacity(0.7))
.foregroundColor(.white)
.cornerRadius(8)
.padding(),
alignment: .bottom
)
}
var averageRenderTime: String {
let avg = renderDurations.reduce(0, +) / Double(max(1, renderDurations.count))
return String(format: "%.2f", avg * 1000)
}
var maxRenderTime: String {
return String(format: "%.2f", (renderDurations.max() ?? 0) * 1000)
}
struct RenderProfiler: View {
@State private var startTime = Date()
var body: some View {
Color.clear
.onAppear {
startTime = Date()
}
.onDisappear {
let duration = Date().timeIntervalSince(startTime)
// Сохранение метрики
}
}
}
} |
|
Эти метрики помогут выявить элементы списка, которые замедляют отрисовку, и сосредоточить усилия по оптимизации на проблемных местах.
Фильтрация и сортировка данных в реальном времени
При разработке приложений с большими наборами данных важно предоставить пользователям инструменты для быстрого поиска нужной информации. Сочетание фильтрации, сортировки и реактивного обновления UI создаёт плавный и отзывчивый пользовательский опыт:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| struct FilterableSortableListView: View {
@State private var searchText = ""
@State private var sortOrder: SortOrder = .nameAscending
enum SortOrder {
case nameAscending, nameDescending, dateNewest, dateOldest
}
var filteredAndSortedItems: [Item] {
let filtered = searchText.isEmpty
? items
: items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
return filtered.sorted { first, second in
switch sortOrder {
case .nameAscending:
return first.name < second.name
case .nameDescending:
return first.name > second.name
case .dateNewest:
return first.date > second.date
case .dateOldest:
return first.date < second.date
}
}
}
var body: some View {
VStack {
Picker("Сортировка", selection: $sortOrder) {
Text("По имени (А-Я)").tag(SortOrder.nameAscending)
Text("По имени (Я-А)").tag(SortOrder.nameDescending)
Text("Сначала новые").tag(SortOrder.dateNewest)
Text("Сначала старые").tag(SortOrder.dateOldest)
}
.pickerStyle(.segmented)
.padding(.horizontal)
List {
ForEach(filteredAndSortedItems) { item in
ItemRow(item: item)
}
}
.searchable(text: $searchText, prompt: "Найти элемент")
.animation(.spring(), value: filteredAndSortedItems)
}
}
} |
|
Сложные жесты и перетаскивание элементов
SwiftUI предоставляет встроенную поддержку перетаскивания элементов списка через модификаторы .onMove и .onDrag/.onDrop . Для более сложных сценариев можно создать детальную реализацию:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| struct DraggableTaskList: View {
@State private var tasks: [Task] = sampleTasks
@State private var draggedTask: Task?
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
.onDrag {
self.draggedTask = task
return NSItemProvider(object: task.id.uuidString as NSString)
}
.onDrop(of: [.text], delegate: TaskDropDelegate(
item: task,
items: $tasks,
draggedItem: $draggedTask)
)
.background(draggedTask?.id == task.id ? Color.gray.opacity(0.3) : Color.clear)
}
}
}
}
struct TaskDropDelegate: DropDelegate {
let item: Task
@Binding var items: [Task]
@Binding var draggedItem: Task?
func performDrop(info: DropInfo) -> Bool {
guard let draggedItem = self.draggedItem else { return false }
if draggedItem.id != item.id {
if let from = items.firstIndex(where: { $0.id == draggedItem.id }),
let to = items.firstIndex(where: { $0.id == item.id }) {
withAnimation(.spring()) {
items.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
self.draggedItem = nil
return true
}
} |
|
Предварительная загрузка и кеширование
Стратегия предварительной загрузки (prefetching) данных улучшает восприятие скорости работы приложения, особенно при работе с сетевыми запросами:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| class CachingImageManager {
static let shared = CachingImageManager()
private var cache = NSCache<NSString, UIImage>()
func loadImage(url: URL) async -> UIImage? {
let key = NSString(string: url.absoluteString)
// Проверка кеша
if let cachedImage = cache.object(forKey: key) {
return cachedImage
}
// Загрузка из сети
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
cache.setObject(image, forKey: key)
return image
}
} catch {
print("Ошибка загрузки изображения: \(error)")
}
return nil
}
}
struct PrefetchingListView: View {
@State private var visibleItems = Set<UUID>()
@State private var prefetchedItems = Set<UUID>()
var body: some View {
List {
ForEach(items) { item in
ItemRow(item: item)
.onAppear {
// Отмечаем элемент как видимый
visibleItems.insert(item.id)
// Предзагрузка следующих элементов
prefetchNextItems(after: item)
}
.onDisappear {
visibleItems.remove(item.id)
}
}
}
}
private func prefetchNextItems(after currentItem: Item) {
guard let index = items.firstIndex(where: { $0.id == currentItem.id }) else { return }
// Предзагрузка 5 следующих элементов
let endIndex = min(index + 5, items.count - 1)
for i in (index + 1)...endIndex {
let nextItem = items[i]
if !prefetchedItems.contains(nextItem.id) {
prefetchedItems.insert(nextItem.id)
// Запуск асинхронной предзагрузки
Task {
await prefetchResources(for: nextItem)
}
}
}
}
private func prefetchResources(for item: Item) async {
// Предзагрузка изображений, данных или других ресурсов
if let imageURL = item.imageURL {
_ = await CachingImageManager.shared.loadImage(url: imageURL)
}
}
} |
|
Технические ограничения списков в SwiftUI
Несмотря на все преимущества списков в SwiftUI, они имеют ряд технических ограничений, которые необходимо учитывать при разработке сложных приложений. Понимание этих ограничений и знание обходных путей — ключевой навык для профессиональной SwiftUI-разработки. Одно из самых заметных ограничений — отсутствие полного контроля над производительностью отрисовки ячеек. В отличие от UIKit, где разработчик может детально управлять процессом отрисовки с помощью cellForRowAt и переиспользования ячеек, в SwiftUI эта часть работы делегирована фреймворку. В большинстве случаев это упрощает разработку, но при работе с очень сложными ячейками могут возникать проблемы с производтельностью.
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| // В сложных случаях можно использовать UIViewRepresentable
struct UITableViewWrapper: UIViewRepresentable {
var items: [Item]
var configure: (UITableViewCell, Item) -> Void
var select: (Item) -> Void
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
return tableView
}
func updateUIView(_ tableView: UITableView, context: Context) {
context.coordinator.items = items
context.coordinator.configure = configure
context.coordinator.select = select
tableView.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource {
var parent: UITableViewWrapper
var items: [Item] = []
var configure: (UITableViewCell, Item) -> Void = { _, _ in }
var select: (Item) -> Void = { _ in }
init(_ parent: UITableViewWrapper) {
self.parent = parent
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
configure(cell, items[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
select(items[indexPath.row])
tableView.deselectRow(at: indexPath, animated: true)
}
}
} |
|
Еще одно ограничение — сложности с реализацией нестандартного скроллинга. Например, в UIKit легко реализовать "прилипание" строк к верхнему краю или особые эффекты при прокрутке. В SwiftUI подобные возможности ограничены, хотя в последних версиях появились улучшения, такие как модификатор .scrollPosition() .
Сложные жесты тоже могут быть проблемой. Реализация жестов в SwiftUI достаточно проста, но при создании нестандартных взаимодействий (например, масштабирование ячеек при свайпе) приходится обращаться к низкоуровневым API:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| struct GestureRecognizerViewModifier: ViewModifier {
var recognizer: UIGestureRecognizer
var update: () -> Void
func body(content: Content) -> some View {
content
.overlay(
GeometryReader { geometry in
UIPanGestureRecognizerView(
recognizer: recognizer,
update: update
)
}
)
}
}
struct UIPanGestureRecognizerView: UIViewRepresentable {
var recognizer: UIGestureRecognizer
var update: () -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .clear
recognizer.addTarget(context.coordinator, action: #selector(Coordinator.gestureChanged))
view.addGestureRecognizer(recognizer)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
context.coordinator.update = update
}
func makeCoordinator() -> Coordinator {
Coordinator(update: update)
}
class Coordinator: NSObject {
var update: () -> Void
init(update: @escaping () -> Void) {
self.update = update
}
@objc func gestureChanged() {
update()
}
}
} |
|
Работа с большими наборами данных — ещё одна потенциальная проблема. Хотя SwiftUI предлагает ленивую загрузку через LazyVStack и List , сложные списки с многоуровневой фильтрацией могут работать не так эффективно, как их аналоги в UIKit. Для преодоления этих ограничений часто применяют гибридный подход, сочетая SwiftUI и UIKit в одном приложении. Это позволяет использовать преимущества обоих фреймворков: декларативный синтаксис SwiftUI для простых интерфейсов и мощные возможности UIKit для сложных компонентов.
При реализации интерфейсов drag-and-drop в SwiftUI также существуют определённые ограничения. Стандартные модификаторы .onDrag и .onDrop предоставляют базовую функциональность, но для создания полноценного пользовательского опыта перетаскивания элементов между различными списками или контейнерами может потребоваться значительное количество дополнительного кода:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| struct DragDropBetweenListsView: View {
@State private var todoItems = [Item(name: "Задача 1"), Item(name: "Задача 2")]
@State private var doneItems = [Item(name: "Выполнено 1")]
@State private var draggingItem: Item?
var body: some View {
HStack {
// Список задач
TaskListView(
title: "Задачи",
items: $todoItems,
activeItem: $draggingItem,
dropAction: { item in
if let index = doneItems.firstIndex(where: { $0.id == item.id }) {
doneItems.remove(at: index)
todoItems.append(item)
return true
}
return false
}
)
// Список выполненных задач
TaskListView(
title: "Выполнено",
items: $doneItems,
activeItem: $draggingItem,
dropAction: { item in
if let index = todoItems.firstIndex(where: { $0.id == item.id }) {
todoItems.remove(at: index)
doneItems.append(item)
return true
}
return false
}
)
}
.padding()
}
} |
|
Создание масштабируемых и переиспользуемых компонентов списков для корпоративных приложений — ещё одна область, где ограничения SwiftUI становятся очевидными. В крупных проектах с множеством различных типов списков важно создать архитектуру, которая позволит повторно использовать код и поддерживать единообразие интерфейса:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // Протокол для элементов списка
protocol ListItemModel: Identifiable {
var title: String { get }
var subtitle: String? { get }
var iconName: String? { get }
}
// Базовое представление для любого элемента списка
struct GenericListItemView<Model: ListItemModel>: View {
let model: Model
let action: (Model) -> Void
var body: some View {
Button(action: { action(model) }) {
HStack {
if let iconName = model.iconName {
Image(systemName: iconName)
.frame(width: 24, height: 24)
}
VStack(alignment: .leading) {
Text(model.title)
.font(.headline)
if let subtitle = model.subtitle {
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
Spacer()
}
.padding(.vertical, 8)
}
.buttonStyle(.plain)
}
} |
|
Отсутствие встроенных возможностей для создания sticky headers (заголовков, остающихся видимыми при прокрутке) — это ещё одно ограничение, хотя в последних версиях SwiftUI появились некоторые улучшения. Для реализации такого поведения часто приходится обращаться к UIKit или использовать сторонние библиотеки.
Ещё одно ограничение — сложность создания нестандартных анимаций для элементов списка. Например, реализация "расширяющейся" ячейки, которая увеличивается при нажатии, может потребовать комбинации множества модификаторов и состояний:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| struct ExpandableListItemView: View {
let item: Item
@State private var isExpanded = false
var body: some View {
VStack(alignment: .leading) {
Button(action: { withAnimation(.spring()) { isExpanded.toggle() } }) {
HStack {
Text(item.title)
.font(.headline)
Spacer()
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
}
}
.buttonStyle(.plain)
if isExpanded {
Text(item.description)
.font(.body)
.padding(.top, 8)
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1)
.padding(.horizontal)
}
} |
|
Для преодоления этих и других ограничений разработчики часто создают собственные абстракции, которые инкапсулируют сложную логику и предоставляют простой интерфейс для использования в различных частях приложения. Такой подход позволяет максимально использовать преимущества SwiftUI, минимизируя влияние его ограничений.
Практические примеры
Теория приобретает настоящую ценность только при её применении на практике. Рассмотрим несколько полезных примеров, которые демонстрируют решение типичных задач при работе со списками в SwiftUI.
Бесконечная прокрутка с подгрузкой данных
Одна из самых востребованных функций в современных приложениях — бесконечная прокрутка с подгрузкой новых данных при достижении конца списка. Вот элегантная реализация такого списка:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| struct InfiniteScrollView: View {
@StateObject private var viewModel = FeedViewModel()
var body: some View {
List {
ForEach(viewModel.posts) { post in
PostView(post: post)
.onAppear {
// Проверяем, дошёл ли пользователь до последних элементов
if post.id == viewModel.posts.last?.id && !viewModel.isLoading {
Task {
await viewModel.loadMorePosts()
}
}
}
}
if viewModel.isLoading {
HStack {
Spacer()
ProgressView()
.padding()
Spacer()
}
.listRowSeparator(.hidden)
}
}
.refreshable {
await viewModel.refreshFeed()
}
}
}
class FeedViewModel: ObservableObject {
@Published var posts: [Post] = []
@Published var isLoading = false
private var currentPage = 1
func loadMorePosts() async {
guard !isLoading else { return }
await MainActor.run { self.isLoading = true }
do {
let newPosts = try await fetchPosts(page: currentPage)
await MainActor.run {
self.posts.append(contentsOf: newPosts)
self.currentPage += 1
self.isLoading = false
}
} catch {
await MainActor.run { self.isLoading = false }
print("Ошибка загрузки: \(error)")
}
}
func refreshFeed() async {
currentPage = 1
await MainActor.run {
isLoading = true
posts = []
}
await loadMorePosts()
}
private func fetchPosts(page: Int) async throws -> [Post] {
// Эмуляция сетевого запроса
try await Task.sleep(nanoseconds: 1_000_000_000)
return (1...10).map { Post(id: UUID(), title: "Пост \(page * 10 + $0)") }
}
} |
|
Ключевой трюк здесь — использование метода .onAppear() для отслеживания момента, когда пользователь дошёл до конца доступных данных. Индикатор загрузки появляется только при активной подгрузке, что создаёт плавный пользовательский опыт.
Сворачиваемые секции с анимацией
Интерактивные сворачиваемые секции делают длинные списки более управляемыми и организованными:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| struct CollapsibleSectionsList: View {
@State private var collapsedSections = Set<String>()
let categories = [
"Финансы": ["Банковское приложение", "Мониторинг расходов", "Инвестиционный трекер"],
"Здоровье": ["Фитнес-трекер", "Медитация", "Напоминания о лекарствах"],
"Продуктивность": ["Список задач", "Календарь", "Заметки", "Привычки"]
]
var body: some View {
List {
ForEach(Array(categories.keys.sorted()), id: \.self) { category in
Section {
if !collapsedSections.contains(category) {
ForEach(categories[category]!, id: \.self) { app in
Text(app)
}
.transition(.opacity.combined(with: .move(edge: .top)))
}
} header: {
HStack {
Text(category)
Spacer()
Button(action: {
withAnimation(.spring()) {
if collapsedSections.contains(category) {
collapsedSections.remove(category)
} else {
collapsedSections.insert(category)
}
}
}) {
Image(systemName: collapsedSections.contains(category)
? "chevron.down" : "chevron.up")
}
}
}
}
}
.listStyle(.insetGrouped)
}
} |
|
Этот пример объединяет несколько концепций: управление состоянием через Set , условное отображение элементов и плавные анимации переходов. Обратите внимание на использование .transition() для создания эффекта появления и исчезновения содержимого секций.
Список с перетаскиваемыми элементами и редактируемым порядком
Реализация списка с возможностью изменения порядка элементов — одна из самых частых задач в мобильной разработке. SwiftUI предлагает элегантное решение с помощью модификатора .onMove :
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| struct ReorderableListView: View {
@State private var tasks = [
"Обновить дизайн приложения",
"Исправить баги после тестирования",
"Добавить новые функции",
"Написать документацию",
"Подготовить релиз"
]
var body: some View {
NavigationView {
List {
ForEach(tasks, id: \.self) { task in
Text(task)
}
.onMove(perform: moveTask)
}
.toolbar {
EditButton()
}
.navigationTitle("Задачи проекта")
}
}
func moveTask(from source: IndexSet, to destination: Int) {
tasks.move(fromOffsets: source, toOffset: destination)
}
} |
|
Этот пример демонстрирует использование стандартного режима редактирования для перемещения элементов. EditButton() автоматически переключает список в режим редактирования, а модификатор .onMove обрабатывает перемещение элементов.
Свайп-действия для элементов списка
Свайп-действия — одна из самых популярных функций в iOS-приложениях. SwiftUI предлагает встроенную поддержку таких действий с помощью модификаторов .swipeActions :
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| struct SwipeActionsListView: View {
@State private var emails = [
Email(subject: "Обновление проекта", isRead: false, isFlagged: false),
Email(subject: "Встреча в четверг", isRead: true, isFlagged: true),
Email(subject: "Отчёт за квартал", isRead: false, isFlagged: false)
]
var body: some View {
List {
ForEach(emails) { email in
HStack {
Circle()
.fill(email.isRead ? Color.gray : Color.blue)
.frame(width: 10, height: 10)
Text(email.subject)
.fontWeight(email.isRead ? .regular : .bold)
Spacer()
if email.isFlagged {
Image(systemName: "flag.fill")
.foregroundColor(.red)
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteEmail(email)
} label: {
Label("Удалить", systemImage: "trash")
}
Button {
toggleFlag(for: email)
} label: {
Label("Флаг", systemImage: "flag")
}
.tint(.orange)
}
.swipeActions(edge: .leading) {
Button {
toggleReadStatus(for: email)
} label: {
if email.isRead {
Label("Непрочитано", systemImage: "envelope.badge")
} else {
Label("Прочитано", systemImage: "envelope.open")
}
}
.tint(.blue)
}
}
}
}
func toggleFlag(for email: Email) {
if let index = emails.firstIndex(where: { $0.id == email.id }) {
emails[index].isFlagged.toggle()
}
}
func toggleReadStatus(for email: Email) {
if let index = emails.firstIndex(where: { $0.id == email.id }) {
emails[index].isRead.toggle()
}
}
func deleteEmail(_ email: Email) {
emails.removeAll { $0.id == email.id }
}
}
struct Email: Identifiable {
let id = UUID()
let subject: String
var isRead: Bool
var isFlagged: Bool
} |
|
В этом примере реализованы действия по свайпу вправо (удаление и пометка флагом) и влево (отметка о прочтении). Обратите внимание на использование параметра role: .destructive для действия удаления, которое автоматически придаёт кнопке красный цвет.
Список с динамическим изменением стиля при прокрутке
Создание интерфейсов, которые динамически меняются при прокрутке, добавляет интерактивности и улучшает пользовательский опыт:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| struct DynamicStyleListView: View {
@State private var scrollOffset: CGFloat = 0
let items = (1...30).map { "Элемент \($0)" }
var body: some View {
ScrollViewReader { proxy in
ZStack(alignment: .top) {
// Фон, который меняется при прокрутке
LinearGradient(
gradient: Gradient(colors: [.blue, .purple]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.opacity(scrollOffset > 0 ? 0.3 - min(scrollOffset / 500, 0.3) : 0.3)
.ignoresSafeArea()
VStack {
// Заголовок с изменением размера при прокрутке
Text("Динамический список")
.font(.system(size: 28 - min(scrollOffset / 20, 10)))
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 20 - min(scrollOffset / 10, 20))
.padding(.bottom, 10)
.background(Color.white.opacity(min(scrollOffset / 100, 0.5)))
// Список с отслеживанием прокрутки
ScrollView {
LazyVStack {
ForEach(items, id: \.self) { item in
Text(item)
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.white)
.shadow(color: .black.opacity(0.1), radius: 3)
)
.padding(.horizontal)
.padding(.vertical, 4)
}
}
.background(GeometryReader { geometry in
Color.clear.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("scrollView")).minY
)
})
}
.coordinateSpace(name: "scrollView")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
scrollOffset = -value
}
}
}
}
}
}
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
} |
|
Этот пример демонстрирует использование системы предпочтений (preference) SwiftUI для отслеживания положения прокрутки и динамического изменения внешнего вида интерфейса. Заголовок уменьшается, а фоновый градиент становится менее ярким по мере прокрутки списка.
запросы к api в swiftui и uikit Прошу не бросаться гнилыми помидорами я только начал изучать разработку под ios. Вопрос такой:... Передать результат запроса в SwiftUI Как передать полученные данные в SwiftUI, чтобы на эмуляторе их вывести а не в консоли?
... swiftui self self.items = try container.decode(.self, forKey: .items)
Скажите пожалуйста что означает запись... дизайн swiftui Как делаются интерфейсы приложений типа duolingo на swiftui? Я имею ввиду графические элементы.
Перспективы и возможности использования списков в SwiftUI
Завершая наше погружение в мир списков SwiftUI, стоит обратить внимание на перспективы дальнейшего развития этого компонента и текущие сценарии его использования в реальных проектах. SwiftUI продолжает эволюционировать с каждым обновлением iOS, и списки, как один из центральных элементов большинства приложений, получают постоянные улучшения.
Если посмотреть на тенденции в разработке мобильных интерфейсов, можно заметить смещение фокуса в сторону более отзывчивых и интерактивных списков. Пользователи уже привыкли к плавным анимациям, тактильной обратной связи и хорошо продуманным микро-взаимодействиям при работе со списками. SwiftUI делает создание таких интерфейсов значительно проще по сравнению с императивными подходами вроде UIKit. Одно из интересных направлений развития — интеграция списков с другими системами SwiftUI. Например, комбинация списков с новыми API для навигации позволяет создавать сложные иерархические интерфейсы с минимальными усилиями:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| struct ContentNavigationView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List(categories) { category in
NavigationLink(value: category) {
CategoryRow(category: category)
}
}
.navigationDestination(for: Category.self) { category in
ItemsList(category: category)
}
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
} |
|
Этот пример демонстрирует, как новая навигационная система SwiftUI в сочетании со списками упрощает создание многоуровневых структур данных с типизированными переходами между экранами.
Искуственный интеллект и машинное обучение тоже оказывают влияние на то, как разработчики проектируют списки в своих приложениях. Например, можно использовать ML для интеллектуальной сортировки или группировки элементов в списке:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| struct SmartList: View {
@StateObject private var viewModel = SmartListViewModel()
var body: some View {
List {
ForEach(viewModel.categorizedItems.keys.sorted(), id: \.self) { category in
Section(header: Text(category)) {
ForEach(viewModel.categorizedItems[category]!) { item in
Text(item.title)
}
}
}
}
.onAppear {
viewModel.categorizeThroughML()
}
}
}
class SmartListViewModel: ObservableObject {
@Published var categorizedItems: [String: [Item]] = [:]
private var items: [Item] = sampleItems
func categorizeThroughML() {
// Интеграция с Core ML для интеллектуальной категоризации
// items.forEach { item in
// let predictedCategory = mlModel.predict(item)
// if categorizedItems[predictedCategory] == nil {
// categorizedItems[predictedCategory] = []
// }
// categorizedItems[predictedCategory]!.append(item)
// }
}
} |
|
Доступность и эргономика
Отдельного внимания заслуживает вопрос доступности списков в SwiftUI. Apple уделяет большое внимание тому, чтобы iOS-приложения были доступны всем пользователям, включая людей с ограниченными возможностями. SwiftUI упрощает реализацию доступных интерфейсов за счёт встроенной поддержки VoiceOver, Dynamic Type и других ассистивных технологий.
Для списков особенно важно обеспечить хорошую поддержку VoiceOver и корректное озвучивание элементов:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
| List(items) { item in
HStack {
Image(systemName: item.iconName)
VStack(alignment: .leading) {
Text(item.title)
Text(item.subtitle)
.font(.caption)
}
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(item.title), \(item.subtitle)")
.accessibilityHint("Нажмите для просмотра подробностей")
} |
|
Модификатор .accessibilityElement(children: .combine) объединяет все дочерние элементы в один доступный элемент, что улучшает навигацию для пользователей с VoiceOver. .accessibilityLabel и .accessibilityHint предоставляют дополнительный контекст.
Важными вопросами при разработке списков остаются также их эргономика и удобство использования. Исследования показывают, что пользователи смартфонов всё чаще взаимодействуют с устройством одной рукой, что делает верхнюю часть экрана труднодоступной. Эту проблему можно решить, размещая наиболее важные или часто используемые элементы управления в нижней части экрана:
Swift
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| struct ErgoList: View {
@State private var showActionSheet = false
var body: some View {
ZStack(alignment: .bottom) {
List(items) { item in
ItemRow(item: item)
}
Button(action: { showActionSheet = true }) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Добавить")
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(25)
.shadow(radius: 3)
}
.padding(.bottom, 16)
}
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Действия"), buttons: [
.default(Text("Создать новый элемент")) { /* ... */ },
.default(Text("Импортировать")) { /* ... */ },
.cancel()
])
}
}
} |
|
В этом примере кнопка добавления размещена в нижней части экрана, что делает её легкодоступной для большинства пользователей, а действия вызываются через ActionSheet , который также открывается снизу. Списки в SwiftUI представляют собой не просто способ отображения данных, но и мощный инструмент для организации пользовательского опыта. От простых перечислений до сложных иерархических структур с переиспользуемыми компонентами — всё это можно реализовать с минимальными усилиями благодаря декларативной природе SwiftUI.
Декларативный подход SwiftUI к построению интерфейсов постоянно доказывает свою эффективность в реальных проектах. Разработчики отмечают значительное сокращение объёма кода и времени разработки, особенно при создании сложных списков с различными типами данных и интерактивными элементами. По мере того как SwiftUI продолжает эволюционировать, мы можем ожидать появления ещё более мощных и гибких инструментов для работы со списками. Будущие версии iOS наверняка принесут новые модификаторы, стили и возможности, которые сделают разработку интерфейсов еще более простой и приятной.
Напоследок хочется отметить, что при всех преимуществах декларативного подхода SwiftUI, осознанное использование списков требует понимания не только синтаксиса, но и принципов, лежащих в основе фреймворка. Знание внутренних механизмов работы списков, оптимизации производительности и грамотного структурирования данных позволит создавать приложения, которые не только красивы, но и эффективны.
SwiftUI. ScrollView с DragGesture Использую swiftUI. У меня экран (VStack) появляется снизу вверх. На VStack расположен scrollview,... Стилизация input-ов Ребята привет!
Помогите пожалуйста со стилизаций input-ов. Input должен выглядеть как линия.... Интегрировать UIKit в SwiftUI или наоборот? Я начинающий разработчик под Apple. Насколько я понимаю, UIKit и SwiftUI – это полностью... Работа с изображениями SwiftUI Всем добрый день!
Вот в продолжении обучения по теме swiftUI столкнулся со следующей проблемой:... SwiftUI текущий календарь Добрый день.
Изучаю SwiftUI и столкнулся с ошибкой в строке:
let calendar = Calendar.current... SwiftUI <Клавиатура> Версия XCode 12.5.1
Упёрся в то что как-то не получается показать клавиатуру в самом простом... swiftui foreach Скажите пожалуйста где ошибка:
import SwiftUI
struct ContentView: View {
var body: some... SwiftUI как сделать много языковой интерфейс Я новичок, изучаю SwiftUI. Подскажите как сделать много языковой интерфейс. Буду благодарен за... SwiftUI шибка при получении данных с Апи Всем привет. Понимаю, вопрос простой, но что то не могу найти нормальной информации в инэте. Начал... Создание и сохранение линейных списков. Обработка линейных списков. (Нужно доделать немного) Задание:
Тип линейного списка: очередь
Количество элементов списка: 10
Поля информационной... Копирование данных двумерного массива состоящего из списков в список списков. Для нахождения МСТ (минимального остового дерева) написал функцию, которая принимает масив - гарф.... Список списков списков чисел Всем привет)
Задание: l1 - список списков списков чисел.
Необходимо получить список чисел l2,...
|