What You Will Build

A skeleton loading splash screen that displays placeholder shapes with a sliding shimmer highlight while content loads. When data arrives, the placeholders transition smoothly into real content. This is the loading pattern used by Facebook, LinkedIn, and YouTube to keep users engaged during network fetches.

Why Skeleton Screens Beat Spinners

Research shows skeleton screens feel faster than spinning indicators because they preview the layout structure. Users perceive the app as partially loaded rather than completely blocked. SwiftUI makes this easy with animated gradient overlays and a simple boolean toggle.

Key SwiftUI Concepts

  • LinearGradient overlay creates the shimmer highlight sliding across each placeholder.
  • @State offset animated with .repeatForever drives the shimmer from left to right.
  • .clipShape constrains the shimmer within rounded rectangles.

Step 1: Build the SkeletonRow Component

Each placeholder is a gray rounded rectangle with a moving gradient overlay:

private struct SkeletonRow: View {
    var width: CGFloat
    var height: CGFloat
    var isLoaded: Bool
    var shimmerOffset: CGFloat

    var body: some View {
        RoundedRectangle(cornerRadius: 8)
            .fill(.gray.opacity(0.2))
            .frame(maxWidth: width == .infinity ? .infinity : width,
                   maxHeight: height)
            .frame(height: height)
            .overlay {
                if !isLoaded {
                    RoundedRectangle(cornerRadius: 8)
                        .fill(
                            LinearGradient(
                                colors: [.clear, .white.opacity(0.3), .clear],
                                startPoint: .leading,
                                endPoint: .trailing
                            )
                        )
                        .offset(x: shimmerOffset)
                }
            }
            .clipShape(RoundedRectangle(cornerRadius: 8))
    }
}

Step 2: Compose the Splash Layout

Arrange skeleton rows to mimic your real content structure. Here we simulate a header, three list items with avatars, and a large image placeholder:

struct SplashScreenDemo: View {
    @State private var isLoaded = false
    @State private var shimmerOffset: CGFloat = -200

    var body: some View {
        VStack(spacing: 20) {
            SkeletonRow(width: 200, height: 30,
                        isLoaded: isLoaded, shimmerOffset: shimmerOffset)

            ForEach(0..<3, id: \.self) { _ in
                HStack(spacing: 12) {
                    SkeletonRow(width: 60, height: 60,
                                isLoaded: isLoaded, shimmerOffset: shimmerOffset)
                    VStack(alignment: .leading, spacing: 8) {
                        SkeletonRow(width: 180, height: 16,
                                    isLoaded: isLoaded, shimmerOffset: shimmerOffset)
                        SkeletonRow(width: 120, height: 12,
                                    isLoaded: isLoaded, shimmerOffset: shimmerOffset)
                    }
                    Spacer()
                }
            }

            SkeletonRow(width: .infinity, height: 200,
                        isLoaded: isLoaded, shimmerOffset: shimmerOffset)

            Spacer()

            Button(isLoaded ? "Reset" : "Load Content") {
                if isLoaded {
                    isLoaded = false
                } else {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        withAnimation { isLoaded = true }
                    }
                }
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .onAppear {
            withAnimation(.linear(duration: 1.5)
                          .repeatForever(autoreverses: false)) {
                shimmerOffset = 400
            }
        }
    }
}

How the Shimmer Works

The shimmer effect is a three-color LinearGradient (clear, white at 30% opacity, clear) that starts offset at -200 points to the left. On appear, a repeating linear animation moves it to +400 points, creating the left-to-right sweep. The .clipShape on each row ensures the gradient only appears within the placeholder bounds.

Tips and Pitfalls

  • Match your real layout: The skeleton should mirror the dimensions of your actual content so there is no layout jump when data loads.
  • Use .infinity for full-width: Pass .infinity as the width for image placeholders that span the full screen.
  • Avoid autoreverses: The shimmer should only move in one direction. Using autoreverses: false prevents an unnatural back-and-forth sweep.
  • Transition to real content: Wrap your loaded content swap in withAnimation to get a smooth crossfade rather than a jarring pop.
  • Accessibility: Consider reducing motion for users who enable Reduce Motion in system settings.

iOS Version: iOS 15+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.