Contents

sharnabh/shimmerkit

A **layout-aware, auto-generating skeleton loader** for SwiftUI.

Requirements

  • iOS 16+
  • Swift 5.9+
  • SwiftUI

Installation

Add with Swift Package Manager:

https://github.com/Sharnabh/ShimmerKit

In Xcode:

  1. File β†’ Add Packages
  2. Paste the URL
  3. Add ShimmerKit

Quick Start

import SwiftUI
import ShimmerKit

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

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Product title")
                .skeletonNode(kind: .text(lineHeight: 18))

            Text("Product subtitle")
                .skeletonNode(kind: .text(lineHeight: 14))

            RoundedRectangle(cornerRadius: 12)
                .frame(height: 120)
                .skeletonNode(kind: .image)
        }
        .smartSkeleton(isLoading)
    }
}

Full Demo App

For a single app that demonstrates every public API and feature toggle, see:

  • Examples/ShimmerKitShowcase/

What's New in 1.2.2

  • Added whole-view loading overloads with a separate non-shimmering background layer.
  • Improved whole-view loading lifecycle so original content remains mounted (hidden) during loading, avoiding unintended .task cancellation.
  • Improved loading overlay alignment to top-leading for predictable top anchoring.

Public API Reference

### `View` extension APIs

#### `smartSkeleton(_:config:includeScopes:)`

```swift
func smartSkeleton(
    _ isLoading: Bool,
    config: ShimmerConfig = ShimmerConfig(),
    includeScopes: [String]? = nil
) -> some View
```

- `isLoading`: when `true`, skeletons render.
- `config`: shimmer behavior and style.
- `includeScopes`: optional partial rendering filter.

Examples:

```swift
content.smartSkeleton(isLoading)

content.smartSkeleton(
    isLoading,
    config: ShimmerKit.config(.feedLoading),
    includeScopes: ["header", "body"]
)
```

#### `shimmerLoading(_:config:placeholder:)`

```swift
func shimmerLoading<Placeholder: View>(
    _ isLoading: Bool,
    config: ShimmerConfig = ShimmerConfig(),
    @ViewBuilder placeholder: () -> Placeholder
) -> some View
```

- Replaces the entire original content while loading.
- Shows only your custom placeholder (text, shapes, or any view) with shimmer applied.

Example:

```swift
content.shimmerLoading(isLoading, config: ShimmerKit.config(.feedLoading)) {
    VStack(alignment: .leading, spacing: 10) {
        Text("Loading feed")
        RoundedRectangle(cornerRadius: 8).frame(height: 54)
        RoundedRectangle(cornerRadius: 8).frame(height: 54)
    }
}
```

#### `shimmerLoading(_:config:placeholder:)` (controller-driven)

```swift
func shimmerLoading<Placeholder: View>(
    _ controller: ShimmerLoadingController,
    config: ShimmerConfig = ShimmerConfig(),
    @ViewBuilder placeholder: () -> Placeholder
) -> some View
```

- Use this when loading state is shared across screens.
- Useful for showing loading in a home/root container while work runs in child views.

#### `shimmerLoading(_:config:background:placeholder:)`

```swift
func shimmerLoading<Background: View, Placeholder: View>(
    _ isLoading: Bool,
    config: ShimmerConfig = ShimmerConfig(),
    @ViewBuilder background: () -> Background,
    @ViewBuilder placeholder: () -> Placeholder
) -> some View
```

- Renders a non-shimmering background while loading.
- Applies shimmer only to the placeholder layer.

Example:

```swift
content.shimmerLoading(
    isLoading,
    config: ShimmerKit.config(.feedLoading),
    background: {
        Color("LoadingBackground")
    },
    placeholder: {
        VStack(spacing: 12) {
            RoundedRectangle(cornerRadius: 10).frame(height: 40)
            RoundedRectangle(cornerRadius: 10).frame(height: 140)
        }
    }
)
```

#### `shimmerLoading(_:config:background:placeholder:)` (controller-driven)

