История одного модального окна или переходим с 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/


Интересные статьи

Интересные статьи

Усталость, осень, плохое настроение — очень важно учитывать в работе человеческий фактор . Если мы не проанализировали, на что он влияет, как мы можем понять критичность того, что мы им не управл...
В данной статье - покажу новый крутой подход к разработке сайтов на bitrix, который обеспечит легкость в поддержке и масштабируемости.Покажу как работать с bitrix cli внутри компонентов, покажу как пи...
Привет, Хаброжители! Большинство разработчиков ПО тратят тысячи часов на создание излишне сложного кода. Девять основных принципов книги «Искусство чистого кода» научат вас писать понятный и удобн...
Для каких целей? На момент возникновения идеи было еще не совсем понятно. Но предпосылки развития идеи были
Привет! Меня зовут Саша Шутай, я тимлид в AGIMA. В прошлой статье я рассказывал, что делать, если на проекте Bitrix сожительствует с Vue.js и поисковые боты не видят контента сайта. А в этой помогу ра...