Базовое представление
По стандартному сценарию в большинстве приложений на время выполнения длительных операций возможность взаимодействия с приложением блокируется. Обычно это происходит во время скачивания какого-то контента из сети или при выполнении действий, занимающих много времени. Мы не хотим, чтобы в процессе ожидания пользователь блуждал по приложению или жал на все доступные кнопки. Стандартным решением здесь выступает размещение поверх всего представления, показывающего спиннер загрузки и/или сообщение. В коде это выглядит примерно так:
struct LoadingView: View {
var body: some View {
ZStack {
Rectangle()
.fill(.black)
.opacity(0.75)
.ignoresSafeArea()
VStack(spacing: 20) {
ProgressView()
Text("Loading...")
}
.background {
RoundedRectangle(cornerRadius: 20)
.fill(.white)
.frame(width: 200, height: 200)
}
.offset(y: -70)
}
}
}
struct ContentView: View {
@State var showLoading = false
var body: some View {
ZStack {
VStack {
Button {
showLoading = true
// Симуляция процесса...
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showLoading = false
}
} label: {
Text("Show Loading")
}
.buttonStyle(.borderedProminent)
}
.padding()
if showLoading { LoadingView() }
}
.animation(.default, value: showLoading)
}
}
Этот код вполне понятен, только обратите внимание на строку 36, где я добавил задержку сокрытия загрузочного представления для симуляции завершения процесса. Естественно, в реальном приложении вся логика будет размещена внутри ViewModel
, но в целях упрощения я поместил ее в само представление.
Вот результат:
Неплохо, но есть в этом подходе одна проблема. Каждый раз, когда вам нужно использовать LoadingView
, придется реализовывать логику показа/скрытия и добавлять представление ZStack
либо нечто аналогичное, что приведет к постоянному переписыванию одного и того же кода — не самое грамотное решение.
Как сделать по уму
Это не единственный способ отображения загрузочных представлений, и ниже я опишу более грамотную альтернативу для решения этой проблемы с помощью жизненного цикла самого приложения.
Это будет простое решение, состоящее из следующих шагов:
- Создание
@StateObject
со свойством, отслеживающим присутствие загрузки. - Добавление его в структуру
App
проекта и внедрение в качествеenvironmentObject
основного представления (обычноContentView
). - Обертывание основного представления в
ZStack
и добавление логики показа/скрытия. - Добавление
environmentObject
и подключение его в каждом представлении, требующем отображения загрузки.
Взглянем на код:
class LoaderManager: ObservableObject {
@Published var showLoader = false
}
Для отслеживания состояния загрузки я создал ObservableObject
со свойством @Published
.
@main
struct LoaderDemoApp: App {
@StateObject var loaderManager = LoaderManager()
var body: some Scene {
WindowGroup {
ZStack {
ContentView().environmentObject(loaderManager)
if loaderManager.showLoader { LoadingView() }
}
.animation(.default, value: loaderManager.showLoader)
}
}
}
Этот объект я добавил в точку входа в приложение. Поскольку мы с самого начала внедряем менеджер загрузки как environmentObject
, он будет передаваться по нисходящей в каждое дочернее представление иерархии. Любые изменения в свойстве showLoader
менеджера будут приводить к обновлению самого объекта. Как сказано в документации Apple:
При любом изменении наблюдаемого объекта объект среды делает текущее представление недействительным. Если вы объявите свойство как объект среды, обязательно установите соответствующий объект модели в вышестоящее представление путем вызова его модификатора
environmentObject(_:)
.
Последним шагом будет обновление исходного представления:
struct ContentView: View {
@EnvironmentObject var loaderManager: LoaderManager
var body: some View {
ZStack {
VStack {
Button {
loaderManager.showLoader = true
// Симуляция процесса...
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
loaderManager.showLoader = false
}
} label: {
Text("Show Loading")
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
}
Просто добавьте переменную @EnvironmentObject
и свяжите ее с логикой.
Как видите, теперь код стал короче, чище, понятнее и пригоден для повторного использования.
Доработка
Показанное выше решение работает, и это прекрасно… Но что, если нам потребуется изменить текст загрузки? Что, если нам нужно будет полностью изменить UI самого загрузчика в определенном разделе?
С помощью небольших модификаций можно решить и эту задачу.
Мы изменим нашего LoaderManager
, удалив логическое свойство и добавив перечисление, описывающее само состояние загрузки:
class LoaderManager: ObservableObject {
enum LoadingStateType: Equatable {
case hidden
case standard(message: String = "Loading...")
case glassed
}
@Published var loadingState: LoadingStateType = .hidden
}
При отсутствии загрузочного представления состояние будет .hidden
. Для стандартного случая я добавил связанное значение с предустановленной строкой в виде: “Loading…”. Я также добавил еще один тип загрузочного представления, просто чтобы показать этот вариант. Ниже я покажу простой код обоих представлений.
Теперь изменим точку входа в приложение:
@main
struct LoaderDemoApp: App {
@StateObject var loaderManager = LoaderManager()
var body: some Scene {
WindowGroup {
ZStack {
ContentView()
.environmentObject(loaderManager)
switch loaderManager.loadingState {
case .hidden:
EmptyView()
case .standard(let message):
LoadingView(message: message)
case .glassed:
LoadingViewGlassed()
}
}
.animation(.default, value: loaderManager.loadingState)
}
}
}
Логической проверки больше нет, и теперь на строке 13 у нас появился блок switch
со всеми кейсами. Обратите внимание на строку 15, где видно, что для кейса .hidden
мы будем использовать EmptyView()
. Другие кейсы .standard
и .glassed
будут иметь другие представления. Вот их код:
struct LoadingView: View {
@State var message: String
var body: some View {
ZStack {
Rectangle()
.fill(.black)
.opacity(0.75)
.ignoresSafeArea()
VStack(spacing: 20) {
ProgressView()
Text(message)
}
.background {
RoundedRectangle(cornerRadius: 20)
.fill(.white)
.frame(width: 200, height: 200)
}
.offset(y: -70)
}
}
}
struct LoadingViewGlassed: View {
var body: some View {
ZStack {
Rectangle()
.fill(Material.ultraThin)
.ignoresSafeArea()
VStack(spacing: 20) {
ProgressView()
Text("Loading...")
}
.background {
RoundedRectangle(cornerRadius: 20)
.fill(.white)
.frame(width: 200, height: 200)
}
.offset(y: -70)
}
}
}
Исходный LoadingView()
был изменен на строке 3 для получения строки сообщения, указанной в представлении Text
на строке 14.
LoadingViewGlassed()
использует классное заполнение Material.ultraThin
.
Наконец, мы можем изменить наше представление для отображения и обработки этих вариантов:
struct ContentView: View {
@EnvironmentObject var loaderManager: LoaderManager
var body: some View {
ZStack {
VStack (spacing: 20) {
Spacer()
Button {
loaderManager.loadingState = .standard()
hideLoadingWithDelay()
} label: {
Text("Show Loading")
}
.buttonStyle(.borderedProminent)
Button {
loaderManager.loadingState = .standard(message: "Downloading images...")
hideLoadingWithDelay()
} label: {
Text("Show Loading With Message")
}
.buttonStyle(.borderedProminent)
Button {
loaderManager.loadingState = .glassed
hideLoadingWithDelay()
} label: {
Text("Show Glassed Loading")
}
.buttonStyle(.borderedProminent)
}
}
}
func hideLoadingWithDelay() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
loaderManager.loadingState = .hidden
}
}
}
Обратите внимание на использование объекта loaderManager
внутри действий трех кнопок на строках 10, 18 и 26. Также заметьте процесс сокрытия загрузчика на строке 38.
И вот итоговый результат:
Читайте также:
- Реализация масштабируемого и гибкого пользовательского экрана с несколькими переключателями на Swift
- Использование SwiftUI в UIKit
- Как создать пользовательскую поисковую панель SwiftUI с LazyVStack
Читайте нас в Telegram, VK и Дзен
Перевод статьи Alessandro Manilii: Loading Views in SwiftUI.