Вашему вниманию предлагается статья на тему SwiftUI. В ней мы рассмотрим быстрый и простой способ создания такого компонента SwiftUI, как всплывающее сообщение (англ. toast).
Компонент
Toast
— это сообщение, которое появляется в нижней части интерфейса, не нарушая рабочий процесс. Оно обеспечивает моментальную обратную связь о результате выполнения действия.
Всплывающие сообщения предназначены только для подтверждений, простых уведомлений и низкоприоритетных предупреждений, не прерывающих работу пользователя.
1. Создание представления Toast
Сначала создаем файл представления SwiftUI и называем его FancyToastView
или как угодно. Это основное представление для всплывающего сообщения, которое мы показываем пользователю:
struct FancyToastView: View {
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
Image(systemName: "info.circle.fill")
.foregroundColor(Color.red)
VStack(alignment: .leading) {
Text("Error")
.font(.system(size: 14, weight: .semibold))
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
.font(.system(size: 12))
.foregroundColor(Color.black.opacity(0.6))
}
Spacer(minLength: 10)
Button {
//TODO
} label: {
Image(systemName: "xmark")
.foregroundColor(Color.black)
}
}
.padding()
}
.background(Color.white)
.overlay(
Rectangle()
.fill(Color.red)
.frame(width: 6)
.clipped()
, alignment: .leading
)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 1)
.padding(.horizontal, 16)
}
}
Как видно по содержанию кода, всплывающее сообщение состоит из иконок, заголовков, сообщений и кнопок закрытия, расположенных горизонтально. Для иконок применяется SFSymbol
, что снимает необходимость импорта активов (англ. assets) для выполнения кода.
Кроме того, с помощью операции наложения мы создаем боковой список. При выполнении кода получаем следующее представление:
А как же динамический контент во всплывающем сообщении? Об этом далее.
2. Создание стиля enum и модели всплывающего сообщения
Создадим 4 наиболее распространенных стиля оформления всплывающих сообщений: info
(информация), error
(ошибка), warning
(предупреждение) и success
(успешность выполнения). Они отличаются только иконками и цветом темы:
enum FancyToastStyle {
case error
case warning
case success
case info
}
extension FancyToastStyle {
var themeColor: Color {
switch self {
case .error: return Color.red
case .warning: return Color.orange
case .info: return Color.blue
case .success: return Color.green
}
}
var iconFileName: String {
switch self {
case .info: return "info.circle.fill"
case .warning: return "exclamationmark.triangle.fill"
case .success: return "checkmark.circle.fill"
case .error: return "xmark.circle.fill"
}
}
}
Вот такой простой код. У нас 4 типа, для каждого из которых с помощью вычисляемого свойства получаем цвет темы и имя файла иконки.
Далее создаем модель для настройки содержимого всплывающего сообщения:
struct FancyToast: Equatable {
var type: FancyToastStyle
var title: String
var message: String
var duration: Double = 3
}
Модель содержит: 1) ранее созданный тип enum
, заголовок и содержимое сообщения, отображаемое во всплывающем окне; 2) срок действия duration
, значение которого определяет длительность отображения сообщения и время его автоматического закрытия. В модели также используется Equatable
для разграничения одного всплывающего сообщения от другого.
3. Интеграция представлений с моделями
Следующий этап — интеграция основного представления с моделью, что позволит сделать содержимое сообщения динамическим:
struct FancyToastView: View {
var type: FancyToastStyle
var title: String
var message: String
var onCancelTapped: (() -> Void)
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
Image(systemName: type.iconFileName)
.foregroundColor(type.themeColor)
VStack(alignment: .leading) {
Text(title)
.font(.system(size: 14, weight: .semibold))
Text(message)
.font(.system(size: 12))
.foregroundColor(Color.black.opacity(0.6))
}
Spacer(minLength: 10)
Button {
onCancelTapped()
} label: {
Image(systemName: "xmark")
.foregroundColor(Color.black)
}
}
.padding()
}
.background(Color.white)
.overlay(
Rectangle()
.fill(type.themeColor)
.frame(width: 6)
.clipped()
, alignment: .leading
)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(8)
.shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 1)
.padding(.horizontal, 16)
}
}
Этот код не сильно отличается от предыдущего. Жестко закодированное значение мы просто меняем на значение из модели. Обратите внимание на еще одно отличие: для действия кнопки отмены задействуется функция обратного вызова. Так мы получаем родительское представление, которое выполняет логику для закрытия всплывающего сообщения.
Попробуем выполнить код, вызвав представление следующим образом:
struct FancyToastView_Previews: PreviewProvider {
static var previews: some View {
VStack {
FancyToastView(
type: .error,
title: "Error",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ") {}
FancyToastView(
type: .info,
title: "Info",
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ") {}
}
}
}
Получаем вот такое представление:
На шаг ближе к цели…
4. Создание ViewModifier для показа всплывающего сообщения
Всплывающие сообщения должны отображаться легко, бесперебойно и многократно, потому что они будут использоваться во многих представлениях приложения. Есть один сложный и утомительный вариант: создать ZStack
и затем вручную разместить в каждом представлении всплывающее сообщение в представлении tp
. Но мы пойдем другим путем и решим задачу с помощью ViewModifier
!
struct FancyToastModifier: ViewModifier {
@Binding var toast: FancyToast?
@State private var workItem: DispatchWorkItem?
func body(content: Content) -> some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(
ZStack {
mainToastView()
.offset(y: -30)
}.animation(.spring(), value: toast)
)
.onChange(of: toast) { value in
showToast()
}
}
@ViewBuilder func mainToastView() -> some View {
if let toast = toast {
VStack {
Spacer()
FancyToastView(
type: toast.type,
title: toast.title,
message: toast.message) {
dismissToast()
}
}
.transition(.move(edge: .bottom))
}
}
private func showToast() {
guard let toast = toast else { return }
UIImpactFeedbackGenerator(style: .light).impactOccurred()
if toast.duration > 0 {
workItem?.cancel()
let task = DispatchWorkItem {
dismissToast()
}
workItem = task
DispatchQueue.main.asyncAfter(deadline: .now() + toast.duration, execute: task)
}
}
private func dismissToast() {
withAnimation {
toast = nil
}
workItem?.cancel()
workItem = nil
}
}
Возможно, перед вами самый сложный и вызывающий вопросы код из всех ранее рассмотренных. Сейчас мы с ним разберемся.
В этом модификаторе представления есть 2 переменные с разной оберткой свойств.
Первая переменная — toast
, модель всплывающего сообщения, которую мы привязываем к представлению Toast
с помощью обертки свойства @Binding
. Это позволяет менять значение изнутри, а также напрямую отражать изменение значения извне.
Вторая переменная — workItem
, представляющий собой DispatchWorkItem
. Он закрывает всплывающее сообщение по истечении установленного времени его действия.
Кроме того, у нас в наличии 2 функции представления и 2 логические функции. Нет смысла подробно объяснять код UI, так как наверняка все вы владеете основами SwiftUI. В вышеуказанном коде нет ничего сложного, только базовые знания.
В коде мы устанавливаем размер содержимого области сообщения равным бесконечности, что означает его соответствие экрану устройства. Компонент Toast
будет появляться в нижней части экрана, как и планировалось. Далее применяем наложение и ZStack
для размещения компонента Toast
поверх содержимого. Кроме того, используем модификатор onChange
для выполнения функции, отвечающей за отображение всплывающего сообщения.
Что касается функции showToast
, здесь тоже все предельно просто. Вызываем функцию UIImpactFeedbackGenerator
, которая показывает всплывающее сообщение. После этого устанавливаем задачу workItem
и выполняем ее по истечении заданного времени действия.
С последней функцией dismissToast
тоже все ясно. Устанавливаем nil
для привязки всплывающего сообщения, потом отменяем и устанавливаем nil
для workItem
, чтобы предотвратить утечки памяти.
Почти готово
На последнем этапе упрощаем и наводим красоту. С этой целью создаем функцию в расширении View
для применения компонента в более свойственной для SwiftUI манере.
extension View {
func toastView(toast: Binding<FancyToast?>) -> some View {
self.modifier(FancyToastModifier(toast: toast))
}
}
Наконец, все компоненты созданы и готовы к работе!
Пример использования компонента:
struct ContentView: View {
@State private var toast: FancyToast? = nil
var body: some View {
VStack {
Button {
toast = FancyToast(type: .info, title: "Toast info", message: "Toast message")
} label: {
Text("Run")
}
}
.toastView(toast: $toast)
}
}
Выполняем и получаем следующий результат:
Заключение
Всплывающие сообщения — это довольно распространенная функциональность в приложениях. В статье мы рассмотрели легкий способ создания компонента Toast
. Он будет интересен тем, кто до этого момента не знал, как создаются подобные компоненты в SwiftUI.
Далее вы можете самостоятельно изучить вопрос о том, как создать систему очередей при необходимости отобразить несколько всплывающих сообщений.
Читайте также:
- 3 эффективные новинки Swift с WWDC 2022
- Как создать пользовательскую поисковую панель SwiftUI с LazyVStack
- 16 полезных расширений для SwiftUI
Читайте нас в Telegram, VK и Дзен
Перевод статьи Farhan Adji: Create a Fancy Toast Component Using SwiftUI