├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Example
├── Example.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── Example
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Example.entitlements
│ ├── ExampleApp.swift
│ ├── ExampleView.swift
│ └── Preview Content
│ └── Preview Assets.xcassets
│ └── Contents.json
├── LICENSE
├── Package.swift
├── README.md
├── Resources
├── example.gif
└── vision.png
├── Sources
└── EasySkeleton
│ ├── EasySkeleton.swift
│ └── Extensions
│ ├── Color+Skeleton.swift
│ ├── ForEach+Skeleton.swift
│ ├── List+Skeleton.swift
│ └── View+Skeleton.swift
└── Tests
└── EasySkeletonTests
└── EasySkeletonTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | DB528E7C290AE19F0046B393 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB528E7B290AE19F0046B393 /* ExampleApp.swift */; };
11 | DB528E80290AE19F0046B393 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB528E7F290AE19F0046B393 /* Assets.xcassets */; };
12 | DB528E84290AE19F0046B393 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB528E83290AE19F0046B393 /* Preview Assets.xcassets */; };
13 | DB528E8B290AE24F0046B393 /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB528E8A290AE24F0046B393 /* ExampleView.swift */; };
14 | DB528E90290AE2990046B393 /* EasySkeleton in Frameworks */ = {isa = PBXBuildFile; productRef = DB528E8F290AE2990046B393 /* EasySkeleton */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | DB528E78290AE19F0046B393 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
19 | DB528E7B290AE19F0046B393 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
20 | DB528E7F290AE19F0046B393 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
21 | DB528E81290AE19F0046B393 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; };
22 | DB528E83290AE19F0046B393 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
23 | DB528E8A290AE24F0046B393 /* ExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; };
24 | DB528E8D290AE25E0046B393 /* EasySkeleton */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = EasySkeleton; path = ..; sourceTree = ""; };
25 | /* End PBXFileReference section */
26 |
27 | /* Begin PBXFrameworksBuildPhase section */
28 | DB528E75290AE19F0046B393 /* Frameworks */ = {
29 | isa = PBXFrameworksBuildPhase;
30 | buildActionMask = 2147483647;
31 | files = (
32 | DB528E90290AE2990046B393 /* EasySkeleton in Frameworks */,
33 | );
34 | runOnlyForDeploymentPostprocessing = 0;
35 | };
36 | /* End PBXFrameworksBuildPhase section */
37 |
38 | /* Begin PBXGroup section */
39 | DB528E6F290AE19F0046B393 = {
40 | isa = PBXGroup;
41 | children = (
42 | DB528E8C290AE25E0046B393 /* Packages */,
43 | DB528E7A290AE19F0046B393 /* Example */,
44 | DB528E79290AE19F0046B393 /* Products */,
45 | DB528E8E290AE2990046B393 /* Frameworks */,
46 | );
47 | sourceTree = "";
48 | };
49 | DB528E79290AE19F0046B393 /* Products */ = {
50 | isa = PBXGroup;
51 | children = (
52 | DB528E78290AE19F0046B393 /* Example.app */,
53 | );
54 | name = Products;
55 | sourceTree = "";
56 | };
57 | DB528E7A290AE19F0046B393 /* Example */ = {
58 | isa = PBXGroup;
59 | children = (
60 | DB528E8A290AE24F0046B393 /* ExampleView.swift */,
61 | DB528E7B290AE19F0046B393 /* ExampleApp.swift */,
62 | DB528E7F290AE19F0046B393 /* Assets.xcassets */,
63 | DB528E81290AE19F0046B393 /* Example.entitlements */,
64 | DB528E82290AE19F0046B393 /* Preview Content */,
65 | );
66 | path = Example;
67 | sourceTree = "";
68 | };
69 | DB528E82290AE19F0046B393 /* Preview Content */ = {
70 | isa = PBXGroup;
71 | children = (
72 | DB528E83290AE19F0046B393 /* Preview Assets.xcassets */,
73 | );
74 | path = "Preview Content";
75 | sourceTree = "";
76 | };
77 | DB528E8C290AE25E0046B393 /* Packages */ = {
78 | isa = PBXGroup;
79 | children = (
80 | DB528E8D290AE25E0046B393 /* EasySkeleton */,
81 | );
82 | name = Packages;
83 | sourceTree = "";
84 | };
85 | DB528E8E290AE2990046B393 /* Frameworks */ = {
86 | isa = PBXGroup;
87 | children = (
88 | );
89 | name = Frameworks;
90 | sourceTree = "";
91 | };
92 | /* End PBXGroup section */
93 |
94 | /* Begin PBXNativeTarget section */
95 | DB528E77290AE19F0046B393 /* Example */ = {
96 | isa = PBXNativeTarget;
97 | buildConfigurationList = DB528E87290AE19F0046B393 /* Build configuration list for PBXNativeTarget "Example" */;
98 | buildPhases = (
99 | DB528E74290AE19F0046B393 /* Sources */,
100 | DB528E75290AE19F0046B393 /* Frameworks */,
101 | DB528E76290AE19F0046B393 /* Resources */,
102 | );
103 | buildRules = (
104 | );
105 | dependencies = (
106 | );
107 | name = Example;
108 | packageProductDependencies = (
109 | DB528E8F290AE2990046B393 /* EasySkeleton */,
110 | );
111 | productName = Example;
112 | productReference = DB528E78290AE19F0046B393 /* Example.app */;
113 | productType = "com.apple.product-type.application";
114 | };
115 | /* End PBXNativeTarget section */
116 |
117 | /* Begin PBXProject section */
118 | DB528E70290AE19F0046B393 /* Project object */ = {
119 | isa = PBXProject;
120 | attributes = {
121 | BuildIndependentTargetsInParallel = 1;
122 | LastSwiftUpdateCheck = 1400;
123 | LastUpgradeCheck = 1540;
124 | TargetAttributes = {
125 | DB528E77290AE19F0046B393 = {
126 | CreatedOnToolsVersion = 14.0;
127 | };
128 | };
129 | };
130 | buildConfigurationList = DB528E73290AE19F0046B393 /* Build configuration list for PBXProject "Example" */;
131 | compatibilityVersion = "Xcode 14.0";
132 | developmentRegion = en;
133 | hasScannedForEncodings = 0;
134 | knownRegions = (
135 | en,
136 | Base,
137 | );
138 | mainGroup = DB528E6F290AE19F0046B393;
139 | productRefGroup = DB528E79290AE19F0046B393 /* Products */;
140 | projectDirPath = "";
141 | projectRoot = "";
142 | targets = (
143 | DB528E77290AE19F0046B393 /* Example */,
144 | );
145 | };
146 | /* End PBXProject section */
147 |
148 | /* Begin PBXResourcesBuildPhase section */
149 | DB528E76290AE19F0046B393 /* Resources */ = {
150 | isa = PBXResourcesBuildPhase;
151 | buildActionMask = 2147483647;
152 | files = (
153 | DB528E84290AE19F0046B393 /* Preview Assets.xcassets in Resources */,
154 | DB528E80290AE19F0046B393 /* Assets.xcassets in Resources */,
155 | );
156 | runOnlyForDeploymentPostprocessing = 0;
157 | };
158 | /* End PBXResourcesBuildPhase section */
159 |
160 | /* Begin PBXSourcesBuildPhase section */
161 | DB528E74290AE19F0046B393 /* Sources */ = {
162 | isa = PBXSourcesBuildPhase;
163 | buildActionMask = 2147483647;
164 | files = (
165 | DB528E7C290AE19F0046B393 /* ExampleApp.swift in Sources */,
166 | DB528E8B290AE24F0046B393 /* ExampleView.swift in Sources */,
167 | );
168 | runOnlyForDeploymentPostprocessing = 0;
169 | };
170 | /* End PBXSourcesBuildPhase section */
171 |
172 | /* Begin XCBuildConfiguration section */
173 | DB528E85290AE19F0046B393 /* Debug */ = {
174 | isa = XCBuildConfiguration;
175 | buildSettings = {
176 | ALWAYS_SEARCH_USER_PATHS = NO;
177 | CLANG_ANALYZER_NONNULL = YES;
178 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
179 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
180 | CLANG_ENABLE_MODULES = YES;
181 | CLANG_ENABLE_OBJC_ARC = YES;
182 | CLANG_ENABLE_OBJC_WEAK = YES;
183 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
184 | CLANG_WARN_BOOL_CONVERSION = YES;
185 | CLANG_WARN_COMMA = YES;
186 | CLANG_WARN_CONSTANT_CONVERSION = YES;
187 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
188 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
189 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
190 | CLANG_WARN_EMPTY_BODY = YES;
191 | CLANG_WARN_ENUM_CONVERSION = YES;
192 | CLANG_WARN_INFINITE_RECURSION = YES;
193 | CLANG_WARN_INT_CONVERSION = YES;
194 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
195 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
196 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
197 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
198 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
199 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
200 | CLANG_WARN_STRICT_PROTOTYPES = YES;
201 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
202 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
203 | CLANG_WARN_UNREACHABLE_CODE = YES;
204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
205 | COPY_PHASE_STRIP = NO;
206 | DEAD_CODE_STRIPPING = YES;
207 | DEBUG_INFORMATION_FORMAT = dwarf;
208 | ENABLE_STRICT_OBJC_MSGSEND = YES;
209 | ENABLE_TESTABILITY = YES;
210 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
211 | GCC_C_LANGUAGE_STANDARD = gnu11;
212 | GCC_DYNAMIC_NO_PIC = NO;
213 | GCC_NO_COMMON_BLOCKS = YES;
214 | GCC_OPTIMIZATION_LEVEL = 0;
215 | GCC_PREPROCESSOR_DEFINITIONS = (
216 | "DEBUG=1",
217 | "$(inherited)",
218 | );
219 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
220 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
221 | GCC_WARN_UNDECLARED_SELECTOR = YES;
222 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
223 | GCC_WARN_UNUSED_FUNCTION = YES;
224 | GCC_WARN_UNUSED_VARIABLE = YES;
225 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
226 | MTL_FAST_MATH = YES;
227 | ONLY_ACTIVE_ARCH = YES;
228 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
229 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
230 | };
231 | name = Debug;
232 | };
233 | DB528E86290AE19F0046B393 /* Release */ = {
234 | isa = XCBuildConfiguration;
235 | buildSettings = {
236 | ALWAYS_SEARCH_USER_PATHS = NO;
237 | CLANG_ANALYZER_NONNULL = YES;
238 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
240 | CLANG_ENABLE_MODULES = YES;
241 | CLANG_ENABLE_OBJC_ARC = YES;
242 | CLANG_ENABLE_OBJC_WEAK = YES;
243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
244 | CLANG_WARN_BOOL_CONVERSION = YES;
245 | CLANG_WARN_COMMA = YES;
246 | CLANG_WARN_CONSTANT_CONVERSION = YES;
247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
249 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
250 | CLANG_WARN_EMPTY_BODY = YES;
251 | CLANG_WARN_ENUM_CONVERSION = YES;
252 | CLANG_WARN_INFINITE_RECURSION = YES;
253 | CLANG_WARN_INT_CONVERSION = YES;
254 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
255 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
256 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
257 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
258 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
260 | CLANG_WARN_STRICT_PROTOTYPES = YES;
261 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
262 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
263 | CLANG_WARN_UNREACHABLE_CODE = YES;
264 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
265 | COPY_PHASE_STRIP = NO;
266 | DEAD_CODE_STRIPPING = YES;
267 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
268 | ENABLE_NS_ASSERTIONS = NO;
269 | ENABLE_STRICT_OBJC_MSGSEND = YES;
270 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
271 | GCC_C_LANGUAGE_STANDARD = gnu11;
272 | GCC_NO_COMMON_BLOCKS = YES;
273 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
274 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
275 | GCC_WARN_UNDECLARED_SELECTOR = YES;
276 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
277 | GCC_WARN_UNUSED_FUNCTION = YES;
278 | GCC_WARN_UNUSED_VARIABLE = YES;
279 | MTL_ENABLE_DEBUG_INFO = NO;
280 | MTL_FAST_MATH = YES;
281 | SWIFT_COMPILATION_MODE = wholemodule;
282 | SWIFT_OPTIMIZATION_LEVEL = "-O";
283 | };
284 | name = Release;
285 | };
286 | DB528E88290AE19F0046B393 /* Debug */ = {
287 | isa = XCBuildConfiguration;
288 | buildSettings = {
289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
290 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
291 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements;
292 | CODE_SIGN_STYLE = Automatic;
293 | CURRENT_PROJECT_VERSION = 1;
294 | DEAD_CODE_STRIPPING = YES;
295 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
296 | ENABLE_PREVIEWS = YES;
297 | GENERATE_INFOPLIST_FILE = YES;
298 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
299 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
300 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
301 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
302 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
303 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
304 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
305 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
306 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
307 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
308 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
309 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
310 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
311 | MACOSX_DEPLOYMENT_TARGET = 13.0;
312 | MARKETING_VERSION = 1.0;
313 | PRODUCT_BUNDLE_IDENTIFIER = dev.litecode.Example;
314 | PRODUCT_NAME = "$(TARGET_NAME)";
315 | SDKROOT = auto;
316 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
317 | SUPPORTS_MACCATALYST = NO;
318 | SWIFT_EMIT_LOC_STRINGS = YES;
319 | SWIFT_VERSION = 5.0;
320 | TARGETED_DEVICE_FAMILY = "1,2,7";
321 | };
322 | name = Debug;
323 | };
324 | DB528E89290AE19F0046B393 /* Release */ = {
325 | isa = XCBuildConfiguration;
326 | buildSettings = {
327 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
328 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
329 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements;
330 | CODE_SIGN_STYLE = Automatic;
331 | CURRENT_PROJECT_VERSION = 1;
332 | DEAD_CODE_STRIPPING = YES;
333 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
334 | ENABLE_PREVIEWS = YES;
335 | GENERATE_INFOPLIST_FILE = YES;
336 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
337 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
338 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
339 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
340 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
341 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
342 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
343 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
344 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
345 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
346 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
347 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
348 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
349 | MACOSX_DEPLOYMENT_TARGET = 13.0;
350 | MARKETING_VERSION = 1.0;
351 | PRODUCT_BUNDLE_IDENTIFIER = dev.litecode.Example;
352 | PRODUCT_NAME = "$(TARGET_NAME)";
353 | SDKROOT = auto;
354 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
355 | SUPPORTS_MACCATALYST = NO;
356 | SWIFT_EMIT_LOC_STRINGS = YES;
357 | SWIFT_VERSION = 5.0;
358 | TARGETED_DEVICE_FAMILY = "1,2,7";
359 | };
360 | name = Release;
361 | };
362 | /* End XCBuildConfiguration section */
363 |
364 | /* Begin XCConfigurationList section */
365 | DB528E73290AE19F0046B393 /* Build configuration list for PBXProject "Example" */ = {
366 | isa = XCConfigurationList;
367 | buildConfigurations = (
368 | DB528E85290AE19F0046B393 /* Debug */,
369 | DB528E86290AE19F0046B393 /* Release */,
370 | );
371 | defaultConfigurationIsVisible = 0;
372 | defaultConfigurationName = Release;
373 | };
374 | DB528E87290AE19F0046B393 /* Build configuration list for PBXNativeTarget "Example" */ = {
375 | isa = XCConfigurationList;
376 | buildConfigurations = (
377 | DB528E88290AE19F0046B393 /* Debug */,
378 | DB528E89290AE19F0046B393 /* Release */,
379 | );
380 | defaultConfigurationIsVisible = 0;
381 | defaultConfigurationName = Release;
382 | };
383 | /* End XCConfigurationList section */
384 |
385 | /* Begin XCSwiftPackageProductDependency section */
386 | DB528E8F290AE2990046B393 /* EasySkeleton */ = {
387 | isa = XCSwiftPackageProductDependency;
388 | productName = EasySkeleton;
389 | };
390 | /* End XCSwiftPackageProductDependency section */
391 | };
392 | rootObject = DB528E70290AE19F0046B393 /* Project object */;
393 | }
394 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/Example.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/Example/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleView.swift
3 | //
4 | //
5 | // Created by v.prusakov on 10/27/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct ExampleApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ExampleView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Example/Example/ExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleView.swift
3 | //
4 | //
5 | // Created by v.prusakov on 10/27/22.
6 | //
7 |
8 | import SwiftUI
9 | import EasySkeleton
10 |
11 | struct ExampleView: View {
12 |
13 | @State private var isSkeletonActive = true
14 |
15 | var body: some View {
16 | ZStack {
17 | ScrollView {
18 | VStack(alignment: .leading, spacing: 8) {
19 | HeaderView()
20 |
21 | VStack(alignment: .leading, spacing: 8) {
22 | Text("Big announcement!!")
23 | .font(.title)
24 | .foregroundColor(.white)
25 |
26 | Text("Pay now, get latter!")
27 | .font(.body)
28 | .foregroundColor(.white.opacity(0.8))
29 | }
30 | .frame(maxWidth: .infinity)
31 | .padding(.all, 16)
32 | .background(
33 | RoundedRectangle(cornerRadius: 16)
34 | .fill(Color.accentColor)
35 | )
36 | .padding(.bottom, 16)
37 | .skeletonable()
38 |
39 | ForEach(0..<10, id: \.self) { index in
40 | self.cell(at: index)
41 | }
42 | .skeletonForEach(itemsCount: 5) { index in
43 | self.cell(at: index)
44 | }
45 |
46 | Spacer()
47 | }
48 | }
49 | .padding(.bottom, 32)
50 | .skeletonCornerRadius(16, style: .continuous)
51 | .setSkeleton(
52 | self.$isSkeletonActive,
53 | animation: Animation.default,
54 | transition: AnyTransition.opacity,
55 | cornerRadius: 12
56 | )
57 |
58 | self.skeletonControlButton
59 | }
60 | .padding(.all)
61 | .frame(minWidth: 348, minHeight: 400)
62 | }
63 |
64 | @ViewBuilder
65 | private var skeletonControlButton: some View {
66 | VStack {
67 |
68 | Spacer()
69 |
70 | HStack {
71 |
72 | Spacer()
73 |
74 | Button {
75 | withAnimation {
76 | self.isSkeletonActive.toggle()
77 | }
78 | } label: {
79 | Text("Skeleton Active: \(isSkeletonActive ? "Yes" : "No")")
80 | }
81 |
82 | Spacer()
83 | }
84 | .frame(maxWidth: .infinity, idealHeight: 56)
85 | }
86 | }
87 |
88 | private func cell(at index: Int) -> some View {
89 | VStack(alignment: .leading, spacing: 4) {
90 | Text("Element at index \(index)")
91 | .font(.body)
92 | .skeletonable()
93 |
94 | Text("Some text")
95 | .font(.caption)
96 | .skeletonable()
97 | }
98 | .unskeletonable()
99 | }
100 | }
101 |
102 | struct HeaderView: View {
103 |
104 | @Environment(\.skeleton) private var skeleton
105 |
106 | var body: some View {
107 | VStack(alignment: .leading) {
108 | HStack {
109 | Text("Hello there!")
110 | .font(.largeTitle)
111 |
112 | Spacer()
113 |
114 | Button(action: {
115 |
116 | }, label: {
117 | Text("ES")
118 | .font(.system(size: 17))
119 | .frame(width: 48, height: 48)
120 | .overlay(
121 | RoundedRectangle(cornerRadius: 18, style: .continuous)
122 | .stroke(Color.skeleton)
123 | )
124 | })
125 | .buttonStyle(.plain)
126 | .skeletonable()
127 | .disabled(skeleton.isSkeletonActive)
128 | }
129 |
130 | Text("Easy Skeleton")
131 | .skeletonable()
132 | .skeletonLinesCount(2)
133 | }
134 |
135 | }
136 | }
137 |
138 | #Preview {
139 | ExampleView()
140 | }
141 |
--------------------------------------------------------------------------------
/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Vladislav Prusakov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
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: "EasySkeleton",
8 | platforms: [
9 | .iOS(.v16),
10 | .tvOS(.v16),
11 | .macOS(.v13),
12 | .visionOS(.v1)
13 | ],
14 | products: [
15 | .library(
16 | name: "EasySkeleton",
17 | targets: ["EasySkeleton"]
18 | )
19 | ],
20 | targets: [
21 | .target(
22 | name: "EasySkeleton",
23 | dependencies: []
24 | ),
25 |
26 | .testTarget(
27 | name: "EasySkeletonTests",
28 | dependencies: ["EasySkeleton"]
29 | ),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EasySkeleton
2 |
3 | [](https://img.shields.io/badge/Swift-5.-Orange?style=flat-square)
4 | [](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_visionOS-green?style=flat-square)
5 |
6 | Fast and lightweight skeleton framework for SwiftUI!
7 |
8 | 
9 | 
10 |
11 | ## How to use
12 |
13 | We use a simple system with environment to pass information about the skeleton across all the views, but you should tell your layout where and how it should be displayed. Some examples:
14 |
15 | ```swift
16 |
17 | import EasySkeleton
18 |
19 | struct ContentView: View {
20 |
21 | @State private var isLoading = true
22 |
23 | var body: some View {
24 | VStack {
25 | Text("Some title")
26 | .skeletonable() // This text will be skeletonable
27 |
28 | Text("Body")
29 | .skeletonable() // This text will be skeletonable too
30 | }
31 | .setSkeleton($isLoading) // Control skeletonable state
32 | }
33 | }
34 | ```
35 |
36 | But what if you want to design your own skeleton? You can do so with modifiers:
37 |
38 | ```swift
39 |
40 | Text("Some title")
41 | .skeletonable()
42 | .skeletonCornerRadius(16) // Your skeleton will be rounded. Great!
43 |
44 | ```
45 |
46 | And what if for example, you want to use a custom style or animation ? Its easy! Just use the extra arguments in your `setSkeleton` method:
47 |
48 |
49 | ```swift
50 |
51 | struct ContentView {
52 |
53 | @State private var isLoading = true
54 |
55 | var body: some View {
56 | VStack {
57 | /// ...
58 | }
59 | .setSkeleton(
60 | $isLoading,
61 | animationType: .solid(Color.blue), // by default is .gradient(Color.skeleton.makeGradient())
62 | animation: Animation.default, // Default animation
63 | transition: AnyTransition.opacity // Transition how skeleton appears or disappers.
64 | )
65 | }
66 |
67 | }
68 |
69 | ```
70 |
71 | To disable the skeleton in your child views just call the `unskeletonable` method
72 |
73 | ```swift
74 |
75 | struct Row {
76 |
77 | var body: some View {
78 | VStack {
79 | /// ...
80 | }
81 | .unskeletonable() // Your view will not be skeletonable
82 | }
83 |
84 | }
85 |
86 | struct ContentView: View {
87 | var body: some View {
88 | Row()
89 | .skeletonable() // Doesn't apply on Row
90 | }
91 | }
92 |
93 | ```
94 |
95 | EasySkeleton it a great fit to work with ForEach loops. You can apply the native ForEach and modify it with the `skeletonForEach` modifier:
96 |
97 | ```swift
98 | struct NewsView: View {
99 | var body: some View {
100 | ForEach(items) {
101 | NewsRow(item: $0)
102 | }
103 | .skeletonForEach(itemsCount: 4) { _ in // only applies on ForEach component to avoid using `SkeletonForEach` or similar component
104 | NewsRow()
105 | .skeletonFrame(height: 56) // Set frame for skeleton overlay, but also, you can set native `frame()` modifier.
106 | }
107 | }
108 | }
109 | ```
110 |
111 | And last, but not least! The main killer feature: Reading your skeleton state on the fly using Environment feature!
112 |
113 | ```swift
114 |
115 | struct Header: View {
116 |
117 | @Environment(\.skeleton) private var skeleton // You can grab skeleton state from any view!
118 |
119 | var body: some View {
120 | Button("Press to me") {
121 | /// ...
122 | }
123 | .disabled(skeleton.isSkeletonActive) // Control your layout when skeleton is active!
124 | }
125 | }
126 |
127 | ```
128 |
129 | ## Contributing
130 |
131 | You're welcome to change that library and I hope we will make a best tool for any developer!
132 |
--------------------------------------------------------------------------------
/Resources/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpectralDragon/EasySkeleton/54f616e5f58cbb2aabb5b671921e470689c91ca5/Resources/example.gif
--------------------------------------------------------------------------------
/Resources/vision.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SpectralDragon/EasySkeleton/54f616e5f58cbb2aabb5b671921e470689c91ca5/Resources/vision.png
--------------------------------------------------------------------------------
/Sources/EasySkeleton/EasySkeleton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EasySkeleton.swift
3 | // EasySkeleton
4 | //
5 | // Created by v.prusakov on 10/27/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct SkeletonData: @unchecked Sendable {
11 |
12 | public enum AnimationType {
13 | case solid(Color)
14 | case gradient([Color])
15 | }
16 |
17 | // Set current state for nested skeletons
18 | fileprivate(set) var skeletonActive: Binding = .constant(false)
19 | public fileprivate(set) var animation: Animation = Animation.easeInOut(duration: 1)
20 |
21 | public var isSkeletonActive: Bool {
22 | self.skeletonActive.wrappedValue
23 | }
24 |
25 | public fileprivate(set) var transition: AnyTransition = AnyTransition
26 | .asymmetric(insertion: .opacity, removal: .opacity)
27 | .animation(.easeInOut(duration: 0.2))
28 |
29 | public fileprivate(set) var cornerRadius: CGFloat = 0
30 | public fileprivate(set) var cornerStyle: RoundedCornerStyle = .circular
31 | public fileprivate(set) var customShape: AnyShape?
32 |
33 | public fileprivate(set) var skeletonWidth: CGFloat?
34 | public fileprivate(set) var skeletonHeight: CGFloat?
35 |
36 | /// Only for Text
37 | public fileprivate(set) var linesCount: Int?
38 | public fileprivate(set) var animationType: AnimationType = .solid(Color.skeleton)
39 | }
40 |
41 |
42 | struct SkeletonableEnvironmentKey: EnvironmentKey {
43 | static var defaultValue: SkeletonData = .init()
44 | }
45 |
46 | public extension EnvironmentValues {
47 | internal(set) var skeleton: SkeletonData {
48 | get { self[SkeletonableEnvironmentKey.self] }
49 | set { self[SkeletonableEnvironmentKey.self] = newValue }
50 | }
51 | }
52 |
53 | struct SkeletonPreferenceKey: PreferenceKey {
54 | static var defaultValue: Bool = true
55 |
56 | static func reduce(value: inout Bool, nextValue: () -> Bool) {
57 | value = nextValue()
58 | }
59 | }
60 |
61 | public extension View {
62 |
63 | /// Apply skeleton modifier to View
64 | func skeletonable() -> some View {
65 | self.modifier(SkeletonViewModifier(contentType: .view))
66 | }
67 |
68 | /// Disable skeleton for current view and it childs
69 | func unskeletonable() -> some View {
70 | self.preference(key: SkeletonPreferenceKey.self, value: false)
71 | }
72 |
73 | /// Apply corner radius to skeleton modifier
74 | func skeletonCornerRadius(_ cornerRadius: CGFloat, style: RoundedCornerStyle = .circular) -> some View {
75 | self.transformEnvironment(\.skeleton) { skeleton in
76 | skeleton.cornerRadius = cornerRadius
77 | skeleton.cornerStyle = style
78 | }
79 | }
80 |
81 | /// Apply `linesCount` to skeleton modifier if SkeletonModifier was applied to `Text`
82 | func skeletonLinesCount(_ count: Int?) -> some View {
83 | self.transformEnvironment(\.skeleton) { skeleton in
84 | skeleton.linesCount = count
85 | }
86 | }
87 |
88 | /// Apply width and/or height modification for skeleton
89 | func skeletonFrame(width: CGFloat? = nil, height: CGFloat? = nil) -> some View {
90 | self.transformEnvironment(\.skeleton) { skeleton in
91 | height.flatMap { skeleton.skeletonHeight = $0 }
92 | width.flatMap { skeleton.skeletonWidth = $0 }
93 | }
94 | }
95 |
96 | /// Change shape for skeleton.
97 | /// - Parameter shape: New shape for skeleton. Set nil to return default skeleton.
98 | func skeletonShape(_ shape: S?) -> some View {
99 | self.transformEnvironment(\.skeleton) { skeleton in
100 | skeleton.customShape = shape.flatMap(AnyShape.init)
101 | }
102 | }
103 |
104 | /// Change state for nested skeletons settings in environment
105 | /// - Parameter isActive: Control visible state for skeletons.
106 | /// - Parameter animationType: Set animation behaviour for skeletons.
107 | /// - Parameter animation: Animation for skeletons.
108 | /// - Parameter transition: Transition for appearing and disappearing for skeletons.
109 | func setSkeleton(
110 | _ isActive: Binding,
111 | animationType: SkeletonData.AnimationType = .gradient(Color.skeleton.makeGradient()),
112 | animation: Animation? = nil,
113 | transition: AnyTransition? = nil,
114 | cornerRadius: CGFloat = 0
115 | ) -> some View {
116 | self.transformEnvironment(\.skeleton) { skeleton in
117 | skeleton.skeletonActive = isActive.animation(animation)
118 | skeleton.animationType = animationType
119 | skeleton.cornerRadius = cornerRadius
120 | animation.flatMap { skeleton.animation = $0 }
121 | transition.flatMap { skeleton.transition = $0 }
122 | }
123 | }
124 | }
125 |
126 | public extension Text {
127 | /// Apply skeleton modifer to Text.
128 | /// - Note: Skeleton applied to Text can be customized with `skeletonLinesCount(_ count: Int?)` method
129 | func skeletonable() -> ModifiedContent {
130 | self.modifier(SkeletonViewModifier(contentType: .text))
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/Sources/EasySkeleton/Extensions/Color+Skeleton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color+Skeleton.swift
3 | //
4 | //
5 | // Created by v.prusakov on 10/27/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if canImport(UIKit)
11 | import UIKit
12 |
13 | typealias ESColor = UIColor
14 |
15 | #elseif canImport(AppKit)
16 | import AppKit
17 |
18 | typealias ESColor = NSColor
19 | #endif
20 |
21 | extension Color {
22 | var complementaryColor: Color {
23 | isLight ? darker : lighter
24 | }
25 |
26 | var lighter: Color {
27 | adjust(by: 1.35)
28 | }
29 |
30 | var darker: Color {
31 | adjust(by: 0.9)
32 | }
33 |
34 | var isLight: Bool {
35 | guard let components = self.uiColor.cgColor.components,
36 | components.count >= 3 else { return false }
37 | let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
38 | return !(brightness < 0.5)
39 | }
40 |
41 | func adjust(by percent: CGFloat) -> Color {
42 | // swiftlint:disable:next identifier_name
43 | var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
44 | self.uiColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
45 | return Color(ESColor(hue: h, saturation: s, brightness: b * percent, alpha: a))
46 | }
47 |
48 | public func makeGradient() -> [Color] {
49 | [self, self.complementaryColor, self]
50 | }
51 | }
52 |
53 | extension Color {
54 | var uiColor: ESColor {
55 | ESColor(self)
56 | }
57 | }
58 |
59 |
60 | public extension Color {
61 | static var skeleton: Color {
62 | #if os(iOS)
63 | return Color(.systemGray4)
64 | #elseif os(tvOS)
65 | return Color(.tertiaryLabel)
66 | #elseif os(watchOS)
67 | return Color.secondary
68 | #elseif os(macOS)
69 | return Color(NSColor(red: 0.82, green: 0.82, blue: 0.84, alpha: 1))
70 | #else
71 | return Color(.tertiaryLabel)
72 | #endif
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/EasySkeleton/Extensions/ForEach+Skeleton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForEach+Skeleton.swift
3 | //
4 | //
5 | // Created by v.prusakov on 10/27/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SkeletonForEachModifier: ViewModifier {
11 |
12 | @Environment(\.skeleton) private var skeleton
13 |
14 | let itemsCount: Int
15 | let autoSkeletonable: Bool
16 | @ViewBuilder var content: (Int) -> V
17 |
18 | @ViewBuilder
19 | func body(content: Content) -> some View {
20 | if skeleton.isSkeletonActive {
21 | ForEach(0..(
38 | itemsCount: Int,
39 | autoSkeletonable: Bool = true,
40 | @ViewBuilder content: @escaping (Int) -> V
41 | ) -> some View {
42 | self.modifier(
43 | SkeletonForEachModifier(
44 | itemsCount: itemsCount,
45 | autoSkeletonable: autoSkeletonable,
46 | content: content
47 | )
48 | )
49 | }
50 | }
51 |
52 | public extension View {
53 | /// Create ForEach view if skeleton is active.
54 | func skeletonForEach(
55 | itemsCount: Int,
56 | autoSkeletonable: Bool = true,
57 | @ViewBuilder content: @escaping (Int) -> V
58 | ) -> some View {
59 | self.modifier(
60 | SkeletonForEachModifier(
61 | itemsCount: itemsCount,
62 | autoSkeletonable: autoSkeletonable,
63 | content: content
64 | )
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/EasySkeleton/Extensions/List+Skeleton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // List+Skeleton.swift
3 | //
4 | //
5 | // Created by Jalil Fierro on 23/07/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SkeletonListModifier: ViewModifier {
11 |
12 | @Environment(\.skeleton) private var skeleton
13 |
14 | let itemsCount: Int
15 | let autoSkeletonable: Bool
16 | @ViewBuilder var content: (Int) -> V
17 |
18 | @ViewBuilder
19 | func body(content: Content) -> some View {
20 | if skeleton.isSkeletonActive {
21 | List(0..(
37 | itemsCount: Int,
38 | autoSkeletonable: Bool = true,
39 | @ViewBuilder content: @escaping (Int) -> V
40 | ) -> some View {
41 | self.modifier(
42 | SkeletonListModifier(
43 | itemsCount: itemsCount,
44 | autoSkeletonable: autoSkeletonable,
45 | content: content
46 | )
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/EasySkeleton/Extensions/View+Skeleton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+Skeleton.swift
3 | //
4 | //
5 | // Created by v.prusakov on 10/27/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct SkeletonViewModifier: ViewModifier {
11 |
12 | @Environment(\.skeleton) private var skeleton
13 |
14 | enum ContentType {
15 | case text
16 | case view
17 | }
18 |
19 | let contentType: ContentType
20 |
21 | @State private var internalIsSkeletonable = true
22 |
23 | var isSkeletonable: Bool {
24 | self.skeleton.isSkeletonActive && self.internalIsSkeletonable
25 | }
26 |
27 | public func body(content: Content) -> some View {
28 | content
29 | .onPreferenceChange(SkeletonPreferenceKey.self, perform: { value in
30 | self.internalIsSkeletonable = value
31 | })
32 | .opacity(self.isSkeletonable ? 0 : 1)
33 | .allowsHitTesting(!self.isSkeletonable)
34 | .overlay(
35 | self.body
36 | .transition(self.skeleton.transition)
37 | )
38 | }
39 |
40 | @ViewBuilder
41 | private var body: some View {
42 | if self.isSkeletonable {
43 | if let linesCount = self.skeleton.linesCount, self.contentType == .text {
44 | ForEach(0 ..< linesCount, id: \.self) { _ in
45 | SkeletonShape()
46 | .frame(width: self.skeleton.skeletonWidth, height: self.skeleton.skeletonHeight)
47 | }
48 | } else {
49 | SkeletonShape()
50 | .frame(width: self.skeleton.skeletonWidth, height: self.skeleton.skeletonHeight)
51 | }
52 | }
53 | }
54 | }
55 |
56 | private struct SkeletonShape: Shape, @unchecked Sendable {
57 |
58 | @Environment(\.skeleton) private var skeleton
59 | @State private var opacity: Double = 1
60 |
61 | func path(in rect: CGRect) -> Path {
62 | skeleton.customShape?.path(in: rect) ?? RoundedRectangle(cornerRadius: self.skeleton.cornerRadius).path(in: rect)
63 | }
64 |
65 | var body: some View {
66 | self.shapeView
67 | .animation(self.skeleton.animation, value: self.skeleton.isSkeletonActive)
68 | .onAppear {
69 | DispatchQueue.main.async {
70 | if self.skeleton.isSkeletonActive {
71 | withAnimation(self.skeleton.animation.repeatForever(autoreverses: true), {
72 | self.opacity = 0
73 | })
74 | }
75 | }
76 | }
77 | }
78 |
79 | @ViewBuilder
80 | private var shapeView: some View {
81 | switch self.skeleton.animationType {
82 | case .solid(let color):
83 | ZStack {
84 | self.fill(color)
85 | self.fill(color.complementaryColor)
86 | .opacity(self.opacity)
87 | }
88 | case .gradient(let colors):
89 | ZStack {
90 | self.fill(colors.first!)
91 | self.fill(
92 | LinearGradient(
93 | gradient: Gradient(colors: colors),
94 | startPoint: .bottomLeading,
95 | endPoint: .topTrailing
96 | )
97 | )
98 | .opacity(self.opacity)
99 | }
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Tests/EasySkeletonTests/EasySkeletonTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import EasySkeleton
3 |
4 | final class EasySkeletonTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | // XCTAssertEqual(EasySkeleton().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------