Продолжаем серию статей, посвященных нововведениям в SwiftUI 3.0, которые будут доступны в iOS 15. Сегодня рассмотрим пример того, как можно обновлять списки свайпом сверху вниз (Pull to refresh), как реализовать меню пользовательских действий в списках, а так же рассмотрим пример реализации строки поиска в навигейшн баре.
SwiftUI 3.0. Pull to refresh
За обновление контента при работе с типом List в SwiftUI отвечает модификатор refreshable(), который работает по принципу "Pull to refresh". Это значит, что для обновления контента в представлении List нужно потянуть список сверху вниз. Это в свою очередь позволит вызывать метод, привязанный к жесту свайпа и отобразит анимацию загрузки данных:
struct ContentView: View {
var body: some View {
NavigationView {
List(1..<100) { row in
Text("Row \(row)")
}
.refreshable {
print("Do your refresh work here")
}
}
}
}
Код, выполняемый при вызове refreshable() работает в асинхронном контексте, поэтому это идеальное место для выполнения сетевых запросов:
struct NewsItem: Decodable, Identifiable {
let id: Int
let title: String
let strap: String
}
struct ContentView: View {
@State private var news = [
NewsItem(id: 0, title: "Want the latest news?", strap: "Pull to refresh!")
]
var body: some View {
NavigationView {
List(news) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.strap)
.foregroundColor(.secondary)
}
}
.refreshable {
do {
// Fetch and decode JSON into news items
let url = URL(string: "https://www.hackingwithswift.com/samples/news-1.json")!
let (data, _) = try await URLSession.shared.data(from: url)
news = try JSONDecoder().decode([NewsItem].self, from: data)
} catch {
// Something went wrong; clear the news
news = []
}
}
}
}
}
SwiftUI 3.0. Search Bar
Благодаря модификатору searchable() строку поиска можно интегрировать непосредственно в навигейшин бар. Search Bar будет автоматически появляться и исчезать при прокручивании контента при работе со списком:
struct ContentView: View {
@State private var searchText = ""
var body: some View {
NavigationView {
Text("Searching for \(searchText)")
.searchable(text: $searchText)
.navigationTitle("Searchable Example")
}
}
}
В строке поиска также можно отображать подсказку:
struct ContentView: View {
@State private var searchText = ""
var body: some View {
NavigationView {
Text("Searching for \(searchText)")
.searchable(text: $searchText, prompt: "Look for something")
.navigationTitle("Searchable Example")
}
}
}
На практике данный элемент чаще всего используется для фильтрации списка данных:
struct ContentView: View {
let names = ["Holly", "Josh", "Rhonda", "Ted"]
@State private var searchText = ""
var body: some View {
NavigationView {
List(searchResults, id: \.self) { name in
NavigationLink(destination: Text(name)) {
Text(name)
}
}
.searchable(text: $searchText)
.navigationTitle("Contacts")
}
}
var searchResults: [String] {
if searchText.isEmpty {
return names
} else {
return names.filter { $0.contains(searchText) }
}
}
}
При работе со списком панель поиска по умолчанию скрыта и что бы отобразить её список нужно потянуть сверху вниз.
Для более продвинутого использования searchchable() позволяет отображать список предложений из выпадающего меню с автоподстановкой наиболее подходящих вариантов, чтобы их не приходилось набирать полностью с клавиатуры. Для этого необходимо воспользоваться модификатором searchCompletion(), вызвав его у результата, который возвращает сам Search Bar:
struct ContentView: View {
let names = ["Holly", "Josh", "Rhonda", "Ted"]
@State private var searchText = ""
var body: some View {
NavigationView {
List {
ForEach(searchResults, id: \.self) { name in
NavigationLink(destination: Text(name)) {
Text(name)
}
}
}
.searchable(text: $searchText) {
ForEach(searchResults, id: \.self) { result in
Text("Are you looking for \(result)?").searchCompletion(result)
}
}
.navigationTitle("Contacts")
}
}
var searchResults: [String] {
if searchText.isEmpty {
return names
} else {
return names.filter { $0.contains(searchText) }
}
}
}
Теперь если активировать строку поиска, то будет раскрыт список всех возможных вариантов в виде вопроса «Вы ищите Холли?» и так для каждого имени. Пользователь может начать набирать искомое имя при помощи клавиатуры и тогда список вопросов будет отфильтровываться в соответсвии с тем, что набрал пользователь, либо же можно воспользоваться готовым предложением, выбрав имя из предлагаемого списка.
SwiftUI 3.0. List: Меню пользовательских действий для строк
Модификатор swipeActions() позволяет добавлять пользовательские действия для строк при работе со списками. Меню пользовательских действий можно вызывать свайпом по строке, как слева, так и справа. Кроме того можно назначить действие по умолчанию на длинный свайп. Давайте рассмотрим несколько примеров того, как это можно реализовать в коде:
List {
Text("Pepperoni pizza")
.swipeActions {
Button("Order") {
print("Awesome!")
}
.tint(.green)
}
Text("Pepperoni with pineapple")
.swipeActions {
Button("Burn") {
print("Right on!")
}
.tint(.red)
}
}
В этом примере у нас есть две статические строки, на которые назначены два пользовательских действия по свайпу справа налево. Если не задать цвета для кнопок самостоятельно, то по умолчанию кнопки будут серого цвета.
Совет: если кнопка меню должна отвечать за удаление, то в этом случае для кнопки надо использовать инициализатор для создания деструктивных кнопок: Button(role: .destructive). В этом случае кнопка будет красного цвета.
По умолчанию первое назначенное действие будет являться действием по умолчанию при длинном свайпе. Если необходимо отключить эту возможность, то для свойства allowsFullSwipe необходимо присвоить значение false:
struct ContentView: View {
let friends = ["Antoine", "Bas", "Curt", "Dave", "Erica"]
var body: some View {
NavigationView {
List {
ForEach(friends, id: \.self) { friend in
Text(friend)
.swipeActions(allowsFullSwipe: false) {
Button {
print("Muting conversation")
} label: {
Label("Mute", systemImage: "bell.slash.fill")
}
.tint(.indigo)
Button(role: .destructive) {
print("Deleting conversation")
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
}
}
}
SwiftUI достаточно умен, что бы адаптировать лейбл для отображения только иконки пользовательского действия без текста, но при этом название лейбла все равно остается доступным для функции VoiceOver.
Если нужно определить конкретную сторону строки с которой должны отображаться определенные пользовательские действия, то для этого достаточно определить параметр edge при вызове модификатора swipeActions():
struct ContentView: View {
@State private var total = 0
var body: some View {
NavigationView {
List {
ForEach(1..<100) { item in
Text("\(item)")
.swipeActions(edge: .leading) {
Button {
total += item
} label: {
Label("Add \(item)", systemImage: "plus.circle")
}
.tint(.indigo)
}
.swipeActions(edge: .trailing) {
Button {
total -= item
} label: {
Label("Subtract \(item)", systemImage: "minus.circle")
}
}
}
}
.navigationTitle("Total: \(total)")
}
}
}
Для статьи мы использовали следующие источники:
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-enable-pull-to-refresh
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-search-bar-to-filter-your-data
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-custom-swipe-action-buttons-to-a-list-row