```swift
func shimmerLoading<Background: View, Placeholder: View>(
    _ controller: ShimmerLoadingController,
    config: ShimmerConfig = ShimmerConfig(),
    @ViewBuilder background: () -> Background,
    @ViewBuilder placeholder: () -> Placeholder
) -> some View
```

- Same behavior as above, but tied to shared loading controller state.

#### `shimmerText(config:baseColor:)`

```swift
func shimmerText(
    config: ShimmerConfig = ShimmerConfig(),
    baseColor: Color = .primary
) -> some View
```

- Applies animated shimmer directly through a single text view.
- Independent of `smartSkeleton` loading flow.

Example:

```swift
Text("Hello")
    .font(.largeTitle.weight(.bold))
    .shimmerText(
        config: ShimmerConfig(
            gradient: Gradient(colors: [.clear, .pink.opacity(0.9), .orange.opacity(0.9), .clear]),
            speed: 1.0,
            angle: .degrees(20)
        ),
        baseColor: .gray.opacity(0.35)
    )
```

#### `shimmerTextSweep(config:baseColor:)`

```swift
func shimmerTextSweep(
    config: ShimmerConfig = ShimmerConfig(),
    baseColor: Color = .primary
) -> some View
```

- Applies one aligned sweep across all text inside a parent container.

Example:

```swift
VStack(alignment: .leading) {
    Text("Headline")
    Text("Subtitle")
}
.shimmerTextSweep(config: ShimmerKit.config(.subtle), baseColor: .gray.opacity(0.3))
```

#### `shimmerTextSweepExclude(_:)`

```swift
func shimmerTextSweepExclude(_ isExcluded: Bool = true) -> some View
```

- Excludes a specific text view or nested stack from the parent `shimmerTextSweep` effect.

Example:

```swift
VStack(alignment: .leading) {
    Text("Swept text")

    Text("No sweep here")
        .shimmerTextSweepExclude()
}
.shimmerTextSweep(config: ShimmerKit.config(.subtle))
```

#### `skeletonNode(cornerRadius:kind:shape:scope:)`

```swift
func skeletonNode(
    cornerRadius: CGFloat? = nil,
    kind: SkeletonKind? = nil,
    shape: SkeletonShapeStyle = .automatic,
    scope: String? = nil
) -> some View
```

- Marks a view as a skeleton target.
- `scope` works with `includeScopes` in `smartSkeleton`.

Examples:

```swift
Text("Title")
    .skeletonNode(kind: .text(lineHeight: 18), shape: .capsule, scope: "header")

Circle()
    .frame(width: 44, height: 44)
    .skeletonNode(kind: .image, shape: .circle, scope: "avatar")
```

#### `skeletonID(_:)`

```swift
func skeletonID(_ id: AnyHashable) -> some View
```

- Provides stable identity for lazy containers/lists.

Example:

```swift
ForEach(items, id: \.id) { item in
    Row(item: item)
        .skeletonID(item.id)
}
```

---

### `ShimmerKit` APIs

### `ShimmerLoadingController`

```swift
@MainActor
public final class ShimmerLoadingController: ObservableObject
```

Purpose:

- Tracks concurrent async operations.
- Keeps `isLoading` true until the last operation finishes.

Key APIs:

```swift
public func beginLoading()
public func endLoading()
public func run<T>(_ operation: @Sendable () async throws -> T) async rethrows -> T
public func runTaskGroup<ChildTaskResult: Sendable, GroupResult>(
    of childTaskResultType: ChildTaskResult.Type,
    returning returnType: GroupResult.Type,
    body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult
public func runThrowingTaskGroup<ChildTaskResult: Sendable, GroupResult>(
    of childTaskResultType: ChildTaskResult.Type,
    returning returnType: GroupResult.Type,
    body: (inout ThrowingTaskGroup<ChildTaskResult, any Error>) async throws -> GroupResult
) async throws -> GroupResult
```

Root-level loading example:

