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/ShimmerKitIn Xcode:
- File β Add Packages
- Paste the URL
- 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
.taskcancellation. - 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:
splitMultilineTextenableSemanticGroupinguseLayoutProtocolIntegration
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/ShimmerKitOr in Xcode:
- File β Add Packages
- Paste the repo URL
- 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 shownfalseβ real UI shown
π§ How It Works
- Your views are rendered normally (but hidden)
- Layout frames are captured using
GeometryReader - Frames are processed:
filtered merged * grouped
- Skeleton shapes are drawn on top
- 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:
Sendablemodels- Safe
PreferenceKeyusage - 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
β 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