├── .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 | [![Pod Platform](http://img.shields.io/cocoapods/p/SDWebImage.svg?style=flat)](http://cocoadocs.org/docsets/SDWebImage/) 4 | [![Pod Version](http://img.shields.io/cocoapods/v/SDWebImage.svg?style=flat)](https://cocoapods.org/pods/Tutorials) 5 | [![Swift Package Manager](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](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 | --------------------------------------------------------------------------------