Как стать автором
Поиск
Написать публикацию
Обновить

Как я сделал универсальный Skeleton‑View с shimmer‑эффектом в SwiftUI

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

Привет! Я — iOS‑разработчик, и недавно в своём приложении столкнулся с задачей: нужно было красиво показывать placeholder‑загрузку интерфейса. Думал использовать стандартный .redacted — но он неудобен: нет анимации, мало кастомизации. Либо подгружать тяжелую библиотеку вроде SwiftUI‑Shimmer. Решил: сделаю свой легковесный и гибкий подход — и расскажу вам, как это получилось.


Почему не .redacted и не библиотека

  • .redacted(reason: .placeholder) прост, но выглядит скучно, невозможно настроить форму или shimmer.

  • Библиотеки дают красивый shimmer, но добавляют лишний вес и зависимости. Для проекта это был лишний overhead.

Мне хотелось:

  1. Использовать кастомные формы (например, аватар, текст, кнопка), а не лишь прямоугольник.

  2. Управлять цветом, углами, скоростью.

  3. Минимальный код без внешних зависимостей.


Как работает  .skeleton(isLoading:)

extension View {
    func skeleton<S>(_ shape: S? = nil as Rectangle?, isLoading: Bool) -> some View where S: Shape {
        guard isLoading else { return AnyView(self) }

        let shapeView: AnyShape = shape.map(AnyShape.init)
            ?? AnyShape(RoundedRectangle(cornerRadius: 20))

        return AnyView(
            self
                .opacity(0)
                .overlay(
                    shapeView
                        .fill(Color.gray.opacity(0.3))
                        .shimmering()
                )
        )
    }

    func shimmering() -> some View {
        modifier(ShimmeringModifier())
    }
}
  • Если isLoading == false — возвращаем оригинальный View.

  • Иначе — делаем прозрачным контент, накладываем placeholder‑форму с shimmer‑эффектом.

  • Кастомная форма (Circle(), RoundedRectangle, свой Shape) — легко менять.

Реализация shimmer‑анимации

struct ShimmeringModifier: ViewModifier {
    func body(content: Content) -> some View {
        TimelineView(.animation) { timeline in
            let phase = CGFloat(timeline.date.timeIntervalSinceReferenceDate
                                .truncatingRemainder(dividingBy: 1))
            content.modifier(AnimatedMask(phase: phase))
        }
    }
}

struct AnimatedMask: AnimatableModifier {
    var phase: CGFloat
    var animatableData: CGFloat { get { phase } set { phase = newValue } }

    func body(content: Content) -> some View {
        content.mask(GradientMask(phase: phase).scaleEffect(3))
    }
}

struct GradientMask: View {
    let phase: CGFloat

    var body: some View {
        GeometryReader { geo in
            LinearGradient(gradient: Gradient(stops: [
                .init(color: .white.opacity(0.1), location: phase),
                .init(color: .white.opacity(0.6), location: phase + 0.1),
                .init(color: .white.opacity(0.1), location: phase + 0.2),
            ]), startPoint: .leading, endPoint: .trailing)
            .rotationEffect(.degrees(-45))
            .offset(x: -geo.size.width, y: -geo.size.height)
            .frame(width: geo.size.width * 3,
                   height: geo.size.height * 3)
        }
    }
}
  • TimelineView обеспечивает плавную циклическую анимацию.

  • AnimatedMask управляет фазой анимации с помощью AnimatableModifier.

  • GradientMask рисует диагональный градиент, создающий эффект светящегося слоя.

Пример в действии

struct SkeletonPreview: View {
    @State private var isLoading = true

    var body: some View {
        VStack(spacing: 16) {
            RoundedRectangle(cornerRadius: 8)
                .frame(height: 20)
                .skeleton(isLoading: isLoading)

            Circle()
                .frame(width: 50, height: 50)
                .skeleton(Circle(), isLoading: isLoading)

            Button("Toggle") {
                isLoading.toggle()
            }
        }
        .padding()
    }
}

Нажимайте на кнопку — и увидите: скелетоны исчезают, когда данные загружены.

Итоги

Я столкнулся с проблемой загрузочного UI — и решил её сам: написал свой универсальный .skeleton() + .shimmering().

✔️ Минималистичный

✔️ Гибкий (любой Shape, настройки)

✔️ Без сторонних зависимостей


Этот подход уже используется в моём приложении, работает стабильно и приятно. Думаю, он будет полезен знакомым iOS‑разработчикам — да и вам пригодится. Код можно взять и сразу внедрить.

Теги:
Хабы:
+3
Комментарии6

Публикации

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