```swift
@StateObject private var loadingController = ShimmerLoadingController()

NavigationStack {
    HomeView()
}
.shimmerLoading(loadingController, config: ShimmerKit.config(.detailPage)) {
    Text("Preparing your content")
}
```

Child-view work example with multiple calls in one task:

```swift
Task {
    let payload = try await loadingController.run {
        let profile = try await api.loadProfile()
        let permissions = try await api.loadPermissions()
        let feed = try await api.loadFeed()
        return (profile, permissions, feed)
    }
    // Update UI
}
```

Child-view task-group example:

```swift
Task {
    let values = await loadingController.runTaskGroup(of: String.self, returning: [String].self) { group in
        group.addTask { await api.loadSectionA() }
        group.addTask { await api.loadSectionB() }
        group.addTask { await api.loadSectionC() }

        var output: [String] = []
        for await value in group { output.append(value) }
        return output
    }
    // Update UI
}
```

#### `ShimmerKit.defaultConfig`

```swift
public static let defaultConfig: ShimmerConfig
```

Example:

```swift
content.smartSkeleton(isLoading, config: ShimmerKit.defaultConfig)
```

#### `ShimmerKit.config(_ profile: ShimmerProfile)`

```swift
public static func config(_ profile: ShimmerProfile) -> ShimmerConfig
```

Available profiles:

- `.default`
- `.subtle`
- `.feedLoading`
- `.detailPage`

Example:

```swift
content.smartSkeleton(isLoading, config: ShimmerKit.config(.subtle))
```

#### `ShimmerKit.config(gradient:...)`

```swift
public static func config(
    gradient: Gradient = Gradient(colors: [.clear, Color.white.opacity(0.35), .clear]),
    textGradient: Gradient? = nil,
    skeletonColor: Color = Color.gray.opacity(0.25),
    speed: Double = 1.2,
    angle: Angle = .degrees(20),
    splitMultilineText: Bool = false,
    enableSemanticGrouping: Bool = false,
    useLayoutProtocolIntegration: Bool = false
) -> ShimmerConfig
```

Example:

```swift
let config = ShimmerKit.config(
    gradient: Gradient(colors: [.clear, .purple.opacity(0.4), .clear]),
    skeletonColor: .purple.opacity(0.15),
    speed: 1.0,
    angle: .degrees(35),
    splitMultilineText: true,
    enableSemanticGrouping: true,
    useLayoutProtocolIntegration: false
)
```

#### `ShimmerKit.config(shimmerColor:...)`

```swift
public static func config(
    shimmerColor: Color,
    textGradient: Gradient? = nil,
    skeletonColor: Color = Color.gray.opacity(0.25),
    shimmerOpacity: Double = 0.35,
    speed: Double = 1.2,
    angle: Angle = .degrees(20),
    splitMultilineText: Bool = false,
    enableSemanticGrouping: Bool = false,
    useLayoutProtocolIntegration: Bool = false
) -> ShimmerConfig
```

Example:

```swift
let config = ShimmerKit.config(
    shimmerColor: .mint,
    skeletonColor: .mint.opacity(0.18),
    shimmerOpacity: 0.45,
    speed: 1.0,
    angle: .degrees(30),
    splitMultilineText: true,
    enableSemanticGrouping: true,
    useLayoutProtocolIntegration: true
)
```

---

### `ShimmerConfig`

`ShimmerConfig` is the central style/behavior object.

#### Stored properties

- `gradient: Gradient`
- `textGradient: Gradient?`
- `skeletonColor: Color`
- `speed: Double`
- `angle: Angle`
- `splitMultilineText: Bool`
- `enableSemanticGrouping: Bool`
- `useLayoutProtocolIntegration: Bool`

#### Initializer: `gradient` based

```swift
public init(
    gradient: Gradient = Gradient(colors: [.clear, Color.white.opacity(0.35), .clear]),
    textGradient: Gradient? = nil,
    skeletonColor: Color = Color.gray.opacity(0.25),
    speed: Double = 1.2,
    angle: Angle = .degrees(20),
    splitMultilineText: Bool = false,
    enableSemanticGrouping: Bool = false,
    useLayoutProtocolIntegration: Bool = false
)
```

