├── .gitignore
├── Package.swift
├── README.md
└── Sources
└── JTSkeleton
├── Internal
├── ShimmerConfig.swift
├── ShimmerEffect.swift
└── View+Extension.swift
└── Public
├── ShimmerView+ViewModifier.swift
├── ShimmerView.swift
└── Speed.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "JTSkeleton",
8 | platforms: [
9 | .iOS(.v15),
10 | .macOS(.v14),
11 | .tvOS(.v17),
12 | .watchOS(.v10),
13 | .visionOS(.v1)
14 | ],
15 | products: [
16 | .library(
17 | name: "JTSkeleton",
18 | targets: ["JTSkeleton"]),
19 | ],
20 | targets: [
21 | .target(
22 | name: "JTSkeleton",
23 | path: "./Sources/JTSkeleton"
24 | ),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JTSkeleton
2 |
3 | [](http://cocoadocs.org/docsets/SDWebImage/)
4 | [](https://cocoapods.org/pods/Tutorials)
5 | [](https://github.com/apple/swift-package-manager)
6 | [](http://mit-license.org)
7 |
8 | ## Table of Contents
9 | 1. [Overview](#overview)
10 | 2. [Features](#features)
11 | 3. [Requirements](#requirements)
12 | 4. [Installation](#installation)
13 | 5. [Usage](#usage)
14 | 6. [Author](#author)
15 |
16 | ## Overview
17 | Skeleton loading is a popular pattern for improving perceived performance by showing placeholders that mimic content layout during loading. While SwiftUI doesn’t support this out of the box, this SDK makes it easy to add skeleton views to any SwiftUI interface.
18 |
19 |
20 |
21 |
22 |
23 | With minimal setup, you can display animated or static placeholders that blend seamlessly into your design—improving user experience during data fetches or slow content loads.
24 |
25 | ## Features
26 |
27 | - **ShimmerView**: A ready-to-use view that displays animated shimmering placeholders, perfect for representing loading content with minimal setup.
28 |
29 | - **.shimmer**: A flexible modifier that applies the shimmer effect to any SwiftUI view, allowing full customization of layout, shape, and animation.
30 |
31 | ## Requirements
32 |
33 | | Platform | Minimum Version |
34 | |----------|-----------------|
35 | | iOS | 15.0 |
36 | | macOS | 14.0 |
37 | | tvOS | 17.0 |
38 | | watchOS | 10.0 |
39 | | visionOS | 1.0 |
40 |
41 | ## Installation
42 |
43 | ### Swift Package Manager
44 |
45 | 1. Open your project in Xcode.
46 | 2. Navigate to `File` > `Swift Packages` > `Add Package Dependency`.
47 | 3. Paste the repository URL: `https://github.com/Enryun/JTSkeleton`
48 | 4. Follow the prompts to add the package to your project.
49 |
50 | For more details on using Swift Package Manager, visit [Apple's Swift Package Manager documentation](https://swift.org/package-manager/).
51 |
52 |
53 | ## Usage
54 |
55 | ## Shimmer View
56 |
57 | A view displaying a shimmering loading placeholder.
58 |
59 | ```swift
60 | ShimmerView()
61 | ```
62 |
63 | This view simulates a 'shimmer' effect commonly used as a placeholder during content loading. It consists of multiple shimmering elements: a pair of small circular views at the top and bottom, and larger rectangular views in between, all showcasing the shimmer effect.
64 |
65 | `iOS`:
66 |
67 | https://github.com/user-attachments/assets/8f921028-2d13-4ed6-9281-998a883fe53e
68 |
69 | `MacOS`:
70 |
71 | https://github.com/user-attachments/assets/22f70fbf-d63d-4b06-994b-3b4b1b53415c
72 |
73 | No additional configuration is needed. The shimmer effect starts automatically, simulating content loading in your UI.
74 |
75 | Visibility:
76 | - Manage the visibility using `.opacity()` modifier or `if-else` conditions based on your application's state. This helps integrate the indicator seamlessly into your UI or hide it when not needed.
77 |
78 | For more customization, look at the view modifier to apply a shimmer effect to any SwiftUI view.
79 |
80 | ## Shimmer View Modifier
81 |
82 | Applies a shimmer effect to any SwiftUI view, enhancing the UI with a dynamic loading indicator.
83 |
84 | ```swift
85 | Text("Loading...")
86 | .shimmer(isActive: $isShimmer, tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: true)
87 |
88 | Text("Loading...")
89 | .shimmer(tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: false)
90 | ```
91 |
92 | Parameters:
93 | - `isActive`: A `Binding` that controls whether the shimmer effect is active.
94 | - `tint`: The background color of the shimmer.
95 | - `highlight`: The color of the shimmering highlight.
96 | - `blur`: The amount of blur applied to the shimmer effect. Default is 0.
97 | - `highlightOpacity`: The opacity of the shimmer highlight. Default is 1.
98 | - `speed`: The speed of the shimmer effect. Default is case .medium for 2 second.
99 | - `redacted`: A Boolean value that indicates whether the view's content should be redacted during the shimmer effect. Default is false.
100 |
101 | This function overlays a shimmer animation over the view it modifies, typically used as a placeholder during content loading. The effect can be extensively customized to match your app's style.
102 |
103 | Example loading redacted and non-redacted:
104 |
105 | ```swift
106 | VStack {
107 | HStack {
108 | Circle()
109 | .frame(width: 55, height: 55)
110 |
111 | VStack(alignment: .leading, spacing: 6) {
112 | RoundedRectangle(cornerRadius: 4)
113 | .frame(height: 10)
114 |
115 | RoundedRectangle(cornerRadius: 4)
116 | .frame(height: 10)
117 | .padding(.trailing, 50)
118 |
119 | RoundedRectangle(cornerRadius: 4)
120 | .frame(height: 10)
121 | .padding(.trailing, 100)
122 | }
123 | }
124 | .padding(15)
125 | .padding(.horizontal, 30)
126 | .shimmer(tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: true)
127 |
128 | Text("Loading...")
129 | .shimmer(tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: false)
130 | }
131 | ```
132 |
133 | https://github.com/user-attachments/assets/731719e0-7e23-4e56-8a4f-c86c81cc0b13
134 |
135 | Example loading with `Binding` isActive to control loading:
136 |
137 | ```swift
138 | @State private var isShimmer = true
139 |
140 | var body: some View {
141 | VStack {
142 | HStack {
143 | Circle()
144 | .frame(width: 55, height: 55)
145 |
146 | VStack(alignment: .leading, spacing: 6) {
147 | RoundedRectangle(cornerRadius: 4)
148 | .frame(height: 10)
149 |
150 | RoundedRectangle(cornerRadius: 4)
151 | .frame(height: 10)
152 | .padding(.trailing, 50)
153 |
154 | RoundedRectangle(cornerRadius: 4)
155 | .frame(height: 10)
156 | .padding(.trailing, 100)
157 | }
158 | }
159 | .padding(15)
160 | .padding(.horizontal, 30)
161 | .shimmer(isActive: $isShimmer, tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: true)
162 |
163 | Text("Loading...")
164 | .fontWeight(.semibold)
165 | .shimmer(isActive: $isShimmer, tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: false)
166 | }
167 | .onAppear {
168 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
169 | isShimmer = false
170 | }
171 | DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
172 | isShimmer = true
173 | }
174 | }
175 | }
176 | ```
177 |
178 | https://github.com/user-attachments/assets/fffd83e9-6a8e-4604-b6c7-4ba46308f662
179 |
180 | Customize the parameters to fit the style of your app's loading indicators.
181 |
182 | ## Author
183 |
184 | James Thang, find me on [LinkedIn](https://www.linkedin.com/in/jamesthang/)
185 |
186 | Learn more about SwiftUI, check out my book :books: on [Amazon](https://www.amazon.com/Ultimate-SwiftUI-Handbook-iOS-Developers-ebook/dp/B0CKBVY7V6/ref=tmm_kin_swatch_0?_encoding=UTF8&qid=1696776124&sr=8-1)
187 |
--------------------------------------------------------------------------------
/Sources/JTSkeleton/Internal/ShimmerConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShimmerConfig.swift
3 | // CommonSwiftUI
4 | //
5 | // Created by James Thang on 31/05/2024.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ShimmerConfig {
11 | var tint: Color
12 | var highlight: Color
13 | var blur: CGFloat
14 | var highlightOpacity: CGFloat
15 | var speed: CGFloat
16 | var redacted: Bool
17 |
18 | init(tint: Color, highlight: Color, blur: CGFloat = 0, highlightOpacity: CGFloat = 1, speed: CGFloat = 2, redacted: Bool = false) {
19 | self.tint = tint
20 | self.highlight = highlight
21 | self.blur = blur
22 | self.highlightOpacity = highlightOpacity
23 | self.speed = speed
24 | self.redacted = redacted
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/JTSkeleton/Internal/ShimmerEffect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShimmerEffect.swift
3 | // ShimmerAnimationEffect
4 | //
5 | // Created by user on 3/16/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Shimmer Effect Helper
11 | struct ShimmerEffectHelper: ViewModifier {
12 | // Shimmer Config
13 | var config: ShimmerConfig
14 | // Animation Properties
15 | @State private var moveTo: CGFloat = -0.7
16 |
17 | func body(content: Content) -> some View {
18 | content
19 | .apply {
20 | if config.redacted {
21 | $0.hidden()
22 | } else {
23 | $0
24 | }
25 | }
26 | .overlay {
27 | // Changing Tint Color
28 | Rectangle()
29 | .fill(config.tint)
30 | .mask {
31 | content
32 | }
33 | .overlay {
34 | // Shimmer
35 | GeometryReader {
36 | let size = $0.size
37 | let extraOffset = size.height / 2.5
38 |
39 | Rectangle()
40 | .fill(config.highlight)
41 | .mask {
42 | Rectangle()
43 | // Gradient for Glowing at the Center
44 | .fill(.linearGradient(colors: [.white.opacity(0), config.highlight.opacity(config.highlightOpacity), .white.opacity(0)], startPoint: .top, endPoint: .bottom))
45 | // Adding Blur
46 | .blur(radius: config.blur)
47 | // Rotating your choice of wish
48 | .rotationEffect(.init(degrees: -70))
49 | // Moving to the Start
50 | .offset(x: moveTo > 0 ? extraOffset : -extraOffset)
51 | .offset(x: size.width * moveTo)
52 | }
53 | }
54 | // Mask with the Content
55 | .mask {
56 | content
57 | }
58 | }
59 | // Animating Movement
60 | .onAppear {
61 | DispatchQueue.main.async {
62 | moveTo = 0.7
63 | }
64 | }
65 | .animation(.linear(duration: config.speed).repeatForever(autoreverses: false), value: moveTo)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/JTSkeleton/Internal/View+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+Extension.swift
3 | // CommonSwiftUI
4 | //
5 | // Created by James Thang on 17/03/2024.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 | /// Applies a custom transformation to a view and returns the resulting view.
12 | ///
13 | /// This function allows for applying transformations conditionally based on the iOS version. It's particularly useful for modifying views with version-specific features or styles.
14 | ///
15 | /// - Parameter transform: A closure that takes the original view as an argument and returns the modified view.
16 | ///
17 | /// ## Simplified Usage with iOS Version Check:
18 | /// ```swift
19 | /// Text("Conditional Styling")
20 | /// .apply {
21 | /// if #available(iOS 16.0, *) {
22 | /// $0.padding()
23 | /// } else {
24 | /// $0.padding().background(Color.gray)
25 | /// }
26 | /// }
27 | /// ```
28 | /// In this usage example, padding is applied universally, but a gray background is only applied if the iOS version is below 16.0.
29 | public func apply(@ViewBuilder _ transform: (Self) -> Content) -> Content {
30 | transform(self)
31 | }
32 |
33 | /// Extends `View` to conditionally use different `onChange` implementations based on iOS version.
34 | ///
35 | /// This function wraps the `onChange` modifier to handle changes to a value of any `Equatable` type, executing a closure upon change. It accommodates API differences between iOS versions by adjusting the method signature and behavior accordingly.
36 | ///
37 | /// - Parameters:
38 | /// - value: The value to observe for changes. Must conform to `Equatable`.
39 | /// - result: A closure that is executed when the observed value changes.
40 | ///
41 | /// ## Example Usage:
42 | /// ```swift
43 | /// Text("Example")
44 | /// .customChange(value: someObservableValue) { newValue in
45 | /// print("Value changed to \(newValue)")
46 | /// }
47 | /// ```
48 | ///
49 | /// Use this function to seamlessly handle value changes across different iOS versions with custom logic in the closure.
50 | @ViewBuilder
51 | public func customChange(value: T, result: @escaping (T) -> ()) -> some View {
52 | if #available(iOS 17, *) {
53 | self.onChange(of: value) { oldValue, newValue in
54 | result(newValue)
55 | }
56 | } else {
57 | self.onChange(of: value) { newValue in
58 | result(newValue)
59 | }
60 | }
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/JTSkeleton/Public/ShimmerView+ViewModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShimmerLoadingView.swift
3 | // CommonSwiftUI
4 | //
5 | // Created by James Thang on 22/03/2024.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 | /// Applies a shimmer effect to any SwiftUI view, enhancing the UI with a dynamic loading indicator.
12 | ///
13 | /// This function overlays a shimmer animation over the view it modifies, typically used as a placeholder during content loading. The effect can be extensively customized to match your app's style.
14 | ///
15 | /// - Parameters:
16 | /// - tint: The background color of the shimmer.
17 | /// - highlight: The color of the shimmering highlight.
18 | /// - blur: The amount of blur applied to the shimmer effect. Default is 0.
19 | /// - highlightOpacity: The opacity of the shimmer highlight. Default is 1.
20 | /// - speed: The speed of the shimmer effect, with a default of .medium (2 seconds).
21 | /// - redacted: A Boolean value that indicates whether the view's content should be redacted during the shimmer effect. Default is false.
22 | ///
23 | /// ## Usage:
24 | /// ```swift
25 | /// Text("Loading...")
26 | /// .shimmer(tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: true)
27 | /// ```
28 | /// Customize the parameters to fit the style and functionality of your app's loading indicators, with optional content redaction for enhanced visual effect.
29 | @ViewBuilder
30 | public func shimmer(tint: Color, highlight: Color, blur: CGFloat = 0, highlightOpacity: CGFloat = 1, speed: Speed = .medium, redacted: Bool = false) -> some View {
31 | let config = ShimmerConfig(tint: tint, highlight: highlight, blur: blur, highlightOpacity: highlightOpacity, speed: speed.timeInterval, redacted: redacted)
32 | self.modifier(ShimmerEffectHelper(config: config))
33 | }
34 |
35 | /// Applies a conditional shimmer effect to any SwiftUI view based on an active state.
36 | ///
37 | /// This function overlays a shimmer animation on the calling view when `isActive` is true. It's useful for placeholder effects during content loading. The shimmer's appearance can be customized with colors, opacity, speed, and optional redaction.
38 | ///
39 | /// - Parameters:
40 | /// - isActive: A `Binding` that controls whether the shimmer effect is active.
41 | /// - tint: The background color of the shimmer.
42 | /// - highlight: The color of the shimmering highlight.
43 | /// - blur: The amount of blur applied to the shimmer effect, with a default of 0.
44 | /// - highlightOpacity: The opacity of the shimmer highlight, defaulting to 1.
45 | /// - speed: The speed of the shimmer animation, with `.medium` as the default.
46 | /// - redacted: A Boolean that indicates whether the view should show a redacted placeholder state.
47 | ///
48 | /// ## Usage:
49 | /// ```swift
50 | /// Text("Loading...")
51 | /// .shimmer(isActive: $isLoading, tint: .gray.opacity(0.3), highlight: .white, blur: 5)
52 | /// ```
53 | /// Adjust the parameters to fit the style of your app's loading indicators, toggling the effect based on the application state.
54 | @ViewBuilder
55 | public func shimmer(isActive: Binding, tint: Color, highlight: Color, blur: CGFloat = 0, highlightOpacity: CGFloat = 1, speed: Speed = .medium, redacted: Bool = false) -> some View {
56 | if isActive.wrappedValue {
57 | let config = ShimmerConfig(tint: tint, highlight: highlight, blur: blur, highlightOpacity: highlightOpacity, speed: speed.timeInterval, redacted: redacted)
58 | self.modifier(ShimmerEffectHelper(config: config))
59 | } else {
60 | self
61 | }
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/JTSkeleton/Public/ShimmerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShimmerView.swift
3 | // CommonSwiftUI
4 | //
5 | // Created by James Thang on 18/06/2024.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A view displaying a shimmering loading placeholder.
11 | ///
12 | /// This view simulates a 'shimmer' effect commonly used as a placeholder during content loading. It consists of multiple shimmering elements: a pair of small circular views at the top and bottom, and larger rectangular views in between, all showcasing the shimmer effect.
13 | ///
14 | /// ## Usage:
15 | /// Simply initialize and place this view where content is loading:
16 | /// ```swift
17 | /// ShimmerView()
18 | /// ```
19 | ///
20 | /// No additional configuration is needed. The shimmer effect starts automatically, simulating content loading in your UI.
21 | public struct ShimmerView: View {
22 |
23 | public init() {}
24 |
25 | public var body: some View {
26 | VStack(spacing: 20) {
27 | smallLoadingView
28 | smallLoadingView
29 | RoundedRectangle(cornerRadius: 10)
30 | .frame(idealHeight: 180)
31 | .shimmer(tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: true)
32 | smallLoadingView
33 | smallLoadingView
34 | RoundedRectangle(cornerRadius: 10)
35 | .frame(idealHeight: 180)
36 | .shimmer(tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: true)
37 | }
38 | }
39 |
40 | var smallLoadingView: some View {
41 | HStack {
42 | Circle()
43 | .frame(width: 55, height: 55)
44 |
45 | VStack(alignment: .leading, spacing: 6) {
46 | RoundedRectangle(cornerRadius: 4)
47 | .frame(height: 10)
48 |
49 | RoundedRectangle(cornerRadius: 4)
50 | .frame(height: 10)
51 | .padding(.trailing, 50)
52 |
53 | RoundedRectangle(cornerRadius: 4)
54 | .frame(height: 10)
55 | .padding(.trailing, 100)
56 | }
57 | }
58 | .shimmer(tint: .gray.opacity(0.3), highlight: .white, blur: 5, redacted: true)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/JTSkeleton/Public/Speed.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Speed.swift
3 | // CommonSwiftUI
4 | //
5 | // Created by James Thang on 09/06/2024.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// An enumeration representing the speed of an operation.
11 | ///
12 | /// This enum provides both predefined and customizable time intervals to represent different speeds at which an operation can occur. Each case of the enum corresponds to a specific speed, with four predefined speeds and one customizable option, allowing for flexible loading behavior tailored to specific needs.
13 | ///
14 | /// - Cases:
15 | /// - flash: A very fast operation speed, with a time interval of 0.1 second.
16 | /// - fast: A fast operation speed, with a time interval of 1 second.
17 | /// - medium: A medium operation speed, with a time interval of 2 seconds.
18 | /// - slow: A slow operation speed, with a time interval of 3 seconds.
19 | /// - custom(TimeInterval): A customizable operation speed, where the time interval can be specified dynamically.
20 | public enum Speed {
21 | case flash
22 | case fast
23 | case medium
24 | case slow
25 | case custom(TimeInterval)
26 |
27 | var timeInterval: TimeInterval {
28 | switch self {
29 | case .flash:
30 | return 0.1
31 | case .fast:
32 | return 1
33 | case .medium:
34 | return 2
35 | case .slow:
36 | return 3
37 | case .custom(let interval):
38 | return interval
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------