SwiftUI Skeleton Component

Placeholder shapes for loading states.

Variants: Row, Text

Install the free SwiftUI skeleton

  1. Add SwiftKitUI.swift to your app once. It holds the tokens and motion curves.
  2. Copy SKSkeleton.swift below into your project. It needs nothing else.

Usage

Usage.swift
SKSkeleton(.text(lines: 3))
SKSkeleton(.circle, size: 48)
SKSkeletonRow()
Text("Loaded").skRedacted(isLoading)

Full source · 110 lines · MIT

SKSkeleton.swift
//
//  SKSkeleton.swift
//  Swift Kit UI
//
//  Requires: SwiftKitUI.swift
//
//  Placeholder shapes for loading states.
//
//  Skeletons beat spinners when you know the shape of what is coming: the
//  layout does not jump when real content lands, because the placeholder was
//  already the right size.
//
//  Usage:
//      SKSkeleton(.text(lines: 3))
//      SKSkeleton(.circle, size: 48)
//      SKSkeletonRow()
//      Text("Loaded").skRedacted(isLoading)
//

import SwiftUI

public struct SKSkeleton: View {
    public enum Shape_: Sendable {
        case rect
        case circle
        case text(lines: Int)
    }

    private let shape: Shape_
    private let size: CGFloat?
    private let cornerRadius: CGFloat

    public init(_ shape: Shape_ = .rect, size: CGFloat? = nil, cornerRadius: CGFloat = SK.Radius.sm) {
        self.shape = shape
        self.size = size
        self.cornerRadius = cornerRadius
    }

    public var body: some View {
        Group {
            switch shape {
            case .rect:
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
                    .fill(SK.Palette.placeholder)
                    .frame(height: size ?? 16)
            case .circle:
                Circle()
                    .fill(SK.Palette.placeholder)
                    .frame(width: size ?? 44, height: size ?? 44)
            case .text(let lines):
                VStack(alignment: .leading, spacing: SK.Space.sm) {
                    ForEach(0..<max(lines, 1), id: \.self) { index in
                        RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
                            .fill(SK.Palette.placeholder)
                            .frame(height: size ?? 12)
                            // Last line short, like real text.
                            .frame(maxWidth: index == lines - 1 && lines > 1 ? 160 : .infinity,
                                   alignment: .leading)
                    }
                }
            }
        }
        .skShimmer()
        .accessibilityHidden(true)
    }
}

/// Avatar + two lines. The single most-repeated skeleton in any list app.
public struct SKSkeletonRow: View {
    private let showsAvatar: Bool

    public init(showsAvatar: Bool = true) { self.showsAvatar = showsAvatar }

    public var body: some View {
        HStack(spacing: SK.Space.md) {
            if showsAvatar { SKSkeleton(.circle, size: 44) }
            VStack(alignment: .leading, spacing: SK.Space.sm) {
                SKSkeleton(.rect, size: 12)
                SKSkeleton(.rect, size: 10).frame(maxWidth: 140, alignment: .leading)
            }
        }
        .accessibilityHidden(true)
    }
}

extension View {
    /// Greys out and shimmers this view while loading, keeping its exact layout.
    public func skRedacted(_ isLoading: Bool) -> some View {
        redacted(reason: isLoading ? .placeholder : [])
            .skShimmer(isLoading)
    }
}

#Preview("SKSkeleton") {
    VStack(alignment: .leading, spacing: SK.Space.xl) {
        SKSkeletonRow()
        SKSkeletonRow()
        SKSkeleton(.text(lines: 3))
        SKSkeleton(.rect, size: 120, cornerRadius: SK.Radius.lg)
        VStack(alignment: .leading, spacing: SK.Space.xs) {
            Text("Real text, redacted").font(SK.Type_.headline)
            Text("Same layout, no jump when it loads.").font(SK.Type_.callout)
        }
        .skRedacted(true)
    }
    .padding(SK.Space.xl)
    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
    .background(SK.Palette.surfaceSunken)
}

Questions

Is this SwiftUI skeleton free for commercial apps?
Yes. Swift Kit UI is MIT licensed, so you can use it in personal and commercial apps with no payment or sign-up. Keep the licence notice with the code.
What does it depend on?
SKSkeleton.swift depends only on SwiftKitUI.swift, a single foundation file with the design tokens and motion curves. There are no third-party packages.
Which iOS versions does it support?
iOS 17 and later, plus macOS 14.

Related components

Building a whole app? The Swift Kit adds auth, payments and onboarding, ready to ship.