#### Initializer: `shimmerColor` based

```swift
public init(
    shimmerColor: Color,
    textGradient: Gradient? = nil,
    skeletonColor: Color = Color.gray.opacity(0.25),
    shimmerOpacity: Double = 0.35,
    speed: Double = 1.2,
    angle: Angle = .degrees(20),
    splitMultilineText: Bool = false,
    enableSemanticGrouping: Bool = false,
    useLayoutProtocolIntegration: Bool = false
)
```

---

### Enums

#### `ShimmerProfile`

```swift
public enum ShimmerProfile: Hashable, Sendable {
    case `default`
    case subtle
    case feedLoading
    case detailPage
}
```

#### `SkeletonKind`

```swift
public enum SkeletonKind: Hashable, Sendable {
    case text(lineHeight: CGFloat)
    case image
    case generic
}
```

#### `SkeletonShapeStyle`

```swift
public enum SkeletonShapeStyle: Hashable, Sendable {
    case automatic
    case roundedRectangle(cornerRadius: CGFloat)
    case capsule
    case circle
}
```

---

### `SkeletonNode` (advanced)

`SkeletonNode` is the captured/rendered node model used internally and exposed publicly.

```swift
public struct SkeletonNode: Identifiable, Hashable, Sendable {
    public var id: String { get }
    public var frame: CGRect
    public var cornerRadius: CGFloat
    public var kind: SkeletonKind
    public var shapeStyle: SkeletonShapeStyle
    public var scope: String?
}
```

---

Feature Toggles (Optional)

All advanced behavior is opt-in and defaults to off:

  • splitMultilineText
  • enableSemanticGrouping
  • useLayoutProtocolIntegration

Example:

let config = ShimmerConfig(
    shimmerColor: .cyan,
    splitMultilineText: true,
    enableSemanticGrouping: true,
    useLayoutProtocolIntegration: true
)

content.smartSkeleton(isLoading, config: config)

Partial Rendering with Scopes

VStack {
    Text("Header").skeletonNode(scope: "header")
    Text("Body").skeletonNode(scope: "body")
    Button("Retry") {}.skeletonNode(scope: "actions")
}
.smartSkeleton(
    true,
    includeScopes: ["header", "body"]
)

License

MIT

Maintenance

  • Release process: RELEASE_CHECKLIST.md

✨ ShimmerKit

A layout-aware, auto-generating skeleton loader for SwiftUI.

Not another shimmer modifier. This is a rendering system that mirrors your actual UI layout and builds skeletons automatically.


πŸš€ Features

  • ⚑ Auto Layout Skeletons

No manual placeholder views. It reads your real UI and generates skeletons.

  • 🧠 Heuristic-Based Rendering

Detects:

Text β†’ pill-shaped lines Images β†’ rounded blocks * Generic views β†’ adaptive shapes

  • 🎯 Zero Layout Duplication

Your skeleton always matches your UI. No maintenance hell.

  • πŸ”„ Timeline-based Animation

Uses TimelineView for smooth, frame-synced shimmer.

  • πŸ“¦ Swift Package Manager Ready
  • 🧡 Swift 6 Concurrency Safe
  • πŸ“± iOS 16+ Only (by design)

πŸ“¦ Installation

Swift Package Manager

Add this to your project:

https://github.com/Sharnabh/ShimmerKit

Or in Xcode:

  1. File β†’ Add Packages
  2. Paste the repo URL
  3. Select ShimmerKit

🧱 Basic Usage

1. Apply skeleton to any view

ProductCards(...)
    .smartSkeleton(true)

That’s it.

No duplicate UI. No placeholder views.


πŸ”„ Toggle Loading State

.smartSkeleton(isLoading)
  • true β†’ skeleton shown
  • false β†’ real UI shown

🧠 How It Works

  1. Your views are rendered normally (but hidden)
  2. Layout frames are captured using GeometryReader
  3. Frames are processed:

