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

Почему не .redacted и не библиотека
- .redacted(reason: .placeholder) прост, но выглядит скучно, невозможно настроить форму или shimmer. 
- Библиотеки дают красивый shimmer, но добавляют лишний вес и зависимости. Для проекта это был лишний overhead. 
Мне хотелось:
- Использовать кастомные формы (например, аватар, текст, кнопка), а не лишь прямоугольник. 
- Управлять цветом, углами, скоростью. 
- Минимальный код без внешних зависимостей. 
Как работает .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‑разработчикам — да и вам пригодится. Код можно взять и сразу внедрить.
Комментарии (6)
 - house200806.08.2025 17:10- У нас тоже скелетон в пару строк просто анимация цветов с слабо серого до сильно серого. Можно использовать view builder чтобы убрать AnyView, он сам каждую бранчу завернет в нее автоматом. 
 
           
 


infund
А можно пояснительную бригаду: что такое skeleton view и shimmer-эффект? Ну или какой-нибудь, прости господи, анимированный гиф с результатом?
Gjolly Автор
Согласен, мой недочет) исправился) гиф записалась не очень красиво, но думаю суть понятна