Продолжаю эпопею с модальными экранами на SwiftUI. В первой части в комментариях уже раскрыли главную интригу, но ничего, сегодня будет больше кода. Была задача, сделать ProgressView и SkeletonView. Вдруг кому-то пригодится, показываю.
ProgressView по дизайну должен был быть с градиентной полоской загрузки, по дефолту так нельзя сделать, поэтому я решила заменить полосочку - имитацией полоски загрузки. То есть у нас есть нормальный ProgressView, у него делаем невидимой полоску загрузки, а сверху имитация полоски загрузки - градиентная View.
Хотя, сказать по правде, я даже и нормальный ProgressView в итоге удалила, т к фейковый полностью дублирует его. В общем, меньше слов, больше кода!
struct GenerateReportView: View {
@Environment(\.presentationMode) var presentationMode
@State private var progress: Float = 0.0
@State private var progressIncrement: Float = 0.05
@State private var displayLink: Timer? = nil
@State private var text = "Получаем данные с сервера..."
var body: some View {
VStack {
Spacer()
VStack {
Image("reviewIcon")
ZStack(alignment: .leading) {
// Фейковый фон ProgressView
RoundedRectangle(cornerRadius: 16)
// Фейковая полоска загрузки для ProgressView с градиентом
RoundedRectangle(cornerRadius: 16)
.fill(LinearGradient(gradient:
Gradient(colors: [Color(UIColor(hex: "#5C4EF2")),
Color(UIColor(hex: "#1A96FF"))]),
startPoint: .leading,
endPoint: .trailing))
}
Text(text)
...
}
}
}
}
Что здесь происходит: создаём два RoundedRectangle()
высотой 8 и накладываем их друг на друга в ZStack
. Далее прописываем второму LinearGradient
и в общем-то всё. Сделала для демонстрации фейковый таймер прогресса, по желанию можно заменить на данные прогресса из API. По мере загрузки данных меняется надпись под прогрессом загрузки.
// Function to start fake progress
private func startFakeProgress() {
displayLink = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.progress < 0.99 {
self.progress = min(self.progress + self.progressIncrement, 0.99)
self.updateText()
self.adjustProgressIncrement()
} else {
self.completeProgress()
}
}
}
// Function to update text based on progress
private func updateText() {
switch progress {
case 0.0...0.19:
text = "Получаем данные с сервера..."
case 0.2...0.498:
text = "Обновляем данные с сервера..."
case 0.5...0.598:
text = "Нужно ещё немного времени..."
case 0.599...0.698:
text = "Скоро загрузится..."
case 0.699...0.89:
text = "Ещё чуть-чуть..."
case 0.899...0.999:
text = "Уже почти..."
default:
text = "Получаем данные с сервера..."
}
}
// Function to adjust progress increment as it gets closer to completion
private func adjustProgressIncrement() {
switch progress {
case 0.0...0.19:
progressIncrement /= 1.0
case 0.2...0.89:
progressIncrement /= 1.1
case 0.9...0.99:
progressIncrement /= 1.12
default:
break
}
}
// Function to complete progress quickly once the server responds
private func completeProgress() {
displayLink?.invalidate()
displayLink = nil
// Complete the progress in 1 second
withAnimation(.linear(duration: 1.0)) {
progress = 1.0
}
// Call the delegate function if needed
// delegate?.progressDone()
}
// Call this function when you receive the server response
func serverResponseReceived() {
completeProgress()
}
Теперь перейдём к SkeletonView.
Его делать гораздо геморройнее. Для начала я создала общую структуру SkeletonLoadingView
, которая может на входе принимать любую форму, размер и цвет. После этого в любом месте кода можем просто добавить необходимое количество этих View.
struct SkeletonLoadingView<ShapeType: Shape>: View {
@State private var animationPosition: CGFloat = -1
var width: CGFloat = 100
var height: CGFloat = 10
let shape: ShapeType
let animation: Animation
let gradient: Gradient
var body: some View {
shape
.fill(self.gradientFill())
.frame(width: width, height: height)
.onAppear {
withAnimation(animation) {
animationPosition = 2
}
}
}
private func gradientFill() -> LinearGradient {
return LinearGradient(gradient: gradient,
startPoint: .init(x: animationPosition - 1, y: animationPosition - 1),
endPoint: .init(x: animationPosition + 1, y: animationPosition + 1))
}
}
Ну и чтобы добавить 4 полоски на мой экран, я сделала вот так:
VStack(alignment: .leading, spacing: 8) {
SkeletonLoadingView(width: 350,
shape: RoundedRectangle(cornerRadius: 8),
animation: .easeIn(duration: 1).repeatForever(autoreverses: true),
gradient: Gradient(colors: [Color.blue, Color.white]))
SkeletonLoadingView(width: 380,
shape: RoundedRectangle(cornerRadius: 8),
animation: .easeIn(duration: 1).repeatForever(autoreverses: true),
gradient: Gradient(colors: [Color.blue, Color.white]))
SkeletonLoadingView(width: 350,
shape: RoundedRectangle(cornerRadius: 8),
animation: .easeIn(duration: 1).repeatForever(autoreverses: true),
gradient: Gradient(colors: [Color.blue, Color.white]))
SkeletonLoadingView(width: 180,
shape: RoundedRectangle(cornerRadius: 8),
animation: .easeIn(duration: 1).repeatForever(autoreverses: true),
gradient: Gradient(colors: [Color.blue, Color.white]))
}
Естественно, этот код тоже лучше вынести в отдельный модуль реализации. Но вот на этом этапе я уже начала соединять View и логику и тут-то у меня закрались некоторые подозрения... Вот мы и подошли к главной интриге - а, собственно, зачем нам SkeletonView
, если я уже сделала ProgressView
?
Получается, что на этапе показа экрана с описаниями - они уже все у нас подгружены и Skeleton точно не вызовется. На этапе генерации описания - показываем ProgressView
. То есть SkeletonView
оказался не нужен. Ну, бывает...
Полный код, как обычно, на моём GitHub.
В качестве бонуса добавила там ещё один вариант реализации Skeleton - BreathingSkeletonText
. Там используется уже по дефолту RoundedRectangle (или можно изменить на свой), добавлены цвета и остальные параметры. Подойдёт, если вы уже заранее точно знаете какие фигуры и цвета будете использовать для Skeleton.
Напишите пожалуйста в комментариях, какие вам ещё темы интересны? У меня есть всякие мини видео по 10 секунд где я делаю какие-нибудь забавные мелкие штуки на SwiftUI чисто для тренировки. Могу так же дублировать сюда описание и код.
Например вот пост в ТГ где я делаю снежинки или вот создаю простую игру "кошки-мышки".