filtered merged * grouped

  1. Skeleton shapes are drawn on top
  2. Shimmer animation is applied via TimelineView

🎯 Advanced Usage

✏️ Manually define skeleton behavior

Override automatic detection when needed:

Text("Title")
    .skeletonNode(kind: .text(lineHeight: 12))

AsyncImage(...)
    .skeletonNode(kind: .image, cornerRadius: 12)

πŸ†” Fix LazyVStack / Scroll issues

ForEach(items, id: \.id) { item in
    ProductCards(...)
        .skeletonID(item.id)
}

Prevents flickering and incorrect frame reuse.


βš™οΈ Customize shimmer

.smartSkeleton(
    true,
    config: ShimmerConfig(
        gradient: Gradient(colors: [
            .clear,
            .white.opacity(0.4),
            .clear
        ]),
        skeletonColor: Color.gray.opacity(0.2),
        speed: 0.8,
        angle: .degrees(45)
    )
)

🌈 Change shimmer and skeleton colors

Use a single shimmer tint color and custom base skeleton color:

.smartSkeleton(
    true,
    config: ShimmerKit.config(
        shimmerColor: .mint,
        skeletonColor: .mint.opacity(0.18),
        shimmerOpacity: 0.45,
        speed: 1.0,
        angle: .degrees(30)
    )
)

πŸ“š Complete Usage Snippets (All Public APIs)

### 1) `smartSkeleton(_:config:)` with defaults

```swift
struct ProductView: View {
    @State private var isLoading = true

    var body: some View {
        VStack {
            Text("Product Title").skeletonNode()
            Text("β‚Ή99").skeletonNode()
        }
        .smartSkeleton(isLoading)
    }
}
```

### 2) `smartSkeleton(_:config:)` with custom config

```swift
VStack(alignment: .leading, spacing: 8) {
    Text("Headline").skeletonNode(kind: .text(lineHeight: 20))
    Text("Subtitle").skeletonNode(kind: .text(lineHeight: 14))
}
.smartSkeleton(
    true,
    config: ShimmerConfig(
        gradient: Gradient(colors: [.clear, .blue.opacity(0.4), .clear]),
        skeletonColor: .blue.opacity(0.15),
        speed: 0.9,
        angle: .degrees(35)
    )
)
```

### 3) `skeletonNode()` (auto-detect kind + corner radius)

```swift
Text("Auto detected skeleton")
    .font(.body)
    .skeletonNode()
```

### 4) `skeletonNode(cornerRadius:)`

```swift
RoundedRectangle(cornerRadius: 16)
    .frame(height: 60)
    .skeletonNode(cornerRadius: 20)
```

### 5) `skeletonNode(kind:)`

```swift
Text("Name")
    .skeletonNode(kind: .text(lineHeight: 18))

Image(systemName: "person.crop.circle.fill")
    .resizable()
    .frame(width: 40, height: 40)
    .skeletonNode(kind: .image)

Rectangle()
    .frame(height: 80)
    .skeletonNode(kind: .generic)
```

### 6) `skeletonNode(cornerRadius:kind:)`

```swift
AsyncImage(url: URL(string: "https://example.com/cover.jpg")) { image in
    image.resizable().scaledToFill()
} placeholder: {
    Color.gray
}
.frame(width: 120, height: 120)
.clipShape(RoundedRectangle(cornerRadius: 18))
.skeletonNode(cornerRadius: 18, kind: .image)
```

### 7) `skeletonID(_:)` for stable identity in lists/lazy stacks

```swift
ForEach(items, id: \.id) { item in
    HStack {
        Circle().frame(width: 44, height: 44).skeletonNode(kind: .image)
        Text(item.title).skeletonNode(kind: .text(lineHeight: 16))
    }
    .skeletonID(item.id)
}
```

### 8) Multiple shape support (`shape:`)

