Привет! Я — 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‑разработчикам — да и вам пригодится. Код можно взять и сразу внедрить.
ссылка на оригинал статьи https://habr.com/ru/articles/934756/
Добавить комментарий