История одного модального окна или переходим с UIKit на SwiftUI. Часть 3. ProgressView vs SkeletonView

от автора

Продолжаю эпопею с модальными экранами на 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 чисто для тренировки. Могу так же дублировать сюда описание и код.

Например вот пост в ТГ где я делаю снежинки или вот создаю простую игру «кошки-мышки».


ссылка на оригинал статьи https://habr.com/ru/articles/853600/