```swift
VStack(spacing: 12) {
    Circle()
        .frame(width: 48, height: 48)
        .skeletonNode(shape: .circle)

    Text("Name")
        .skeletonNode(kind: .text(lineHeight: 16), shape: .capsule)

    Rectangle()
        .frame(height: 56)
        .skeletonNode(shape: .roundedRectangle(cornerRadius: 14))
}
.smartSkeleton(true)
```

### 9) Partial skeleton rendering (`includeScopes:`)

Render shimmer only on specific skeleton scopes.

```swift
VStack(alignment: .leading, spacing: 10) {
    Text("Header")
        .skeletonNode(scope: "header")

    Text("Body line 1")
        .skeletonNode(scope: "body")

    Text("Body line 2")
        .skeletonNode(scope: "body")

    Button("Retry") {}
        .skeletonNode(scope: "actions")
}
.smartSkeleton(
    true,
    includeScopes: ["header", "body"]
)
```

### 10) Turn partial rendering OFF (default)

```swift
content.smartSkeleton(isLoading)
```

or

```swift
content.smartSkeleton(isLoading, includeScopes: nil)
```

### 11) Multi-line text splitting (optional)

When enabled, text skeleton blocks can be split into multiple pill lines.

```swift
let config = ShimmerKit.config(
    shimmerColor: .mint,
    skeletonColor: .mint.opacity(0.2),
    angle: .degrees(30),
    splitMultilineText: true
)

content.smartSkeleton(isLoading, config: config)
```

### 12) Keep multi-line text splitting OFF (default)

```swift
let config = ShimmerConfig(
    gradient: Gradient(colors: [.clear, .white.opacity(0.4), .clear]),
    skeletonColor: .gray.opacity(0.2),
    speed: 1.0,
    angle: .degrees(20)
)

content.smartSkeleton(isLoading, config: config)
```

### 13) Semantic grouping (title vs subtitle) (optional)

When enabled, text skeletons are heuristically grouped into title/subtitle styles (subtitle lines become slightly shorter).

```swift
let config = ShimmerKit.config(
    shimmerColor: .indigo,
    skeletonColor: .indigo.opacity(0.18),
    splitMultilineText: true,
    enableSemanticGrouping: true
)

content.smartSkeleton(isLoading, config: config)
```

### 14) Keep semantic grouping OFF (default)

```swift
let config = ShimmerConfig(
    gradient: Gradient(colors: [.clear, .white.opacity(0.35), .clear]),
    skeletonColor: .gray.opacity(0.2),
    speed: 1.0,
    angle: .degrees(20),
    splitMultilineText: true,
    enableSemanticGrouping: false
)

content.smartSkeleton(isLoading, config: config)
```

### 15) SwiftUI Layout protocol integration (optional)

Enable this to route hidden content through a `Layout`-based placement path.

```swift
let config = ShimmerKit.config(
    shimmerColor: .blue,
    skeletonColor: .blue.opacity(0.18),
    splitMultilineText: true,
    enableSemanticGrouping: true,
    useLayoutProtocolIntegration: true
)

content.smartSkeleton(isLoading, config: config)
```

### 16) Keep Layout integration OFF (default)

```swift
let config = ShimmerConfig(
    gradient: Gradient(colors: [.clear, .white.opacity(0.35), .clear]),
    skeletonColor: .gray.opacity(0.2),
    speed: 1.0,
    angle: .degrees(20),
    splitMultilineText: false,
    enableSemanticGrouping: false,
    useLayoutProtocolIntegration: false
)

content.smartSkeleton(isLoading, config: config)
```

### 17) `ShimmerKit.defaultConfig`

```swift
VStack {
    Text("Default config")
        .skeletonNode()
}
.smartSkeleton(true, config: ShimmerKit.defaultConfig)
```

### 18) `ShimmerKit.config(gradient:skeletonColor:speed:angle:splitMultilineText:enableSemanticGrouping:useLayoutProtocolIntegration:)`

```swift
let config = ShimmerKit.config(
    gradient: Gradient(colors: [.clear, .purple.opacity(0.45), .clear]),
    skeletonColor: .purple.opacity(0.18),
    speed: 1.1,
    angle: .degrees(60),
    splitMultilineText: true,
    enableSemanticGrouping: true,
    useLayoutProtocolIntegration: true
)

content.smartSkeleton(isLoading, config: config)
```

### 19) `ShimmerKit.config(shimmerColor:skeletonColor:shimmerOpacity:speed:angle:splitMultilineText:enableSemanticGrouping:useLayoutProtocolIntegration:)`

```swift
let config = ShimmerKit.config(
    shimmerColor: .mint,
    skeletonColor: .mint.opacity(0.2),
    shimmerOpacity: 0.5,
    speed: 1.0,
    angle: .degrees(25),
    splitMultilineText: true,
    enableSemanticGrouping: true,
    useLayoutProtocolIntegration: true
)

content.smartSkeleton(isLoading, config: config)
```

### 20) `ShimmerConfig(...)` full initializer

```swift
let custom = ShimmerConfig(
    gradient: Gradient(colors: [.clear, .orange.opacity(0.4), .clear]),
    skeletonColor: .orange.opacity(0.16),
    speed: 0.85,
    angle: .degrees(45),
    splitMultilineText: true,
    enableSemanticGrouping: true,
    useLayoutProtocolIntegration: true
)
```

### 21) `ShimmerConfig(shimmerColor:skeletonColor:shimmerOpacity:speed:angle:splitMultilineText:enableSemanticGrouping:useLayoutProtocolIntegration:)`

```swift
let quick = ShimmerConfig(
    shimmerColor: .teal,
    skeletonColor: .teal.opacity(0.15),
    shimmerOpacity: 0.4,
    speed: 1.25,
    angle: .degrees(30),
    splitMultilineText: true,
    enableSemanticGrouping: true,
    useLayoutProtocolIntegration: true
)
```

### 22) End-to-end screen example

```swift
import SwiftUI
import ShimmerKit

struct UserRow: View {
    let title: String

    var body: some View {
        HStack(spacing: 12) {
            Circle()
                .frame(width: 44, height: 44)
                .skeletonNode(kind: .image)

            VStack(alignment: .leading, spacing: 6) {
                Text(title)
                    .font(.headline)
                    .skeletonNode(kind: .text(lineHeight: 18))

                Text("Subtitle")
                    .font(.subheadline)
                    .skeletonNode(kind: .text(lineHeight: 14))
            }
        }
        .padding(.vertical, 6)
    }
}

struct UsersScreen: View {
    @State private var isLoading = true
    private let placeholders = Array(0..<6)
    private let users = ["Jane", "Alex", "Mia"]

    var body: some View {
        List {
            ForEach(isLoading ? placeholders.map(String.init) : users, id: \.self) { value in
                UserRow(title: value)
                    .skeletonID(value)
            }
        }
        .smartSkeleton(
            isLoading,
            config: ShimmerKit.config(
                shimmerColor: .cyan,
                skeletonColor: .cyan.opacity(0.15),
                shimmerOpacity: 0.45,
                speed: 1.0,
                angle: .degrees(40),
                splitMultilineText: true,
                enableSemanticGrouping: true,
                useLayoutProtocolIntegration: true
            )
        )
        .task {
            try? await Task.sleep(nanoseconds: 2_000_000_000)
            isLoading = false
        }
    }
}
```

### 23) Preset profiles (`ShimmerProfile`)

Use ready-made configs for common loading styles:

```swift
content.smartSkeleton(isLoading, config: ShimmerKit.config(.default))
content.smartSkeleton(isLoading, config: ShimmerKit.config(.subtle))
content.smartSkeleton(isLoading, config: ShimmerKit.config(.feedLoading))
content.smartSkeleton(isLoading, config: ShimmerKit.config(.detailPage))
```

Profile intent:

* `.default` β†’ balanced baseline config
* `.subtle` β†’ softer highlight + slower animation
* `.feedLoading` β†’ stronger shimmer for list/feed placeholders
* `.detailPage` β†’ richer shimmer with advanced toggles enabled

### 24) `shimmerText(config:baseColor:)` for single text

```swift
Text("Hello")
    .font(.system(size: 56, weight: .heavy, design: .rounded))
    .shimmerText(
        config: ShimmerConfig(
            gradient: Gradient(colors: [.clear, .pink.opacity(0.9), .orange.opacity(0.9), .clear]),
            speed: 1.0,
            angle: .degrees(20)
        ),
        baseColor: .gray.opacity(0.35)
    )
```

### 25) `shimmerTextSweep(config:baseColor:)` for one aligned sweep over mixed text layouts

```swift
VStack(alignment: .leading, spacing: 10) {
    Text("Title").font(.title3.weight(.bold))

    HStack {
        VStack(alignment: .leading) {
            Text("Left")
            Text("Stack")
        }
        Spacer()
        Text("Right")
    }
}
.shimmerTextSweep(
    config: ShimmerConfig(
        gradient: Gradient(colors: [.clear, .cyan.opacity(0.9), .mint.opacity(0.9), .clear]),
        speed: 1.2,
        angle: .degrees(28)
    ),
    baseColor: .gray.opacity(0.32)
)
```

### 26) `shimmerTextSweepExclude(_:)` to opt out specific text/stack

```swift
VStack(alignment: .leading) {
    VStack(alignment: .leading) {
        Text("Excluded block")
        Text("No shimmer here")
    }
    .shimmerTextSweepExclude()

    Text("Still shimmering")
}
.shimmerTextSweep(config: ShimmerKit.config(.subtle))
```

---

🎨 Skeleton Types

| Type | Behavior | | ---------- | ------------------------------------- | | .text | Rounded pill (auto line height) | | .image | Rounded rectangle (12 default radius) | | .generic | Default rounded rectangle |


🧠 Heuristics (Auto Detection)

ShimmerKit automatically infers:

  • Text β†’ height < 20
  • Image β†’ width β‰ˆ height
  • Generic β†’ everything else

You can override anytime.


⚠️ Requirements

  • iOS 16+
  • Swift 5.9+
  • SwiftUI

🧡 Concurrency

Fully compatible with Swift 6 strict concurrency:

  • Sendable models
  • Safe PreferenceKey usage
  • No unsafe global state

🧱 Architecture

Input Layer
   ↓
Skeleton Nodes (layout capture)
   ↓
Processing Engine (merge + group)
   ↓
Renderer (shapes + shimmer)

πŸ“ Project Structure

Core/
Skeleton/
Modifiers/
Containers/
Engine/
Shapes/
Extensions/
Utilities/

🚫 What This Is NOT

  • ❌ Not a simple .shimmer() modifier
  • ❌ Not manual skeleton UI
  • ❌ Not tied to specific layouts

πŸ’£ Why ShimmerKit

Most libraries:

β€œDraw grey rectangles”

ShimmerKit:

Reconstructs your UI structure automatically


πŸ§ͺ Example

VStack(alignment: .leading, spacing: 12) {
    Text("Product Title")
    Text("Subtitle")
    HStack {
        Text("β‚Ή99")
        Text("β‚Ή199")
    }
}
.smartSkeleton(true)

πŸš€ Roadmap

  • πŸ”₯ Multi-line text splitting
  • ⚑ SwiftUI Layout protocol integration
  • 🎯 Partial skeleton rendering
  • 🧠 Semantic grouping (title vs subtitle detection)
  • 🎨 Multiple shape support (circle, capsule, etc.)

πŸ› οΈ Contributing

PRs are welcomeβ€”but keep it:

  • clean
  • modular
  • concurrency-safe

πŸ“„ License

MIT License


πŸ‘€ Author

Built with intent, not shortcuts.


⭐ Final Note

If your skeleton UI breaks when your layout changes, you built it wrong.

ShimmerKit fixes that.

Package Metadata

Repository: sharnabh/shimmerkit

Default branch: master

README: README.md