Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров663

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

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

Теги:
Хабы:
0
Комментарии2

Публикации

Истории

Работа

iOS разработчик
15 вакансий
Swift разработчик
15 вакансий

Ближайшие события

22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань