Compare commits

...
171 Commits
Author SHA1 Message Date
Felix M. 87d8a1deda 📝 :: Fix typos 2026-02-24 21:35:38 +01:00
Felix M.andGitHub bd7828f8ee Merge pull request #35 from fxm90/feature/version-4
Version 4.0
2026-01-31 10:45:51 +01:00
Felix M. d8728a8fe8 🔖 :: Bump version 2026-01-31 10:41:03 +01:00
Felix M. 8528c2b71c 📝 :: Update documentation 2026-01-31 10:41:03 +01:00
Felix M. 7baa3b116c 🍱 :: Update assets 2026-01-31 10:41:02 +01:00
Felix M. 1cab02e3d3 :: Add GitHub action to run formatting, linting and tests 2026-01-31 10:41:02 +01:00
Felix M. 81a830c36f :: Add example application 2026-01-31 10:41:02 +01:00
Felix M. 27392ece2a ♻️ :: Rewrite library using modern Swift features, following SPM conventions 2026-01-31 10:41:00 +01:00
Felix Mau 4455b1125e ⬆️ :: Use updated codecov version 2024-09-06 20:43:56 +02:00
Felix MandGitHub dd1323e31c Merge pull request #34 from woxtu/settings
Update the example build settings
2023-04-23 20:52:30 +02:00
woxtu ae3a7fa25e Update the build settings 2023-04-21 02:33:03 +09:00
Felix Mau 85cd3fe00d 🐛 :: Fix missing UIKit import 2022-10-07 15:38:39 +02:00
Felix Mau 8ad9d7051a 🔖 :: Bump version 2022-10-07 15:33:23 +02:00
Felix Mau 27dfd49656 ⬆️ :: Update dev dependencies 2022-10-07 15:30:23 +02:00
Felix Mau 709980d6a7 ♻️ :: Use latest iOS and iPhone for running snapshot tests 2022-10-07 15:18:45 +02:00
Felix Mau cd941efb52 ♻️ :: Unify Swift versions 2022-10-07 15:18:23 +02:00
Felix MandGitHub ebc76148ce Merge pull request #33 from fxm90/feature/restructure-tests
Restructure tests
2022-10-07 15:10:22 +02:00
Felix MandGitHub 730a4cbe42 Merge pull request #32 from fxm90/feature/prefer-combine-instead-of-lightweight-observable-dependency
Prefer Combine instead of Dependency `LightweightObservable`
2022-10-07 15:09:06 +02:00
Felix Mau 3d4a8ec568 ♻️ :: Use Xcode 14 on CI
The snapshot tests were also generated using this Xcode version
2022-09-19 21:08:02 +02:00
Felix Mau 231330730a :: Update snapshot tests for iOS 16 2022-09-19 13:46:21 +02:00
Felix Mau b6593371cd ♻️ :: Refactor CI test setup 2022-09-19 13:20:31 +02:00
Felix Mau 8757179241 ♻️ :: Restructure tests 2022-09-16 14:35:38 +02:00
Felix Mau 55372e4bf5 🚚 :: Delete obsolete files 2022-09-16 14:35:24 +02:00
Felix Mau 64ec43e8f8 📝 :: Update docs 2022-09-16 14:34:12 +02:00
Felix Mau a878ba4ff2 🎨 :: Format code 2022-09-16 14:34:03 +02:00
Felix Mau df453eeb5a 🐛 :: Explicitly set deployment target for GradientLoadingBar in Pods project, as this will be read by Carthage 2022-09-13 19:49:15 +02:00
Felix Mau 0cfdc7d269 :: Adapt tests 2022-09-13 19:35:53 +02:00
Felix Mau 8ca35493fc ♻️ :: Use Combine instead of Lightweight Observable 2022-09-13 19:35:53 +02:00
Felix Mau 08ac0a5224 ♻️ :: Remove dependency LightweightObservable and increase min version to iOS 13
This is required to use Combine intead
2022-09-13 19:35:47 +02:00
Felix Mau 385d133dfc 🔖 :: Bump version 2022-09-12 21:05:54 +02:00
Felix Mau 901fadaffb ⬆️ :: Update dev-dependencies 2022-09-12 20:53:10 +02:00
Felix Mau 097b26b5ad 🐛 :: Explicitly set deployment target for GradientLoadingBar in Pods project, as this will be read by Carthage 2022-09-12 20:38:48 +02:00
Felix Mau 066064280b 🐛 :: Remove shared scheme for Example
https://www.amerhukic.com/how-to-add-carthage-support-in-existing-cocoapod-project
2022-09-12 20:37:17 +02:00
Felix Mau c6a9fdb124 🔖 :: Bump version 2022-09-11 21:40:50 +02:00
Felix Mau 8090a3502a 🐛 :: Add missing import statement 2022-09-11 21:22:57 +02:00
Felix Mau 9e7d80e3d2 ♻️ :: Update snapshot tests to iOS 15.5 2022-09-09 14:47:53 +02:00
Felix Mau c6ed57a24e 💚 :: Increase timeout for CI 2022-09-09 14:29:15 +02:00
Felix Mau 999a3497de Merge branch 'main' into task/dependency-update 2022-09-09 13:51:49 +02:00
Felix Mau 4b2987dc79 :: Adapt tests 2022-09-09 13:51:39 +02:00
Felix Mau b209ecae78 Merge branch 'main' into task/dependency-update 2022-09-09 13:42:23 +02:00
Felix Mau e58c15c381 ♻️ :: Rename SafeAreaDevice to NotchDevice as the iPhone 14 Pro (Max) has a safe area without a notch 2022-09-09 13:42:12 +02:00
Felix Mau 345a874a04 ✏️ :: Fix typo 2022-09-08 22:29:58 +02:00
Felix Mau 47166ac314 🔖 :: Bump version 2022-09-08 19:41:10 +02:00
Felix Mau 707793be13 :: Adapt tests 2022-09-08 19:25:17 +02:00
Felix Mau 2b19e431ec ♻️ :: Prefer nil over .unknown enum case 2022-09-08 19:24:13 +02:00
Felix Mau 4521bc8622 :: Adapt tests 2022-09-08 19:23:42 +02:00
Felix Mau 77b7a5929e :: Add support for iOS 14 2022-09-08 19:23:25 +02:00
Felix Mau 3c145963e9 Merge branch 'main' into task/dependency-update 2022-09-08 12:25:34 +02:00
Felix Mau fcb33e68f7 🔖 :: Bump version 2022-09-08 12:23:55 +02:00
Felix Mau e82081460a 🐛 :: Fixed incorrect layout of NotchGradientLoadingBar when using "iPhone 12 Mini" or "iPhone 13 Mini" 2022-09-08 11:39:34 +02:00
Felix Mau 4de6fd74ec ⬆️ :: Update dev-dependencies 2022-09-07 18:18:09 +02:00
Felix Mau 49cf8c9ba2 ♻️ :: Use Mac OS 12, required by SwiftLint 0.49
> SwiftLint now requires Swift 5.6 or higher to build, and macOS 12 or higher to run.

https://github.com/realm/SwiftLint/blob/main/CHANGELOG.md#0490-asynchronous-defuzzer
2022-09-07 18:16:38 +02:00
Felix MandGitHub 5aa2f5d5ba Enhance BezierPath of NotchGradientLoadingBar (#31)
* ♻️ :: Enhance bezierpath

* ⬆️ :: Update fastlane plugin

* ⬆️ :: Update dev-dependencies

*  :: Adapt snapshot tests

* Revert "⬆️ :: Update dev-dependencies"

This reverts commit 45328c27ea.
2022-09-07 18:15:15 +02:00
Felix MandGitHub 9779449ab4 Merge pull request #30 from fxm90/feature/adapt-uikit-animation-to-swiftui-variant
Adapt UIKit animation implementation to SwiftUI variant
2022-09-07 17:50:46 +02:00
Felix Mau 60ccf4b1d2 🎨 :: Format code 2022-09-07 16:53:59 +02:00
Felix Mau ee6c985326 📝 :: Update documentation 2022-09-07 16:53:45 +02:00
Felix Mau f9b72b1621 ⬆️ :: Update dependencies 2022-08-22 20:20:35 +02:00
Felix Mau 84e1d0f2a4 📝 :: Update documentation 2022-08-22 20:19:18 +02:00
Felix Mau 85fa8e3851 :: Add test for GradientActivityIndicatorViewModel.SizeUpdate 2022-08-18 22:05:19 +02:00
Felix Mau 0ec26dfee9 🚚 :: Restructure test cases
They are now in folders with the same name as the features
2022-08-18 21:56:44 +02:00
Felix Mau d979f65b80 ♻️ :: Extract logic into SizeUpdate struct 2022-08-18 19:01:46 +02:00
Felix Mau 01c7df73f0 ♻️ :: Prefer property over method call 2022-08-18 18:56:06 +02:00
Felix Mau a7c82024d0 ♻️ :: Prefer private extension 2022-08-18 18:55:35 +02:00
Felix Mau 0b6424f4a8 📝 :: Update documentation 2022-08-18 18:55:21 +02:00
Felix Mau bfcdca3efb Revert "♻️ :: Reduce view updates by subscribing distinct"
This reverts commit fa7e30c44f.
2022-08-18 18:54:52 +02:00
Felix Mau 3c82b1464c ♻️ :: Fix snapshot tests 2022-08-17 22:50:03 +02:00
Felix Mau ea234f73bb ♻️ :: Fix snapshot tests 2022-08-15 22:18:55 +02:00
Felix Mau 0bfd932196 ♻️ :: Adapt tests 2022-08-15 21:33:58 +02:00
Felix Mau fa7e30c44f ♻️ :: Reduce view updates by subscribing distinct 2022-08-15 21:29:54 +02:00
Felix Mau 38115a2827 ♻️ :: Refactor to non stored property, to avoid duplicate data 2022-08-15 21:29:33 +02:00
Felix Mau 596692c2d4 Merge branch 'main' into feature/adapt-uikit-animation-to-swiftui-variant
# Conflicts:
#	GradientLoadingBar/Feature/GradientActivityIndicatorView/GradientActivityIndicatorViewModel.swift
2022-08-14 18:29:09 +02:00
Felix Mau 68c39399c4 🎨 :: Format code 2022-08-14 18:25:02 +02:00
Felix Mau 8a85860ba1 ⬆️ :: Update dev-dependencies 2022-08-14 18:24:14 +02:00
Felix Mau 24b380bf50 Merge branch 'main' into feature/adapt-uikit-animation-to-swiftui-variant
# Conflicts:
#	Example/Podfile.lock
#	Example/Pods/Manifest.lock
#	Example/Pods/SwiftFormat/CommandLineTool/swiftformat
2022-07-18 20:10:47 +02:00
Felix Mau c4294823e7 ⬆️ :: Update dev-dependencies 2022-07-18 20:09:54 +02:00
Felix Mau c319b42a49 ⬆️ :: Update dependencies 2022-05-28 12:02:47 +02:00
Felix Mau 2addd9fa7d ♻️ :: Enhance test cases 2022-05-20 15:40:16 +02:00
Felix Mau 8fb9963150 ♻️ :: Fix typo in test method 2022-05-20 15:34:57 +02:00
Felix Mau 68a53add0c 📝 :: Update documentation 2022-05-20 15:25:35 +02:00
Felix Mau b786421871 ♻️ :: Adapt constants 2022-05-20 15:03:37 +02:00
Felix Mau 4e52985153 ♻️ :: Adapt documentation 2022-05-20 14:57:30 +02:00
Felix Mau ae24582345 Merge branch 'main' into feature/adapt-uikit-animation-to-swiftui-variant 2022-05-12 21:49:55 +02:00
Felix Mau 61d787e1d6 🔖 :: Bump version 2022-05-10 18:39:25 +02:00
Felix Mau 5c45b2e9f5 :: Adapt tests 2022-05-10 18:35:56 +02:00
Felix Mau ff7c892079 🐛 :: Fix size of NotchGradientLoadingBar for "iPhone 11 Pro" and "iPhone 11 Pro Max"
fixes #29
2022-05-10 18:34:32 +02:00
Felix Mau 5f48a38dca Merge branch 'main' into feature/adapt-uikit-animation-to-swiftui-variant 2022-05-06 14:59:54 +01:00
Felix Mau 104d4c8f60 🔖 :: Bump version 2022-05-06 14:58:40 +01:00
Felix Mau 0e0b31ffbe 🐛 :: Fix incorrect path 2022-05-06 14:40:17 +01:00
Felix Mau 244e1eba94 ♻️ :: Add shared scheme GradientLoadingBar 2022-05-02 11:25:49 +01:00
Felix Mau 4e5aaae570 🐛 :: Fix invalid version 2022-05-02 11:25:30 +01:00
Felix Mau dfc9049be2 Merge branch 'feature/add-support-for-swiftui' into feature/adapt-uikit-animation-to-swiftui-variant 2022-04-29 18:49:04 +01:00
Felix Mau c0f27a3e48 ♻️ :: Remove outdated code 2022-04-29 18:43:44 +01:00
Felix Mau 54a413d986 🐛 :: Fix xcodebuild errors when running pod lib lint 2022-04-29 18:40:10 +01:00
Felix Mau 3d24237307 ♻️ :: Reduce precision to fix flaky tests on CI 2022-04-29 18:24:09 +01:00
Felix Mau 7c02e472d7 ♻️ :: Reduce precision to fix flaky tests on CI 2022-04-29 18:16:51 +01:00
Felix Mau 00275d6778 Merge branch 'feature/add-support-for-swiftui' into feature/adapt-uikit-animation-to-swiftui-variant 2022-04-29 18:07:42 +01:00
Felix Mau 1b41081034 🎨 :: Format code 2022-04-29 18:06:32 +01:00
Felix Mau f6a959a4b5 📝 :: Update documentation 2022-04-29 18:05:09 +01:00
Felix Mau ffdc50eada 🎨 :: Format code 2022-04-29 18:05:00 +01:00
Felix Mau 2ae619db5a Merge branch 'main' into feature/add-support-for-swiftui
# Conflicts:
#	Example/Gemfile.lock
2022-04-29 17:51:05 +01:00
Felix Mau 587e6d0095 ⬆️ :: Update dev-dependencies 2022-04-28 08:58:19 +01:00
Felix Mau 9012d6ec6a 🐛 :: Fix invalid comparison url 2022-04-28 08:55:58 +01:00
Felix Mau 8730c614de Merge branch 'feature/add-support-for-swiftui' into feature/adapt-uikit-animation-to-swiftui-variant
# Conflicts:
#	Example/ExampleTests/ViewModel/GradientActivityIndicatorViewModelTestCase.swift
#	GradientLoadingBar/Feature/GradientActivityIndicatorView/GradientActivityIndicatorViewModel.swift
2022-04-28 08:50:51 +01:00
Felix Mau f7486690ac :: Update snapshot tests 2022-04-28 08:49:33 +01:00
Felix Mau 846005af52 ♻️ :: Update // MARK: - statements 2022-04-28 08:49:17 +01:00
Felix Mau cd14d3cfee ⬆️ :: Update dev-dependencies 2022-04-28 08:48:18 +01:00
Felix Mau 9ae07a9601 ♻️ :: Simplified tests 2022-04-27 18:57:29 +02:00
Felix Mau f0e4a7ec26 🐛 :: Update incorrect navigation title 2022-04-27 18:50:46 +02:00
Felix Mau 3d8e5c32fc :: Update snapshot tests 2022-04-27 18:49:13 +02:00
Felix Mau 240150d8c8 🎨 :: Format code 2022-04-27 18:48:31 +02:00
Felix Mau 6a80af2cd7 Merge branch 'feature/add-support-for-swiftui' into feature/adapt-uikit-animation-to-swiftui-variant
# Conflicts:
#	Example/ExampleTests/ViewModel/GradientActivityIndicatorViewModelTestCase.swift
#	GradientLoadingBar/Feature/GradientActivityIndicatorView/GradientActivityIndicatorViewModel.swift
2022-04-27 18:42:50 +02:00
Felix Mau b981df3519 ♻️ :: Update example application 2022-04-15 18:40:44 +02:00
Felix Mau 6c9c0ab1e1 📝 :: Add documentation 2022-04-12 21:39:57 +02:00
Felix Mau 563e8c6ecf Merge branch 'main' into feature/adapt-uikit-animation-to-swiftui-variant 2022-03-27 10:10:37 +02:00
Felix Mau 6980c0e3c3 ♻️ :: Remove outdated code 2022-03-27 10:10:20 +02:00
Felix Mau 94106389e2 Merge branch 'main' into feature/add-support-for-swiftui
# Conflicts:
#	Example/Podfile.lock
#	Example/Pods/Manifest.lock
#	GradientLoadingBar/Feature/NotchGradientLoadingBar/NotchGradientLoadingBarController.swift
2022-03-27 10:08:28 +02:00
Felix Mau b2279955a2 🔖 :: Bump version 2022-03-27 10:03:10 +02:00
Felix Mau 0b99b7853b 🐛 :: Fix invalid masking on iPhone 11 2022-03-27 09:47:08 +02:00
Felix Mau bc6575e9db ♻️ :: Improve safe area device check 2022-03-27 09:46:35 +02:00
Felix Mau 7b586da85b :: Adapt tests 2022-03-23 21:07:46 +01:00
Felix Mau 66467cf4d9 ♻️ :: Adapt UIKit variant to SwiftUI variant, by animating the gradient layer frame 2022-03-23 21:07:35 +01:00
Felix Mau 8e7650dd46 🚚 :: Use feature based folder structure 2022-03-23 09:57:13 +01:00
Felix Mau 8b257545ee ♻️ :: Rename property 2022-03-23 09:32:35 +01:00
Felix Mau 285d8ef2d4 :: Adapt snapshot tests 2022-03-23 09:32:19 +01:00
Felix Mau 090a59959f :: Add tests for new view-model 2022-03-22 20:07:27 +01:00
Felix Mau 67f356caf0 ♻️ :: Extract view logic into view model 2022-03-22 16:32:29 +01:00
Felix Mau 85a61b4433 🐛 :: Fix glitch in animation due to incorrect gradient size 2022-03-21 22:03:16 +01:00
Felix Mau f44ab07301 🚚 :: Rename tests 2022-03-20 08:11:23 +01:00
Felix Mau ae1ff09572 📝 :: Update documentation 2022-03-20 08:04:54 +01:00
Felix Mau e654161db3 🎨 :: Format code 2022-03-20 08:04:43 +01:00
Felix Mau df93f889b5 ♻️ :: Move numbers to configuration values 2022-03-20 08:04:14 +01:00
Felix Mau 63f66740c9 💚 :: Fix CI build 2022-03-16 20:34:22 +01:00
Felix Mau 80e113d194 ⬆️ :: Update gems 2022-03-16 20:30:06 +01:00
Felix Mau 91fdbbfeb6 ⬆️ :: Run pod install to resolve merge conflicts correctly 2022-03-16 17:45:31 +01:00
Felix Mau 555b5309a9 Merge branch 'main' into feature/add-support-for-swiftui
# Conflicts:
#	Example/ExampleTests/ViewModel/NotchGradientLoadingBarViewModelTestCase.swift
#	Example/Podfile.lock
#	Example/Pods/Manifest.lock
#	GradientLoadingBar/NotchGradientLoadingBarController.swift
2022-03-16 17:42:48 +01:00
Felix Mau cafd394cb7 🔖 :: Bump version 2022-03-16 17:30:49 +01:00
Felix Mau e558c54cb7 📝 :: Update documentation 2022-03-16 17:24:27 +01:00
Felix Mau 9382248471 :: Fix tests 2022-03-11 15:25:20 +01:00
Felix Mau 44307a6b7a 📝 :: Update documentation 2022-03-11 15:24:04 +01:00
Felix Mau f54023a6b9 🎨 :: Format code 2022-03-11 15:19:48 +01:00
Felix Mau 67f6ab75cc :: Add shortcut to check whether the loading bar is currently visible or not 2022-03-11 15:18:04 +01:00
Felix Mau 1f00f1d61f 📝 :: Update documentation 2022-03-11 15:05:51 +01:00
Felix Mau 41c084b27d 📝 :: Update documentation 2022-03-11 14:45:35 +01:00
Felix Mau e8d03a9ccc ♻️ :: Adapt configuration for iPhone XR 2022-03-11 14:33:45 +01:00
Felix Mau ecad3b98b9 ♻️ :: Allow offsetting the large circle in x and y direction 2022-03-11 14:08:56 +01:00
Felix Mau d97b99ab83 :: Add notch configuration for iPhone XR (WIP) 2022-03-11 14:04:12 +01:00
Felix Mau 0e20b0fb0e ♻️ :: Extract config for notch into separate struct
This struct is available per device as an extension on `NotchGradientLoadingBarViewModel.SafeAreaDevice`
2022-03-11 13:56:28 +01:00
Felix Mau 3e1c18818a ♻️ :: Combine scenes in example application 2022-03-11 13:42:30 +01:00
Felix Mau 76cd8c3ddf ♻️ :: Increase button height in example application 2022-03-11 10:00:10 +01:00
Felix Mau 66eace181a ♻️ :: Unify // MARK: - statements in tests 2022-03-10 14:09:33 +01:00
Felix Mau bbe2ddfe2f :: Fix flaky snapshot test 2022-03-10 14:08:39 +01:00
Felix Mau 0783d958a2 🚨 :: Fix warning 2022-03-10 14:08:18 +01:00
Felix Mau 07f2733ef8 ♻️ :: Use property drawHierarchyInKeyWindow instead of own custom helper 2022-03-10 14:08:02 +01:00
Felix Mau 03b9fac539 🚨 :: Fix keyWindow deprecation warning 2022-03-10 13:58:36 +01:00
Felix Mau be40ac8c35 :: Add snapshot tests 2022-03-10 12:01:35 +01:00
Felix Mau dec430d951 :: Add unit tests 2022-03-10 10:59:37 +01:00
Felix Mau 25de79f73d ♻️ :: Move custom colors to config value 2022-03-09 21:48:21 +01:00
Felix Mau 9ca33c4432 :: Adapt example application to show SwiftUI variant 2022-03-09 21:48:03 +01:00
Felix Mau f4b85195f5 :: Add SwiftUI view showing a gradient loading bar 2022-03-09 21:37:52 +01:00
Felix Mau 7309d0bd21 🚚 :: Rename file 2022-03-09 20:29:37 +01:00
Felix Mau d9c7840323 ♻️ :: Update example application 2022-03-07 21:53:13 +01:00
Felix Mau d484da4f83 ⬆️ :: Update dependencies 2022-03-07 19:41:14 +01:00
Felix Mau 4a17b218ee 🔖 :: Bump version 2021-12-29 14:36:58 +01:00
Felix Mau fc88310e3e ♻️ :: Comments & Cleanup 2021-12-29 14:16:16 +01:00
Felix Mau 1cd1d0c8ee 🚨 :: Fix warning 2021-12-29 13:19:41 +01:00
Felix Mau a6b56d9b32 🐛 :: Fix not running SwiftLint on CI 2021-12-29 13:17:56 +01:00
Felix Mau e9da3c4118 ⬆️ :: Update dev-dependencies 2021-12-29 13:11:50 +01:00
4b1fe483aa Draw a shape for specified height instead of 1 line (#26)
Co-authored-by: Alin Farcas <alin.farcas@emag.ro>
2021-12-29 12:45:06 +01:00
Felix Mau edd0800a9c ♻️ :: Run snapshot tests with iOS 15 2021-12-03 11:40:29 +00:00
Felix Mau f48eb2439a ⬆️ :: Update dependencies 2021-12-03 11:37:16 +00:00
Felix Mau faa4f48d54 ⬆️ :: Update gems 2021-12-03 11:37:09 +00:00
270 changed files with 4429 additions and 15790 deletions
-13
View File
@@ -1,13 +0,0 @@
#
# codecov.yml
# Created by Felix Mau (https://felix.hamburg)
#
# How to:
# - https://github.com/codecov/example-swift
#
# Documentation:
# - https://docs.codecov.io/docs/
#
ignore:
- "Example" # ignore test coverage of example application
+43 -45
View File
@@ -2,63 +2,61 @@
# continuous-integration.yml
# Created by Felix Mau (https://felix.hamburg)
#
# Based on:
# - https://docs.github.com/en/actions/quickstart
# - https://futurestud.io/tutorials/github-actions-trigger-builds-on-schedule-cron#schedulegithubactionsusingacron
# - https://about.codecov.io/blog/code-coverage-for-ios-development-using-swift-xcode-and-github-actions/
#
name: Continuous Integration
on:
push:
on:
pull_request:
schedule:
# Run workflow every day at midnight.
#
# - Note: "Scheduled workflows run on the latest commit on the default or base branch."
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events
- cron: '0 0 * * *'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel.
push:
branches:
- main
workflow_dispatch:
jobs:
# This workflow starts with a job called "build-and-test".
build-and-test:
name: Build & Test
runs-on: macos-latest
# Steps represent a sequence of tasks that will be executed as part of the job.
# Lint and Format
# Ensures that the code adheres to the defined style guidelines.
lint-and-format:
runs-on: macos-26
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v2
uses: actions/checkout@v6
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: Install SwiftFormat via Homebrew
run: brew install swiftformat
- name: Install dependencies
working-directory: ./Example
run: bundle install
- name: Install SwiftLint via Homebrew
run: brew install swiftlint
- name: Execute SwiftFormat and treat any formatting errors as real errors.
working-directory: ./Example
run: bundle exec fastlane format
- name: SwiftFormat
run: make format-check
- name: Execute SwiftLint and treat any formatting errors as real errors.
working-directory: ./Example
run: bundle exec fastlane format
- name: SwiftLint
run: make lint
- name: Execute tests.
working-directory: ./Example
run: bundle exec fastlane tests
# Tests
# Ensures that the package compiles and that all tests pass, including code coverage reporting to Codecov.
tests:
runs-on: macos-26
needs: lint-and-format
steps:
- name: Check out repository code
uses: actions/checkout@v6
- name: Execute validation of library.
working-directory: ./Example
run: bundle exec fastlane pod_lint
- name: Execute tests
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.2.1
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
- run: echo "🍏 This job's status is ${{ job.status }}."
# Build Example Application
# Ensures that the package can be integrated into a real application, to catch any potential issues e.g. with access control.
build-example-application:
runs-on: macos-26
needs: tests
steps:
- name: Check out repository code
uses: actions/checkout@v6
- name: Build Example Application
run: make build-example-application
+6 -49
View File
@@ -1,51 +1,8 @@
# OS X
.DS_Store
# Xcode
build/
.build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
/.build
/Packages
xcuserdata/
*.xccheckout
profile
*.moved-aside
DerivedData
*.hmap
*.ipa
# Bundler
.bundle
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
#
# Note: if you ignore the Pods directory, make sure to uncomment
# `pod install` in .travis.yml
#
# Include `Pods` in repo in order to make symlink `Pods.xcodeproj` work (used for Carthage support).
# Pods/
#
# Fastlane specific
# Based on: https://docs.fastlane.tools/best-practices/source-control/
#
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output
**/fastlane/README.md
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
+1 -1
View File
@@ -1 +1 @@
5.1
6.2
+23
View File
@@ -0,0 +1,23 @@
#
# .swiftformat
# Configuration file for SwiftFormat (https://github.com/nicklockwood/SwiftFormat/)
#
# A more detailed documentation of the rules can be found at
# https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md
#
# Created by Felix Mau (https://felix.hamburg)
#
#
# Rules
#
# Allows an empty line before the first `// MARK: -` statement.
--disable blankLinesAtStartOfScope
#
# Rule Configuration
#
--indent 2
--import-grouping testable-last
--wraparguments before-first
+85
View File
@@ -0,0 +1,85 @@
#
# .swiftlint.yml
# Configuration file for SwiftLint (https://github.com/realm/SwiftLint/)
#
# A more detailed documentation of the rules can be found at
# https://realm.github.io/SwiftLint/rule-directory.html
#
# Created by Felix Mau (https://felix.hamburg)
#
included:
- ./
excluded:
- ".build/**/*"
- Package.swift
opt_in_rules:
- accessibility_label_for_image # Images that provide context should have an accessibility label or should be explicitly hidden from accessibility
- accessibility_trait_for_button # All views with tap gestures added should include the .isButton or the .isLink accessibility traits
- closure_spacing # Closure expressions should have a single space inside each brace.
- contains_over_filter_count # Prefer `contains` over comparing `filter(where:).count` to 0.
- contains_over_filter_is_empty # Prefer `contains` over using `filter(where:).isEmpty`.
- contains_over_first_not_nil # Prefer `contains` over `first(where:) != nil` and `firstIndex(where:) != nil`.
- contains_over_range_nil_comparison # Prefer `contains` over `range(of:) != nil` and `range(of:) == nil`.
- convenience_type # Types used for hosting only static members should be implemented as a caseless enum to avoid instantiation.
- duplicate_imports # Imports should be unique.
- empty_collection_literal # Prefer checking `isEmpty` over comparing collection to an empty array or dictionary literal.
- empty_count # Prefer checking `isEmpty` over comparing count to zero.
- empty_parameters # Prefer `() ->` over `Void ->`.
- empty_string # Prefer checking `isEmpty` over comparing string to an empty string literal.
- empty_xctest_method # Empty XCTest method should be avoided.
- explicit_init # Explicitly calling .init() should be avoided.
- fatal_error_message # A fatalError call should have a message.
- file_header # Header comments should be consistent with project patterns.
- first_where # Prefer using `.first(where:)` over `.filter { }.first` in collections.
- for_where # `where` clauses are preferred over a single `if` inside a `for`.
- force_unwrapping # Force unwrapping should be avoided.
- identical_operands # Comparing two identical operands is likely a mistake.
- implicit_return # Prefer implicit returns in closures, functions and getters.
- is_disjoint # Prefer using `Set.isDisjoint(with:)` over `Set.intersection(_:).isEmpty`.
- last_where # Prefer using `.last(where:)` over `.filter { }.last` in collections.
- legacy_multiple # Prefer using the `isMultiple(of:)` function instead of using the remainder operator (`%`).
- missing_docs # Declarations should be documented.
- modifier_order # Modifier order should be consistent.
- multiline_arguments # Arguments should be either on the same line, or one per line.
- multiline_parameters # Functions and methods parameters should be either on the same line, or one per line.
- operator_usage_whitespace # Operators should be surrounded by a single whitespace when they are being used.
- overridden_super_call # Some overridden methods should always call super
- prefer_self_type_over_type_of_self # Prefer `Self` over `type(of: self)` when accessing properties or calling methods.
- prefer_zero_over_explicit_init # Prefer `.zero` over explicit init with zero parameters (e.g. `CGPoint(x: 0, y: 0)`).
- prohibited_super_call # Some methods should not call super, e.g. `UIViewController.loadView()`.
- redundant_nil_coalescing # nil coalescing operator is only evaluated if the lhs is nil, coalescing operator with nil as rhs is redundant.
- toggle_bool # Prefer `someBool.toggle()` over `someBool = !someBool`.
- unavailable_function # Unimplemented functions should be marked as unavailable.
- unused_optional_binding # Prefer `!= nil` over `let _ =`
- void_return # Prefer `-> Void` over `-> ()`.
- weak_delegate # Delegates should be weak to avoid reference cycles
- yoda_condition # The constant literal should be placed on the right-hand side of the comparison operator.
file_header:
required_pattern: |
\/\/
\/\/ SWIFTLINT_CURRENT_FILENAME
\/\/ (GradientLoadingBar|GradientLoadingBarTests|GradientLoadingBarExample)
\/\/
\/\/ Created by .*? on \d{2}\.\d{2}\.\d{2}\.
\/\/ Copyright © \d{4} .*?\. All rights reserved\.
\/\/
line_length: 150
nesting:
type_level:
warning: 3
trailing_comma:
mandatory_comma: true
custom_rules:
comments_space:
name: "Space After Comment"
regex: '(^ *//\w+)'
message: "There should be a space after //."
severity: warning
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

+125
View File
@@ -0,0 +1,125 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1040" height="600" xml:space="preserve" id="cutout">
<!-- Generated by PaintCode - http://www.paintcodeapp.com -->
<defs>
<linearGradient id="cutout-gradient-bezier18" x1="207.78" y1="187.14" x2="912.47" y2="590.37" gradientUnits="userSpaceOnUse">
<stop offset="0.14" stop-color="rgb(0, 0, 0)" stop-opacity="0"></stop>
<stop offset="0.44" stop-color="rgb(64, 82, 91)" stop-opacity="0.05"></stop>
<stop offset="1" stop-color="rgb(129, 163, 182)" stop-opacity="0.09"></stop>
</linearGradient>
<linearGradient id="cutout-gradient2-rectangle" x1="559" y1="564" x2="559" y2="382" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="rgb(255, 255, 255)" stop-opacity="1"></stop>
<stop offset="1" stop-color="rgb(255, 255, 255)" stop-opacity="0"></stop>
</linearGradient>
</defs>
<path id="cutout-bezier26" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 928.5,563.5 L 928.5,259"></path>
<path id="cutout-bezier25" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 178.5,562.5 L 178.5,259"></path>
<path id="cutout-bezier18" stroke="none" fill="url(#cutout-gradient-bezier18)" d="M 929,561.48 L 929,306.52 C 929,267.79 929,248.42 922.41,227.57 914.12,204.81 896.19,186.88 873.42,178.6 855.96,172.83 833.21,172 794.48,172 L 781.34,172 C 776.06,172 773.42,172 770.58,172.9 767.47,174.03 765.03,176.47 763.9,179.58 763.11,181.96 763,185.06 763,190.34 L 763,192 C 763,214.09 745.09,232 723,232 L 385,232 C 362.91,232 345,214.09 345,192 L 345,190.34 C 345,185.06 345,182.42 344.1,179.58 342.97,176.47 340.53,174.03 337.42,172.9 335.04,172.11 331.94,172 326.66,172 L 313.52,172 C 274.79,172 267.5,172 267.5,172 218.9,172 179.5,211.4 179.5,260 179.5,260 179,267.79 179,306.52 L 179,561.48"></path>
<rect id="cutout-rectangle" stroke="none" fill="url(#cutout-gradient2-rectangle)" x="38" y="382" width="1042" height="182"></rect>
<circle id="cutout-oval3" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="none" cx="383.5" cy="192.5" r="40"></circle>
<path id="cutout-bezier2" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 266.5,467.5 L 266.5,267"></path>
<circle id="cutout-oval4" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="none" cx="0" cy="-0" r="12" transform="translate(331.5, 184.5)"></circle>
<circle id="cutout-oval" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="none" cx="266.5" cy="260.5" r="88"></circle>
<g id="cutout-group8">
<path id="cutout-bezier13" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-linecap="square" stroke-miterlimit="10" fill="none" d="M 178.5,58.5 L 928.5,58.5"></path>
<polygon id="cutout-polygon" stroke="none" fill="rgb(65, 153, 231)" points="0,-9 3.68,4.5 -3.68,4.5" transform="translate(920, 58.5) rotate(90)"></polygon>
<polygon id="cutout-polygon2" stroke="none" fill="rgb(65, 153, 231)" points="-0,-9 -3.68,4.5 3.68,4.5" transform="translate(187, 58.5) rotate(-90)"></polygon>
<text fill="rgb(65, 153, 231)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="18" x="526.52" y="16" text-anchor="middle"><tspan x="553.5" y="45">375 pt</tspan></text>
</g>
<g id="cutout-group9">
<path id="cutout-bezier14" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-linecap="square" stroke-miterlimit="10" fill="none" d="M 178.5,125.5 L 343.5,125.5"></path>
<polygon id="cutout-polygon3" stroke="none" fill="rgb(65, 153, 231)" points="0,-9 3.68,4.5 -3.68,4.5" transform="translate(335, 125.5) rotate(90)"></polygon>
<polygon id="cutout-polygon4" stroke="none" fill="rgb(65, 153, 231)" points="-0,-9 -3.68,4.5 3.68,4.5" transform="translate(187, 125.5) rotate(-90)"></polygon>
<text fill="rgb(65, 153, 231)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="18" x="239.37" y="83" text-anchor="middle"><tspan x="261" y="112">83 pt</tspan></text>
</g>
<g id="cutout-group10">
<path id="cutout-bezier32" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-linecap="square" stroke-miterlimit="10" fill="none" d="M 762.5,125.5 L 928.5,125.5"></path>
<polygon id="cutout-polygon5" stroke="none" fill="rgb(65, 153, 231)" points="0,-9 3.68,4.5 -3.68,4.5" transform="translate(920, 125.5) rotate(90)"></polygon>
<polygon id="cutout-polygon6" stroke="none" fill="rgb(65, 153, 231)" points="-0,-9 -3.68,4.5 3.68,4.5" transform="translate(771, 125.5) rotate(-90)"></polygon>
<text fill="rgb(65, 153, 231)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="18" x="823.87" y="83" text-anchor="middle"><tspan x="845.5" y="112">83 pt</tspan></text>
</g>
<g id="cutout-group11">
<path id="cutout-bezier33" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-linecap="square" stroke-miterlimit="10" fill="none" d="M 343.5,125.5 L 762.5,125.5"></path>
<polygon id="cutout-polygon7" stroke="none" fill="rgb(65, 153, 231)" points="0,-9 3.68,4.5 -3.68,4.5" transform="translate(754, 125.5) rotate(90)"></polygon>
<polygon id="cutout-polygon8" stroke="none" fill="rgb(65, 153, 231)" points="-0,-9 -3.68,4.5 3.68,4.5" transform="translate(352, 125.5) rotate(-90)"></polygon>
<text fill="rgb(65, 153, 231)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="18" x="526.02" y="83" text-anchor="middle"><tspan x="553" y="112">209 pt</tspan></text>
</g>
<g id="cutout-group12">
<path id="cutout-bezier34" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-linecap="square" stroke-miterlimit="10" fill="none" d="M 94.5,221.5 L 94.5,186.5"></path>
<polygon id="cutout-polygon9" stroke="none" fill="rgb(65, 153, 231)" points="0,-9 3.68,4.5 -3.68,4.5" transform="translate(94.5, 223.5) rotate(180)"></polygon>
<polygon id="cutout-polygon10" stroke="none" fill="rgb(65, 153, 231)" points="-0,-9 -3.68,4.5 3.68,4.5" transform="translate(94.5, 182.5)"></polygon>
<text fill="rgb(65, 153, 231)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="18" x="43.75" y="188" text-anchor="end"><tspan x="87" y="208">30 pt</tspan></text>
</g>
<path id="cutout-bezier5" opacity="0.3" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 82,232.5 L 717.5,232.5"></path>
<path id="cutout-bezier7" opacity="0.3" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 343.5,192 L 343.5,108"></path>
<path id="cutout-bezier4" opacity="0.3" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 82,172.5 L 840.5,172.5"></path>
<path id="cutout-bezier8" opacity="0.3" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 762.5,192 L 762.5,108"></path>
<path id="cutout-bezier9" opacity="0.3" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 928.5,373 L 928.5,38"></path>
<path id="cutout-bezier10" opacity="0.3" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 178.5,259.5 L 178.5,39"></path>
<circle id="cutout-oval2" opacity="0.2" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="none" cx="840.5" cy="260.5" r="88"></circle>
<circle id="cutout-oval5" opacity="0.2" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="none" cx="722.5" cy="192.5" r="40"></circle>
<circle id="cutout-oval6" opacity="0.2" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="none" cx="0" cy="-0" r="12" transform="translate(774.5, 184.5)"></circle>
<circle id="cutout-oval10" stroke="none" fill="rgb(65, 153, 231)" cx="266.5" cy="470.5" r="3.5"></circle>
<g id="cutout-group">
<path id="cutout-bezier11" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 331.5,389.5 L 331.5,191"></path>
<circle id="cutout-oval12" stroke="none" fill="rgb(65, 153, 231)" cx="331.5" cy="389.5" r="3.5"></circle>
</g>
<path id="cutout-bezier17" stroke="rgb(65, 153, 231)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 383.5,324.5 L 383.5,202"></path>
<circle id="cutout-oval13" stroke="none" fill="rgb(65, 153, 231)" cx="383.5" cy="325.5" r="3.5"></circle>
<g id="cutout-group2">
<path id="cutout-bezier15" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 383.5,202 L 383.5,189"></path>
<path id="cutout-bezier16" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 377,195.5 L 390,195.5"></path>
<circle id="cutout-oval8" stroke="none" fill="rgb(0, 0, 0)" cx="383.5" cy="195.5" r="3.5"></circle>
</g>
<g id="cutout-group3" opacity="0.3">
<circle id="cutout-oval9" stroke="none" fill="rgb(0, 0, 0)" cx="721.5" cy="193.5" r="3.5"></circle>
<path id="cutout-bezier19" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 721.5,200 L 721.5,187"></path>
<path id="cutout-bezier20" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 715,193.5 L 728,193.5"></path>
</g>
<g id="cutout-group4" opacity="0.2">
<circle id="cutout-oval14" stroke="none" fill="rgb(0, 0, 0)" cx="839.5" cy="260.5" r="3.5"></circle>
<path id="cutout-bezier21" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 839.5,267 L 839.5,254"></path>
<path id="cutout-bezier22" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 833,260.5 L 846,260.5"></path>
</g>
<g id="cutout-group5" opacity="0.2">
<circle id="cutout-oval15" stroke="none" fill="rgb(0, 0, 0)" cx="774.5" cy="184.5" r="3.5"></circle>
<path id="cutout-bezier23" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 774.5,191 L 774.5,178"></path>
<path id="cutout-bezier24" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 768,184.5 L 781,184.5"></path>
</g>
<path id="cutout-bezier27" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 723,232.5 L 383,232.5"></path>
<path id="cutout-bezier28" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 332,172.5 L 265,172.5"></path>
<path id="cutout-bezier29" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 841.5,172.5 L 774,172.5"></path>
<text fill="rgb(65, 153, 231)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="18" x="281" y="454"><tspan x="281" y="476">40 pt radius</tspan></text>
<text fill="rgb(65, 153, 231)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="18" x="346" y="373"><tspan x="346" y="395">6 pt radius</tspan></text>
<text fill="rgb(65, 153, 231)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="18" x="403" y="309"><tspan x="403" y="331">20 pt radius</tspan></text>
<path id="cutout-oval16" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="none" d="M 383.5,232.5 C 361.41,232.5 343.5,214.59 343.5,192.5"></path>
<path id="cutout-oval17" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="none" d="M 178.5,260.5 C 178.5,211.9 217.9,172.5 266.5,172.5"></path>
<path id="cutout-oval18" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="none" d="M 331.5,172.5 C 338.13,172.5 343.5,177.87 343.5,184.5"></path>
<path id="cutout-bezier30" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 343.5,184.5 L 343.5,194.5"></path>
<path id="cutout-oval19" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="none" d="M 774.5,172.5 C 767.87,172.5 762.5,177.87 762.5,184.5"></path>
<path id="cutout-oval20" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="none" d="M 722.5,232.5 C 744.59,232.5 762.5,214.59 762.5,192.5"></path>
<path id="cutout-bezier31" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 762.5,184 L 762.5,194.5"></path>
<path id="cutout-oval21" stroke="rgb(0, 0, 0)" stroke-width="3" stroke-miterlimit="10" fill="none" d="M 928.5,260.5 C 928.5,211.9 889.1,172.5 840.5,172.5"></path>
<g id="cutout-group6">
<path id="cutout-bezier" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 266.5,267 L 266.5,254"></path>
<path id="cutout-bezier12" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 260,260.5 L 273,260.5"></path>
<circle id="cutout-oval7" stroke="none" fill="rgb(0, 0, 0)" cx="266.5" cy="260.5" r="3.5"></circle>
</g>
<g id="cutout-group7">
<path id="cutout-bezier3" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 331.5,191 L 331.5,178"></path>
<path id="cutout-bezier6" stroke="rgb(0, 0, 0)" stroke-width="1" stroke-miterlimit="10" fill="rgb(255, 255, 255)" d="M 325,184.5 L 338,184.5"></path>
<circle id="cutout-oval11" stroke="none" fill="rgb(0, 0, 0)" cx="331.5" cy="184.5" r="3.5"></circle>
</g>
<!-- Manually added copyright -->
<text fill="rgb(0, 0, 0)" font-family="AvenirNext-Medium, 'Avenir Next', sans-serif" font-size="15" x="560" y="560" text-anchor="middle">
Source: https://www.paintcodeapp.com/news/iphone-x-screen-demystified
</text>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

+199 -44
View File
@@ -1,4 +1,5 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
@@ -7,153 +8,307 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [Unreleased]
## [2.2.2] - 2021-05-10
## [4.0.0] - 2026-01-31
### Changed
- Complete rewrite using modern Swift features including the [`Observation`](https://developer.apple.com/documentation/Observation) framework.
Therefore the minimum required iOS version increased to iOS `26.0` and Swift `6.2`.
- Reorganized project structure to follow Swift Package Manager conventions (`Sources/` and `Tests/` directories).
- Rewrote example application using SwiftUI with improved feature demonstrations.
- Updated README with comprehensive documentation and new screenshots.
### Removed
- Dropped support for CocoaPods. This follows CocoaPods' [read-only trunk notice](https://blog.cocoapods.org/CocoaPods-Specs-Repo/). Please migrate to Swift Package Manager.
- Dropped support for Carthage. Please migrate to Swift Package Manager.
- Removed legacy example project and Podfile infrastructure.
## [3.0.0] - 2022-07-10
### Changed
- Use `Combine` instead of dependency `LightweightObservable`. Therefore the minimum required iOS version increased to iOS `13.0`. Otherwise there are no breaking changes.
## [2.3.5] - 2022-12-09
### Fixed
- Fixed missing `return` ([#025])
- Fixed wrong minimum iOS deployment target when using Carthage.
## [2.3.4] - 2022-11-09
### Fixed
- Added missing `UIKit` import when using Swift Package Manager.
## [2.3.3] - 2022-08-09
### Changed
- Added specific Bézier Path for `NotchGradientLoadingBar` for "iPhone 14" and "iPhone 14 Plus"
## [2.3.2] - 2022-08-09
### Changed
- Improved Bézier Path of `NotchGradientLoadingBar`
- Adapt logic of `UIKit` variant for calculating and animating the gradient to `SwiftUI` variant. This should not make any difference visually, but will be easier to maintain.
### Fixed
- Fixed incorrect layout on `NotchGradientLoadingBar` when using "iPhone 12 Mini" or "iPhone 13 Mini"
## [2.3.1] - 2022-10-05
### Fixed
- Fixed incorrect layout on `NotchGradientLoadingBar` when using "iPhone 11 Pro" or "iPhone 11 Pro Max" ([#029])
[#029]: https://github.com/fxm90/GradientLoadingBar/issues/29
## [2.3.0] - 2022-06-05
### Added
- Add `GradientLoadingBarView` to support SwiftUI
## [2.2.5] - 2022-27-03
### Fixed
- Fixed incorrect layout on `NotchGradientLoadingBar` when using "iPhone 11"
## [2.2.4] - 2022-16-03
### Fixed
- Fixed incorrect layout on `NotchGradientLoadingBar` when using "iPhone XR"
## [2.2.3] - 2021-29-12
### Fixed
- Fixed incorrect layout on `NotchGradientLoadingBar` when using an increased height ([#026], thanks to [alinfarcas12](https://github.com/alinfarcas12))
[#026]: https://github.com/fxm90/GradientLoadingBar/issues/26
## [2.2.2] - 2021-05-10
### Fixed
- Fixed missing `return` ([#025])
[#025]: https://github.com/fxm90/GradientLoadingBar/issues/25
## [2.2.1] - 2021-05-10
### Fixed
- Fixed invalid import ([#024])
- Fixed invalid import ([#024])
[#024]: https://github.com/fxm90/GradientLoadingBar/issues/24
## [2.2.0] - 2021-04-10
### Fixed
- Added support for iPhone 13 ([#023])
- Added support for iPhone 13 ([#023])
[#023]: https://github.com/fxm90/GradientLoadingBar/issues/23
## [2.1.1] - 2021-12-05
### Fixed
- Showing loading bar with a masked notch on iPad Pro 12.9 ([#020])
- Showing loading bar with a masked notch on iPad Pro 12.9 ([#020])
[#020]: https://github.com/fxm90/GradientLoadingBar/issues/20
## [2.1.0] - 2020-17-06
### Added
- Added `NotchGradientLoadingBar`: A subclass of `GradientLoadingBar`, wrapping the `GradientActivityIndicatorView` around the notch of the iPhone.
- Added `NotchGradientLoadingBar`: A subclass of `GradientLoadingBar`, wrapping the `GradientActivityIndicatorView` around the notch of the iPhone.
## [2.0.3] - 2020-18-01
### Added
- Added support for Swift Package Manager.
- Added support for Swift Package Manager.
## [2.0.2] - 2019-20-12
### Changed
- Use `windows.first(where: { $0.isKeyWindow })` instead of [`keyWindow`](https://developer.apple.com/documentation/uikit/uiapplication/1622924-keywindow) as this is deprecated in iOS 13
- Use `windows.first(where: { $0.isKeyWindow })` instead of [`keyWindow`](https://developer.apple.com/documentation/uikit/uiapplication/1622924-keywindow) as this is deprecated in iOS 13
## [2.0.1] - 2019-12-10
### Changed
- Refactorings (Optimized progress animation)
- Updated example application for iOS 13
- Refactorings (Optimized progress animation)
- Updated example application for iOS 13
## [2.0.0] - 2019-02-09
### Changed
- Splitted framework into two classes:
- **GradientLoadingBar**: A controller, managing the visibility of the `GradientActivityIndicatorView` on the current key window.
- **GradientActivityIndicatorView**: A `UIView` containing the gradient with the animation, with support for Interface Builder.
- Split framework into two classes:
- **GradientLoadingBar**: A controller, managing the visibility of the `GradientActivityIndicatorView` on the current key window.
- **GradientActivityIndicatorView**: A `UIView` containing the gradient with the animation, with support for Interface Builder.
## [1.1.17] - 2019-30-06
### Changed
- Extract observable implementation into a framework (`LightweightObservable`) and added it as dependency.
- Extract observable implementation into a framework (`LightweightObservable`) and added it as dependency.
## [1.1.16] - 2019-08-04
### Changed
- Changed access controll level for extensions to `internal` to avoid conflicts
- Changed access control level for extensions to `internal` to avoid conflicts
- Added by [Emil Bellmann](https://github.com/emilrb) in Pull Request [#13](https://github.com/fxm90/GradientLoadingBar/pull/13).
- Added support for Swift 5.0
## [1.1.15] - 2019-14-02
### Changed
- Remove dependency `Observable` in favour of a more lightweight implementation
- Small internal refactorings and cleanup
## [1.1.14] - 2018-20-12
### Changed
- Fix fade-out animation not working in iOS 12 (https://openradar.appspot.com/46753872)
- Fixed fade-out animation not working in iOS 12 (https://openradar.appspot.com/46753872)
## [1.1.13] - 2018-14-11
### Changed
- Adapt code to support new version from dependency `Observable`
## [1.1.12] - 2018-22-09
### Changed
- Updates for Swift 4.2
- Refactored to observables
- Removed `UIColor` initializers, as they're not required for the project to work (and it's not very common to use hex color codes in iOS)
- If you need them in your project, feel free to copy & paste to following file into you project: https://gist.github.com/fxm90/1350d27abf92af3be59aaa9eb72c9310
- If you need them in your project, feel free to copy & paste the following file into your project: https://gist.github.com/fxm90/1350d27abf92af3be59aaa9eb72c9310
## [1.1.11] - 2018-04-06
### Changed
- Carthage Support
- Carthage Support
## [1.1.10] - 2018-14-04
### Changed
- Updates for Swift 4.1
- Formatted code
- Updates for Swift 4.1
- Formatted code
## [1.1.9] - 2018-27-01
### Changed
- Formatted code
- Refactorings
- Formatted code
- Refactorings
## [1.1.8] - 2018-20-01
### Added
- Further documentation
- Further examples
- Further documentation
- Further examples
### Changed
- Refactorings (moved all logic into view model)
- Refactorings (moved all logic into view model)
## [1.1.7] - 2017-12-30
### Changed
- Adapt layout for iPhoneX
- Convert code to Swift 4
- Refactor code to MVVM
- Adapt layout for iPhoneX
- Convert code to Swift 4
- Refactor code to MVVM
## [1.1.6] - 2017-10-30
### Added
- Allow setting a custom superview
- Documentation
- Allow setting a custom superview
- Documentation
## [1.1.5] - 2017-10-03
### Fixed
- Struct 'DefaultValues' is private and cannot be referenced from a default argument value ([#001])
- Struct 'DefaultValues' is private and cannot be referenced from a default argument value ([#001])
[#001]: https://github.com/fxm90/GradientLoadingBar/issues/1
## [1.1.4] - 2017-10-01
### Added
- Refactored project structure to match "pod lib create" / TravisCI integration
- Added example project
- Refactored project structure to match "pod lib create" / TravisCI integration
- Added example project
## [1.1.3] - 2017-09-25
### Fixed
- Fix never adding gradient view to "keyWindow" if it is not available on first call to "show()"
- Fixed never adding gradient view to "keyWindow" if it is not available on first call to "show()"
## [1.1.2] - 2017-08-27
### Added
- Basic tests / TravisCI integration
- Refactored extension for UIColor initializer
- Changelog
- Basic tests / TravisCI integration
- Refactored extension for UIColor initializer
- Changelog
## [1.1.1] - 2017-08-18
### Fixed
- Fix optical animation issue
- Fix orientation change bug
- Fixed optical animation issue
- Fixed orientation change bug
### Changed
- Refactor entire code
- Lint code
- Refactor entire code
- Lint code
## [1.1.0] - 2017-02-28
### Changed
- Allow changing gradient colors
- Allow changing gradient colors
## [1.0.0] - 2016-12-28
- Initial release
[Unreleased]: https://github.com/fxm90/GradientLoadingBar/compare/2.2.2...master
[Unreleased]: https://github.com/fxm90/GradientLoadingBar/compare/4.0.0...main
[4.0.0]: https://github.com/fxm90/GradientLoadingBar/compare/3.0.0...4.0.0
[3.0.0]: https://github.com/fxm90/GradientLoadingBar/compare/2.3.5...3.0.0
[2.3.5]: https://github.com/fxm90/GradientLoadingBar/compare/2.3.4...2.3.5
[2.3.4]: https://github.com/fxm90/GradientLoadingBar/compare/2.3.3...2.3.4
[2.3.3]: https://github.com/fxm90/GradientLoadingBar/compare/2.3.2...2.3.3
[2.3.2]: https://github.com/fxm90/GradientLoadingBar/compare/2.3.1...2.3.2
[2.3.1]: https://github.com/fxm90/GradientLoadingBar/compare/2.3.0...2.3.1
[2.3.0]: https://github.com/fxm90/GradientLoadingBar/compare/2.2.5...2.3.0
[2.2.5]: https://github.com/fxm90/GradientLoadingBar/compare/2.2.4...2.2.5
[2.2.4]: https://github.com/fxm90/GradientLoadingBar/compare/2.2.3...2.2.4
[2.2.3]: https://github.com/fxm90/GradientLoadingBar/compare/2.2.2...2.2.3
[2.2.2]: https://github.com/fxm90/GradientLoadingBar/compare/2.2.1...2.2.2
[2.2.1]: https://github.com/fxm90/GradientLoadingBar/compare/2.2.0...2.2.1
[2.2.0]: https://github.com/fxm90/GradientLoadingBar/compare/2.1.1...2.2.0
-1
View File
@@ -1 +0,0 @@
github "fxm90/LightweightObservable" ~> 1.0
-19
View File
@@ -1,19 +0,0 @@
#
# .swiftformat
# Created by Felix Mau (https://felix.hamburg)
#
#
# "Global" formatting options
#
# "Global" formatting options will be applied from the `SwiftConfigurationFiles` repository.
# See https://github.com/fxm90/SwiftConfigurationFiles for further details.
#
#
# File options
#
# Note: Excluded paths are relative to the SwiftFormat configuration file.
# Therefore we can't specify them in the configuration file from the `SwiftConfigurationFiles` repository.
#
--exclude Pods
-27
View File
@@ -1,27 +0,0 @@
#
# .swiftlint.yml
# Created by Felix Mau (https://felix.hamburg)
#
opt_in_rules:
- file_header
file_header:
required_pattern: |
\/\/
\/\/ SWIFTLINT_CURRENT_FILENAME
\/\/ (GradientLoadingBar|GradientLoadingBar_Example|GradientLoadingBar_Tests|GradientLoadingBar_SnapshotTests)
\/\/
\/\/ Created by .*? on \d{2}\.\d{2}\.\d{2}\.
\/\/ Copyright © \d{4} .*?\. All rights reserved\.
\/\/
included:
- ./
- ../GradientLoadingBar
excluded:
- Pods
parent_config: Pods/SwiftConfigurationFiles/.swiftlint.yml
-7
View File
@@ -1,7 +0,0 @@
source "https://rubygems.org"
gem "cocoapods"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
-276
View File
@@ -1,276 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.3)
activesupport (5.2.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.1.1)
aws-partitions (1.479.0)
aws-sdk-core (3.117.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.44.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.96.1)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.4)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.0.3)
cocoapods (1.10.1)
addressable (~> 2.6)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.10.1)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.6.6)
nap (~> 1.0)
ruby-macho (~> 1.4)
xcodeproj (>= 1.19.0, < 2.0)
cocoapods-core (1.10.1)
activesupport (> 5.0, < 6)
addressable (~> 2.6)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.4.0)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-trunk (1.5.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.1.9)
declarative (0.0.20)
digest-crc (0.6.3)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.2)
escape (0.0.4)
ethon (0.14.0)
ffi (>= 1.15.0)
excon (0.85.0)
faraday (1.5.1)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday_middleware (1.0.0)
faraday (~> 1.0)
fastimage (2.2.4)
fastlane (2.187.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.1)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-swiftformat (0.1.0)
ffi (1.15.3)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.8.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.6.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.5.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.6.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.1.0)
google-cloud-storage (1.34.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (0.16.2)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.14)
highline (2.0.3)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
jmespath (1.4.0)
json (2.5.1)
jwt (2.2.3)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.0)
minitest (5.14.4)
molinillo (0.6.6)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
os (1.1.1)
plist (3.6.0)
public_suffix (4.0.6)
rake (13.0.6)
representable (3.1.1)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby-macho (1.4.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.15.0)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
trailblazer-option (0.1.1)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.9)
thread_safe (~> 0.1)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.20.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
cocoapods
fastlane
fastlane-plugin-swiftformat
BUNDLED WITH
2.1.4
@@ -1,903 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
397E9F2412E802485B755A87 /* Pods_GradientLoadingBar_SnapshotTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABA9B1EF5F3928BA6E31D3EA /* Pods_GradientLoadingBar_SnapshotTests.framework */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* BasicExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* BasicExampleViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
640E8AC0231457620008C5A6 /* GradientActivityIndicatorViewModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640E8ABF231457620008C5A6 /* GradientActivityIndicatorViewModelTestCase.swift */; };
6430C3A32338F1CA001F4A33 /* LargeNavigationBarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6430C3A22338F1CA001F4A33 /* LargeNavigationBarHeaderView.swift */; };
649A163C23770B4400404F3C /* DefaultColorSnapshotTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A163B23770B4400404F3C /* DefaultColorSnapshotTestCase.swift */; };
649A164423770E9300404F3C /* CustomColorSnapshotTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A164323770E9300404F3C /* CustomColorSnapshotTestCase.swift */; };
64C596EC2351D80000A6CC97 /* InterfaceBuilderExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C596EB2351D7FF00A6CC97 /* InterfaceBuilderExampleViewController.swift */; };
64E0F5BF2211B7D20066D00D /* GradientLoadingBarViewModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E0F5BB2211B7D20066D00D /* GradientLoadingBarViewModelTestCase.swift */; };
64E7F4252293E16B00BE5095 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E7F4242293E16B00BE5095 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift */; };
77357BD34CFF2778341F80E8 /* Pods_GradientLoadingBar_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03CD5D69D7C3FBBDBF5FABBB /* Pods_GradientLoadingBar_Example.framework */; };
8020C040972E902A8CCAA9E7 /* Pods_GradientLoadingBar_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 434E3C8A6E245A38C2831E73 /* Pods_GradientLoadingBar_Tests.framework */; };
972CFBCC2138849A006F14C9 /* SafeAreaExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 972CFBCB2138849A006F14C9 /* SafeAreaExampleViewController.swift */; };
97464E6721372BCD0097183A /* AdvancedExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97464E6621372BCD0097183A /* AdvancedExampleViewController.swift */; };
97464E6921372C010097183A /* NavigationBarExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97464E6821372C010097183A /* NavigationBarExampleViewController.swift */; };
97B6F5FE2143F31F008CFEA5 /* UIColor+CustomColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B6F5FA2143F31F008CFEA5 /* UIColor+CustomColors.swift */; };
97B6F5FF2143F31F008CFEA5 /* BlueFilledButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B6F5FC2143F31F008CFEA5 /* BlueFilledButton.swift */; };
97B6F6002143F31F008CFEA5 /* BlueBorderedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B6F5FD2143F31F008CFEA5 /* BlueBorderedButton.swift */; };
DC190F622496A25A0069E2B0 /* NotchGradientLoadingBarControllerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC190F602496A24F0069E2B0 /* NotchGradientLoadingBarControllerTestCase.swift */; };
DC9FF938270A470D0065B5B9 /* NotchGradientLoadingBarViewModelTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9FF937270A470D0065B5B9 /* NotchGradientLoadingBarViewModelTestCase.swift */; };
DCE3AB13249761B300889FFA /* Snapshotting+WindowedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE3AB11249761AB00889FFA /* Snapshotting+WindowedImage.swift */; };
DCE3AB16249764BB00889FFA /* GradientLoadingBarControllerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE3AB14249764A000889FFA /* GradientLoadingBarControllerTestCase.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 607FACC81AFB9204008FA782 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 607FACCF1AFB9204008FA782;
remoteInfo = GradientLoadingBar;
};
649A163E23770B4400404F3C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 607FACC81AFB9204008FA782 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 607FACCF1AFB9204008FA782;
remoteInfo = GradientLoadingBar_Example;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
03CD5D69D7C3FBBDBF5FABBB /* Pods_GradientLoadingBar_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GradientLoadingBar_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0E24EC094BE0798D462B5F96 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
14959BE5652C9E83FC09BBDD /* Pods-GradientLoadingBar_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GradientLoadingBar_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-GradientLoadingBar_Tests/Pods-GradientLoadingBar_Tests.release.xcconfig"; sourceTree = "<group>"; };
172FD3755BEBFF66683F5D81 /* Pods-GradientLoadingBar_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GradientLoadingBar_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-GradientLoadingBar_Example/Pods-GradientLoadingBar_Example.release.xcconfig"; sourceTree = "<group>"; };
3DC26E6D2B0C63183F508578 /* Pods-GradientLoadingBar_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GradientLoadingBar_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GradientLoadingBar_Example/Pods-GradientLoadingBar_Example.debug.xcconfig"; sourceTree = "<group>"; };
434E3C8A6E245A38C2831E73 /* Pods_GradientLoadingBar_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GradientLoadingBar_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD01AFB9204008FA782 /* GradientLoadingBar_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GradientLoadingBar_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
607FACD71AFB9204008FA782 /* BasicExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicExampleViewController.swift; sourceTree = "<group>"; };
607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
607FACE51AFB9204008FA782 /* GradientLoadingBar_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GradientLoadingBar_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
640E8ABF231457620008C5A6 /* GradientActivityIndicatorViewModelTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientActivityIndicatorViewModelTestCase.swift; sourceTree = "<group>"; };
6430C3A22338F1CA001F4A33 /* LargeNavigationBarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeNavigationBarHeaderView.swift; sourceTree = "<group>"; };
649A163923770B4400404F3C /* GradientLoadingBar_SnapshotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GradientLoadingBar_SnapshotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
649A163B23770B4400404F3C /* DefaultColorSnapshotTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultColorSnapshotTestCase.swift; sourceTree = "<group>"; };
649A163D23770B4400404F3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
649A164323770E9300404F3C /* CustomColorSnapshotTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomColorSnapshotTestCase.swift; sourceTree = "<group>"; };
64C596EB2351D7FF00A6CC97 /* InterfaceBuilderExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceBuilderExampleViewController.swift; sourceTree = "<group>"; };
64E0F5BB2211B7D20066D00D /* GradientLoadingBarViewModelTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientLoadingBarViewModelTestCase.swift; sourceTree = "<group>"; };
64E7F4242293E16B00BE5095 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift"; sourceTree = "<group>"; };
972CFBCB2138849A006F14C9 /* SafeAreaExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeAreaExampleViewController.swift; sourceTree = "<group>"; };
97464E6621372BCD0097183A /* AdvancedExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedExampleViewController.swift; sourceTree = "<group>"; };
97464E6821372C010097183A /* NavigationBarExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarExampleViewController.swift; sourceTree = "<group>"; };
97B6F5FA2143F31F008CFEA5 /* UIColor+CustomColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+CustomColors.swift"; sourceTree = "<group>"; };
97B6F5FC2143F31F008CFEA5 /* BlueFilledButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlueFilledButton.swift; sourceTree = "<group>"; };
97B6F5FD2143F31F008CFEA5 /* BlueBorderedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlueBorderedButton.swift; sourceTree = "<group>"; };
A98CA9F1B0C1BD6864C14E01 /* Pods-GradientLoadingBar_SnapshotTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GradientLoadingBar_SnapshotTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-GradientLoadingBar_SnapshotTests/Pods-GradientLoadingBar_SnapshotTests.release.xcconfig"; sourceTree = "<group>"; };
ABA9B1EF5F3928BA6E31D3EA /* Pods_GradientLoadingBar_SnapshotTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GradientLoadingBar_SnapshotTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C06CA4DFA1DCB931095ABD3D /* GradientLoadingBar.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = GradientLoadingBar.podspec; path = ../GradientLoadingBar.podspec; sourceTree = "<group>"; };
DC190F602496A24F0069E2B0 /* NotchGradientLoadingBarControllerTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchGradientLoadingBarControllerTestCase.swift; sourceTree = "<group>"; };
DC9FF937270A470D0065B5B9 /* NotchGradientLoadingBarViewModelTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchGradientLoadingBarViewModelTestCase.swift; sourceTree = "<group>"; };
DCE3AB11249761AB00889FFA /* Snapshotting+WindowedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Snapshotting+WindowedImage.swift"; sourceTree = "<group>"; };
DCE3AB14249764A000889FFA /* GradientLoadingBarControllerTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientLoadingBarControllerTestCase.swift; sourceTree = "<group>"; };
DCE3AB172497670400889FFA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
E7B292793F03D669ACAA92D2 /* Pods-GradientLoadingBar_SnapshotTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GradientLoadingBar_SnapshotTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GradientLoadingBar_SnapshotTests/Pods-GradientLoadingBar_SnapshotTests.debug.xcconfig"; sourceTree = "<group>"; };
F76834BE9E43CA11377EC030 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
FECB95A4857728345199461F /* Pods-GradientLoadingBar_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GradientLoadingBar_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GradientLoadingBar_Tests/Pods-GradientLoadingBar_Tests.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
607FACCD1AFB9204008FA782 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
77357BD34CFF2778341F80E8 /* Pods_GradientLoadingBar_Example.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
607FACE21AFB9204008FA782 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8020C040972E902A8CCAA9E7 /* Pods_GradientLoadingBar_Tests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
649A163623770B4400404F3C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
397E9F2412E802485B755A87 /* Pods_GradientLoadingBar_SnapshotTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
607FACC71AFB9204008FA782 = {
isa = PBXGroup;
children = (
607FACF51AFB993E008FA782 /* Podspec Metadata */,
607FACD21AFB9204008FA782 /* Example for GradientLoadingBar */,
607FACE81AFB9204008FA782 /* Tests */,
649A163A23770B4400404F3C /* SnapshotTests */,
607FACD11AFB9204008FA782 /* Products */,
BFC08F9E633527E553EDA250 /* Pods */,
DB185876CEF7C6517EB25B58 /* Frameworks */,
);
sourceTree = "<group>";
};
607FACD11AFB9204008FA782 /* Products */ = {
isa = PBXGroup;
children = (
607FACD01AFB9204008FA782 /* GradientLoadingBar_Example.app */,
607FACE51AFB9204008FA782 /* GradientLoadingBar_Tests.xctest */,
649A163923770B4400404F3C /* GradientLoadingBar_SnapshotTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
607FACD21AFB9204008FA782 /* Example for GradientLoadingBar */ = {
isa = PBXGroup;
children = (
97B6F5F92143F31F008CFEA5 /* Extensions */,
97B6F5FB2143F31F008CFEA5 /* Views */,
607FACD71AFB9204008FA782 /* BasicExampleViewController.swift */,
972CFBCB2138849A006F14C9 /* SafeAreaExampleViewController.swift */,
64C596EB2351D7FF00A6CC97 /* InterfaceBuilderExampleViewController.swift */,
97464E6621372BCD0097183A /* AdvancedExampleViewController.swift */,
97464E6821372C010097183A /* NavigationBarExampleViewController.swift */,
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
607FACD91AFB9204008FA782 /* Main.storyboard */,
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
607FACD31AFB9204008FA782 /* Supporting Files */,
);
name = "Example for GradientLoadingBar";
path = GradientLoadingBar;
sourceTree = "<group>";
};
607FACD31AFB9204008FA782 /* Supporting Files */ = {
isa = PBXGroup;
children = (
607FACD41AFB9204008FA782 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
607FACE81AFB9204008FA782 /* Tests */ = {
isa = PBXGroup;
children = (
64C6B3E72313259500076A69 /* ViewModel */,
64E7F4222293E16B00BE5095 /* Views */,
607FACE91AFB9204008FA782 /* Supporting Files */,
);
path = Tests;
sourceTree = "<group>";
};
607FACE91AFB9204008FA782 /* Supporting Files */ = {
isa = PBXGroup;
children = (
607FACEA1AFB9204008FA782 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
607FACF51AFB993E008FA782 /* Podspec Metadata */ = {
isa = PBXGroup;
children = (
C06CA4DFA1DCB931095ABD3D /* GradientLoadingBar.podspec */,
0E24EC094BE0798D462B5F96 /* README.md */,
F76834BE9E43CA11377EC030 /* LICENSE */,
);
name = "Podspec Metadata";
sourceTree = "<group>";
};
649A163A23770B4400404F3C /* SnapshotTests */ = {
isa = PBXGroup;
children = (
DCE3AB102497619600889FFA /* _Helper */,
DC190F5E2496A2290069E2B0 /* GradientActivityIndicatorView */,
DC190F5F2496A2380069E2B0 /* GradientLoadingBarController */,
649A163D23770B4400404F3C /* Info.plist */,
DCE3AB172497670400889FFA /* README.md */,
);
path = SnapshotTests;
sourceTree = "<group>";
};
64C6B3E72313259500076A69 /* ViewModel */ = {
isa = PBXGroup;
children = (
64E0F5BB2211B7D20066D00D /* GradientLoadingBarViewModelTestCase.swift */,
640E8ABF231457620008C5A6 /* GradientActivityIndicatorViewModelTestCase.swift */,
DC9FF937270A470D0065B5B9 /* NotchGradientLoadingBarViewModelTestCase.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
64E7F4222293E16B00BE5095 /* Views */ = {
isa = PBXGroup;
children = (
64E7F4242293E16B00BE5095 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift */,
);
path = Views;
sourceTree = "<group>";
};
97B6F5F92143F31F008CFEA5 /* Extensions */ = {
isa = PBXGroup;
children = (
97B6F5FA2143F31F008CFEA5 /* UIColor+CustomColors.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
97B6F5FB2143F31F008CFEA5 /* Views */ = {
isa = PBXGroup;
children = (
97B6F5FC2143F31F008CFEA5 /* BlueFilledButton.swift */,
97B6F5FD2143F31F008CFEA5 /* BlueBorderedButton.swift */,
6430C3A22338F1CA001F4A33 /* LargeNavigationBarHeaderView.swift */,
);
path = Views;
sourceTree = "<group>";
};
BFC08F9E633527E553EDA250 /* Pods */ = {
isa = PBXGroup;
children = (
3DC26E6D2B0C63183F508578 /* Pods-GradientLoadingBar_Example.debug.xcconfig */,
172FD3755BEBFF66683F5D81 /* Pods-GradientLoadingBar_Example.release.xcconfig */,
FECB95A4857728345199461F /* Pods-GradientLoadingBar_Tests.debug.xcconfig */,
14959BE5652C9E83FC09BBDD /* Pods-GradientLoadingBar_Tests.release.xcconfig */,
E7B292793F03D669ACAA92D2 /* Pods-GradientLoadingBar_SnapshotTests.debug.xcconfig */,
A98CA9F1B0C1BD6864C14E01 /* Pods-GradientLoadingBar_SnapshotTests.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
DB185876CEF7C6517EB25B58 /* Frameworks */ = {
isa = PBXGroup;
children = (
03CD5D69D7C3FBBDBF5FABBB /* Pods_GradientLoadingBar_Example.framework */,
434E3C8A6E245A38C2831E73 /* Pods_GradientLoadingBar_Tests.framework */,
ABA9B1EF5F3928BA6E31D3EA /* Pods_GradientLoadingBar_SnapshotTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
DC190F5E2496A2290069E2B0 /* GradientActivityIndicatorView */ = {
isa = PBXGroup;
children = (
649A163B23770B4400404F3C /* DefaultColorSnapshotTestCase.swift */,
649A164323770E9300404F3C /* CustomColorSnapshotTestCase.swift */,
);
path = GradientActivityIndicatorView;
sourceTree = "<group>";
};
DC190F5F2496A2380069E2B0 /* GradientLoadingBarController */ = {
isa = PBXGroup;
children = (
DCE3AB14249764A000889FFA /* GradientLoadingBarControllerTestCase.swift */,
DC190F602496A24F0069E2B0 /* NotchGradientLoadingBarControllerTestCase.swift */,
);
path = GradientLoadingBarController;
sourceTree = "<group>";
};
DCE3AB102497619600889FFA /* _Helper */ = {
isa = PBXGroup;
children = (
DCE3AB11249761AB00889FFA /* Snapshotting+WindowedImage.swift */,
);
path = _Helper;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
607FACCF1AFB9204008FA782 /* GradientLoadingBar_Example */ = {
isa = PBXNativeTarget;
buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "GradientLoadingBar_Example" */;
buildPhases = (
8A80B9B4778F0A3D30371790 /* [CP] Check Pods Manifest.lock */,
607FACCC1AFB9204008FA782 /* Sources */,
607FACCD1AFB9204008FA782 /* Frameworks */,
607FACCE1AFB9204008FA782 /* Resources */,
1A66A3C243446BE65EBE3AAE /* [CP] Embed Pods Frameworks */,
972EE1C9201CA5C4001F603C /* Swiftformat */,
9753E83D1F80F7D300EF2822 /* Swiftlint */,
);
buildRules = (
);
dependencies = (
);
name = GradientLoadingBar_Example;
productName = GradientLoadingBar;
productReference = 607FACD01AFB9204008FA782 /* GradientLoadingBar_Example.app */;
productType = "com.apple.product-type.application";
};
607FACE41AFB9204008FA782 /* GradientLoadingBar_Tests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "GradientLoadingBar_Tests" */;
buildPhases = (
04A6606B8779F59C5BA953A7 /* [CP] Check Pods Manifest.lock */,
607FACE11AFB9204008FA782 /* Sources */,
607FACE21AFB9204008FA782 /* Frameworks */,
607FACE31AFB9204008FA782 /* Resources */,
);
buildRules = (
);
dependencies = (
607FACE71AFB9204008FA782 /* PBXTargetDependency */,
);
name = GradientLoadingBar_Tests;
productName = Tests;
productReference = 607FACE51AFB9204008FA782 /* GradientLoadingBar_Tests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
649A163823770B4400404F3C /* GradientLoadingBar_SnapshotTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 649A164223770B4400404F3C /* Build configuration list for PBXNativeTarget "GradientLoadingBar_SnapshotTests" */;
buildPhases = (
C6F0F8135CCFB10901DB76DB /* [CP] Check Pods Manifest.lock */,
649A163523770B4400404F3C /* Sources */,
649A163623770B4400404F3C /* Frameworks */,
649A163723770B4400404F3C /* Resources */,
568034A03F733A9FAEAC587B /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
649A163F23770B4400404F3C /* PBXTargetDependency */,
);
name = GradientLoadingBar_SnapshotTests;
productName = SnapshotTests;
productReference = 649A163923770B4400404F3C /* GradientLoadingBar_SnapshotTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
607FACC81AFB9204008FA782 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1120;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = CocoaPods;
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
LastSwiftMigration = 1020;
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
LastSwiftMigration = 1020;
TestTargetID = 607FACCF1AFB9204008FA782;
};
649A163823770B4400404F3C = {
CreatedOnToolsVersion = 11.2;
ProvisioningStyle = Automatic;
TestTargetID = 607FACCF1AFB9204008FA782;
};
};
};
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "GradientLoadingBar" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 607FACC71AFB9204008FA782;
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
607FACCF1AFB9204008FA782 /* GradientLoadingBar_Example */,
607FACE41AFB9204008FA782 /* GradientLoadingBar_Tests */,
649A163823770B4400404F3C /* GradientLoadingBar_SnapshotTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
607FACCE1AFB9204008FA782 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */,
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
607FACE31AFB9204008FA782 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
649A163723770B4400404F3C /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
04A6606B8779F59C5BA953A7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-GradientLoadingBar_Tests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
1A66A3C243446BE65EBE3AAE /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-GradientLoadingBar_Example/Pods-GradientLoadingBar_Example-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/GradientLoadingBar/GradientLoadingBar.framework",
"${BUILT_PRODUCTS_DIR}/LightweightObservable/LightweightObservable.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GradientLoadingBar.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LightweightObservable.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GradientLoadingBar_Example/Pods-GradientLoadingBar_Example-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
568034A03F733A9FAEAC587B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-GradientLoadingBar_SnapshotTests/Pods-GradientLoadingBar_SnapshotTests-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/SnapshotTesting/SnapshotTesting.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapshotTesting.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GradientLoadingBar_SnapshotTests/Pods-GradientLoadingBar_SnapshotTests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
8A80B9B4778F0A3D30371790 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-GradientLoadingBar_Example-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
972EE1C9201CA5C4001F603C /* Swiftformat */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = Swiftformat;
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "SWIFTFORMAT=\"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat\"\nif [ ! -f \"$SWIFTFORMAT\" ]; then\n echo \"warning: SwiftFormat not installed!\"\n exit 1\nfi\n\n$SWIFTFORMAT ../ --config \"${PODS_ROOT}/SwiftConfigurationFiles/.swiftformat\"\n";
};
9753E83D1F80F7D300EF2822 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = Swiftlint;
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "SWIFTLINT=\"${PODS_ROOT}/SwiftLint/swiftlint\"\nif [ ! -f \"$SWIFTLINT\" ]; then\n echo \"warning: SwiftLint not installed!\"\n exit 1\nfi\n\n$SWIFTLINT\n";
};
C6F0F8135CCFB10901DB76DB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-GradientLoadingBar_SnapshotTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
607FACCC1AFB9204008FA782 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
607FACD81AFB9204008FA782 /* BasicExampleViewController.swift in Sources */,
64C596EC2351D80000A6CC97 /* InterfaceBuilderExampleViewController.swift in Sources */,
972CFBCC2138849A006F14C9 /* SafeAreaExampleViewController.swift in Sources */,
97464E6721372BCD0097183A /* AdvancedExampleViewController.swift in Sources */,
6430C3A32338F1CA001F4A33 /* LargeNavigationBarHeaderView.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
97B6F5FF2143F31F008CFEA5 /* BlueFilledButton.swift in Sources */,
97B6F5FE2143F31F008CFEA5 /* UIColor+CustomColors.swift in Sources */,
97B6F6002143F31F008CFEA5 /* BlueBorderedButton.swift in Sources */,
97464E6921372C010097183A /* NavigationBarExampleViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
607FACE11AFB9204008FA782 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
640E8AC0231457620008C5A6 /* GradientActivityIndicatorViewModelTestCase.swift in Sources */,
64E0F5BF2211B7D20066D00D /* GradientLoadingBarViewModelTestCase.swift in Sources */,
DC9FF938270A470D0065B5B9 /* NotchGradientLoadingBarViewModelTestCase.swift in Sources */,
64E7F4252293E16B00BE5095 /* GradientActivityIndicatorView+AnimateIsHiddenTestCase.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
649A163523770B4400404F3C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DCE3AB13249761B300889FFA /* Snapshotting+WindowedImage.swift in Sources */,
DC190F622496A25A0069E2B0 /* NotchGradientLoadingBarControllerTestCase.swift in Sources */,
DCE3AB16249764BB00889FFA /* GradientLoadingBarControllerTestCase.swift in Sources */,
649A164423770E9300404F3C /* CustomColorSnapshotTestCase.swift in Sources */,
649A163C23770B4400404F3C /* DefaultColorSnapshotTestCase.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
607FACE71AFB9204008FA782 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 607FACCF1AFB9204008FA782 /* GradientLoadingBar_Example */;
targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */;
};
649A163F23770B4400404F3C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 607FACCF1AFB9204008FA782 /* GradientLoadingBar_Example */;
targetProxy = 649A163E23770B4400404F3C /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
607FACD91AFB9204008FA782 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
607FACDA1AFB9204008FA782 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = {
isa = PBXVariantGroup;
children = (
607FACDF1AFB9204008FA782 /* Base */,
);
name = LaunchScreen.xib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
607FACED1AFB9204008FA782 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
607FACEE1AFB9204008FA782 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
607FACF01AFB9204008FA782 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3DC26E6D2B0C63183F508578 /* Pods-GradientLoadingBar_Example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = GradientLoadingBar/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
607FACF11AFB9204008FA782 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 172FD3755BEBFF66683F5D81 /* Pods-GradientLoadingBar_Example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = GradientLoadingBar/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
607FACF31AFB9204008FA782 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FECB95A4857728345199461F /* Pods-GradientLoadingBar_Tests.debug.xcconfig */;
buildSettings = {
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GradientLoadingBar_Example.app/GradientLoadingBar_Example";
};
name = Debug;
};
607FACF41AFB9204008FA782 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 14959BE5652C9E83FC09BBDD /* Pods-GradientLoadingBar_Tests.release.xcconfig */;
buildSettings = {
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Tests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GradientLoadingBar_Example.app/GradientLoadingBar_Example";
};
name = Release;
};
649A164023770B4400404F3C /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E7B292793F03D669ACAA92D2 /* Pods-GradientLoadingBar_SnapshotTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
DEBUG_INFORMATION_FORMAT = dwarf;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = SnapshotTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.SnapshotTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GradientLoadingBar_Example.app/GradientLoadingBar_Example";
};
name = Debug;
};
649A164123770B4400404F3C /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A98CA9F1B0C1BD6864C14E01 /* Pods-GradientLoadingBar_SnapshotTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = SnapshotTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.SnapshotTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GradientLoadingBar_Example.app/GradientLoadingBar_Example";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "GradientLoadingBar" */ = {
isa = XCConfigurationList;
buildConfigurations = (
607FACED1AFB9204008FA782 /* Debug */,
607FACEE1AFB9204008FA782 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "GradientLoadingBar_Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
607FACF01AFB9204008FA782 /* Debug */,
607FACF11AFB9204008FA782 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "GradientLoadingBar_Tests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
607FACF31AFB9204008FA782 /* Debug */,
607FACF41AFB9204008FA782 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
649A164223770B4400404F3C /* Build configuration list for PBXNativeTarget "GradientLoadingBar_SnapshotTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
649A164023770B4400404F3C /* Debug */,
649A164123770B4400404F3C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 607FACC81AFB9204008FA782 /* Project object */;
}
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:GradientLoadingBar.xcodeproj">
</FileRef>
</Workspace>
@@ -1,132 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "GradientLoadingBar_Example.app"
BlueprintName = "GradientLoadingBar_Example"
ReferencedContainer = "container:GradientLoadingBar.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACE41AFB9204008FA782"
BuildableName = "GradientLoadingBar_Tests.xctest"
BlueprintName = "GradientLoadingBar_Tests"
ReferencedContainer = "container:GradientLoadingBar.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "GradientLoadingBar_Example.app"
BlueprintName = "GradientLoadingBar_Example"
ReferencedContainer = "container:GradientLoadingBar.xcodeproj">
</BuildableReference>
</MacroExpansion>
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AD9297F9E6CC3E50EDCFECA0A77800B7"
BuildableName = "GradientLoadingBar.framework"
BlueprintName = "GradientLoadingBar"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACE41AFB9204008FA782"
BuildableName = "GradientLoadingBar_Tests.xctest"
BlueprintName = "GradientLoadingBar_Tests"
ReferencedContainer = "container:GradientLoadingBar.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "649A163823770B4400404F3C"
BuildableName = "GradientLoadingBar_SnapshotTests.xctest"
BlueprintName = "GradientLoadingBar_SnapshotTests"
ReferencedContainer = "container:GradientLoadingBar.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "GradientLoadingBar_Example.app"
BlueprintName = "GradientLoadingBar_Example"
ReferencedContainer = "container:GradientLoadingBar.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607FACCF1AFB9204008FA782"
BuildableName = "GradientLoadingBar_Example.app"
BlueprintName = "GradientLoadingBar_Example"
ReferencedContainer = "container:GradientLoadingBar.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:GradientLoadingBar.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -1,105 +0,0 @@
//
// AdvancedExampleViewController.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 30.08.18.
// Copyright © 2018 Felix Mau. All rights reserved.
//
import UIKit
import GradientLoadingBar
class AdvancedExampleViewController: UIViewController {
// MARK: - Config
private enum Config {
/// The programatically applied height of the `GradientActivityIndicatorView`.
static let height: CGFloat = 3.5
}
// MARK: - Outlets
@IBOutlet private var programmaticallyButton: BlueBorderedButton!
@IBOutlet private var customColorsButton: BlueBorderedButton!
// MARK: - Private properties
// swiftlint:disable:next identifier_name
private let programmaticallyGradientActivityIndicatorView = GradientActivityIndicatorView()
// swiftlint:disable:next identifier_name
private let customColorsGradientActivityIndicatorView: GradientActivityIndicatorView = {
let gradientActivityIndicatorView = GradientActivityIndicatorView()
// Source: https://color.adobe.com/Pink-Flamingo-color-theme-10343714/
gradientActivityIndicatorView.gradientColors = [
#colorLiteral(red: 0.9490196078, green: 0.3215686275, blue: 0.431372549, alpha: 1), #colorLiteral(red: 0.9450980392, green: 0.4784313725, blue: 0.5921568627, alpha: 1), #colorLiteral(red: 0.9529411765, green: 0.737254902, blue: 0.7843137255, alpha: 1), #colorLiteral(red: 0.4274509804, green: 0.8666666667, blue: 0.9490196078, alpha: 1), #colorLiteral(red: 0.7568627451, green: 0.9411764706, blue: 0.9568627451, alpha: 1),
]
return gradientActivityIndicatorView
}()
// MARK: - Public methods
override func viewDidLoad() {
super.viewDidLoad()
setupProgrammaticallyGradientActivityIndicatorView()
setupCustomColorsGradientActivityIndicatorView()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
programmaticallyGradientActivityIndicatorView.fadeOut()
customColorsGradientActivityIndicatorView.fadeOut()
}
@IBAction private func toggleProgrammaticallyButtonTouchUpInside(_: Any) {
if programmaticallyGradientActivityIndicatorView.isHidden {
programmaticallyGradientActivityIndicatorView.fadeIn()
} else {
programmaticallyGradientActivityIndicatorView.fadeOut()
}
}
@IBAction private func toggleCustomColorsButtonTouchUpInside(_: Any) {
if customColorsGradientActivityIndicatorView.isHidden {
customColorsGradientActivityIndicatorView.fadeIn()
} else {
customColorsGradientActivityIndicatorView.fadeOut()
}
}
// MARK: - Private methods
private func setupProgrammaticallyGradientActivityIndicatorView() {
programmaticallyGradientActivityIndicatorView.fadeOut(duration: 0)
programmaticallyGradientActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
programmaticallyButton.addSubview(programmaticallyGradientActivityIndicatorView)
NSLayoutConstraint.activate([
programmaticallyGradientActivityIndicatorView.leadingAnchor.constraint(equalTo: programmaticallyButton.leadingAnchor),
programmaticallyGradientActivityIndicatorView.trailingAnchor.constraint(equalTo: programmaticallyButton.trailingAnchor),
programmaticallyGradientActivityIndicatorView.topAnchor.constraint(equalTo: programmaticallyButton.topAnchor),
programmaticallyGradientActivityIndicatorView.heightAnchor.constraint(equalToConstant: Config.height),
])
}
private func setupCustomColorsGradientActivityIndicatorView() {
customColorsGradientActivityIndicatorView.fadeOut(duration: 0)
customColorsGradientActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
customColorsButton.addSubview(customColorsGradientActivityIndicatorView)
NSLayoutConstraint.activate([
customColorsGradientActivityIndicatorView.leadingAnchor.constraint(equalTo: customColorsButton.leadingAnchor),
customColorsGradientActivityIndicatorView.trailingAnchor.constraint(equalTo: customColorsButton.trailingAnchor),
customColorsGradientActivityIndicatorView.bottomAnchor.constraint(equalTo: customColorsButton.bottomAnchor),
customColorsGradientActivityIndicatorView.heightAnchor.constraint(equalToConstant: Config.height),
])
}
}
@@ -1,52 +0,0 @@
//
// AppDelegate.swift
// GradientLoadingBar
//
// Created by Felix Mau on 30.09.17.
// Copyright © 2017 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Example on how to customize the shared instance of `GradientLoadingBar`.
// We're gonna use the same height though ¯\_()_/¯
let customGradientLoadingBar = GradientLoadingBar(height: 3.0)
GradientLoadingBar.shared = customGradientLoadingBar
return true
}
func applicationWillResignActive(_: UIApplication) {
// Sent when the application is about to move from active to inactive state.
// This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application
// and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your
// application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive.
// If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Gradient Loading Bar" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iUK-He-88D">
<rect key="frame" x="20" y="219.5" width="440" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Example Application" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kZE-Up-yio">
<rect key="frame" x="20" y="240" width="440" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="kZE-Up-yio" secondAttribute="trailing" id="1w6-hQ-DuS"/>
<constraint firstItem="kZE-Up-yio" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leadingMargin" id="JLl-ee-fNq"/>
<constraint firstItem="kZE-Up-yio" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="centerY" id="b41-JE-swY"/>
<constraint firstAttribute="trailingMargin" secondItem="iUK-He-88D" secondAttribute="trailing" id="eCr-27-PHd"/>
<constraint firstItem="iUK-He-88D" firstAttribute="bottom" secondItem="iN0-l3-epB" secondAttribute="centerY" id="g3R-8N-4ZM"/>
<constraint firstItem="iUK-He-88D" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leadingMargin" id="lzx-6Y-lwW"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</objects>
</document>
@@ -1,489 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="31S-8b-Niv">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Basic Example-->
<scene sceneID="ufC-wZ-h7g">
<objects>
<viewController id="vXZ-lx-hvc" customClass="BasicExampleViewController" customModule="GradientLoadingBar_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="jyV-Pf-zRb"/>
<viewControllerLayoutGuide type="bottom" id="2fi-mo-0CV"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="kh9-bI-dsS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="V1J-1V-cbT" customClass="LargeNavigationBarHeaderView" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="138"/>
<constraints>
<constraint firstAttribute="height" constant="138" id="ydQ-Zv-f3W"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="title" value="🏡 Basic Example"/>
<userDefinedRuntimeAttribute type="string" keyPath="prompt" value="Gradient Loading Bar"/>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vCz-UL-eay" userLabel="Wrapper View">
<rect key="frame" x="0.0" y="138" width="375" height="480"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="Hrq-tB-Sk3">
<rect key="frame" x="16" y="220" width="343" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="W6V-cT-b8Q" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="MdR-sO-dac"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Show"/>
<connections>
<action selector="showButtonTouchUpInside:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="cns-MK-XRQ"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mLp-ey-yd5" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="179.5" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="vge-I5-pFX"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Hide"/>
<connections>
<action selector="hideButtonTouchUpInside:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Cyb-6a-nJk"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="Hrq-tB-Sk3" firstAttribute="leading" secondItem="vCz-UL-eay" secondAttribute="leading" constant="16" id="Er7-gx-Ray"/>
<constraint firstItem="Hrq-tB-Sk3" firstAttribute="centerY" secondItem="vCz-UL-eay" secondAttribute="centerY" id="MB9-lc-dAX"/>
<constraint firstAttribute="trailing" secondItem="Hrq-tB-Sk3" secondAttribute="trailing" constant="16" id="ao9-tR-lfs"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="V1J-1V-cbT" firstAttribute="trailing" secondItem="kh9-bI-dsS" secondAttribute="trailing" id="9TW-dR-0YF"/>
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="vCz-UL-eay" secondAttribute="bottom" id="BSE-5g-uVL"/>
<constraint firstItem="vCz-UL-eay" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" id="Lym-2g-94s"/>
<constraint firstItem="V1J-1V-cbT" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" id="PZZ-lR-n3R"/>
<constraint firstItem="V1J-1V-cbT" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" id="VPm-eG-4vI"/>
<constraint firstItem="vCz-UL-eay" firstAttribute="top" secondItem="V1J-1V-cbT" secondAttribute="bottom" id="Vhb-09-feC"/>
<constraint firstAttribute="trailing" secondItem="vCz-UL-eay" secondAttribute="trailing" id="d0u-3Y-rTF"/>
</constraints>
</view>
<tabBarItem key="tabBarItem" title="Basic Example" image="house" catalog="system" id="QbQ-0M-Q7f"/>
<navigationItem key="navigationItem" title="Basic Example" prompt="Gradient Loading Bar" largeTitleDisplayMode="always" id="Rdw-Bd-vul"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-969" y="940"/>
</scene>
<!--Navigation Bar Example-->
<scene sceneID="Blz-xb-a2R">
<objects>
<viewController id="sEh-Tc-lv1" userLabel="Navigation Bar Example" customClass="NavigationBarExampleViewController" customModule="GradientLoadingBar_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Iik-d0-ufh"/>
<viewControllerLayoutGuide type="bottom" id="J9t-Vs-nTW"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="iwi-Ev-GiP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wLK-vz-uDn" userLabel="Wrapper View">
<rect key="frame" x="0.0" y="138.5" width="375" height="479.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="xhZ-U8-Vur">
<rect key="frame" x="16" y="220" width="343" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ogf-h2-Nhm" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="Ae3-GF-JqK"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Show"/>
<connections>
<action selector="showButtonTouchUpInside:" destination="sEh-Tc-lv1" eventType="touchUpInside" id="8M0-h9-3np"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZBM-PV-ZnL" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="179.5" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="2cO-9h-Wp9"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Hide"/>
<connections>
<action selector="hideButtonTouchUpInside:" destination="sEh-Tc-lv1" eventType="touchUpInside" id="oAh-2i-goU"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="xhZ-U8-Vur" secondAttribute="trailing" constant="16" id="2sW-85-0wR"/>
<constraint firstItem="xhZ-U8-Vur" firstAttribute="leading" secondItem="wLK-vz-uDn" secondAttribute="leading" constant="16" id="Al7-Jv-AJK"/>
<constraint firstItem="xhZ-U8-Vur" firstAttribute="centerY" secondItem="wLK-vz-uDn" secondAttribute="centerY" id="zTM-SW-qLa"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="wLK-vz-uDn" firstAttribute="leading" secondItem="iwi-Ev-GiP" secondAttribute="leading" id="A9Q-8d-dYD"/>
<constraint firstItem="wLK-vz-uDn" firstAttribute="top" secondItem="Iik-d0-ufh" secondAttribute="bottom" id="Pna-g3-eRn"/>
<constraint firstAttribute="trailing" secondItem="wLK-vz-uDn" secondAttribute="trailing" id="Vn7-pj-36y"/>
<constraint firstItem="J9t-Vs-nTW" firstAttribute="top" secondItem="wLK-vz-uDn" secondAttribute="bottom" id="b3J-NQ-cWP"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="💡 Navigation Bar Example" prompt=" Gradient Loading Bar " largeTitleDisplayMode="always" id="q3W-Ea-vnS"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="3VI-no-5Tx" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2009" y="1661"/>
</scene>
<!--Safe Area Example-->
<scene sceneID="0Ok-vd-ZTP">
<objects>
<viewController id="G0W-Ta-C7r" userLabel="Safe Area Example" customClass="SafeAreaExampleViewController" customModule="GradientLoadingBar_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="4Ae-UH-dXd"/>
<viewControllerLayoutGuide type="bottom" id="9TC-19-AMF"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="5Bk-7E-1bh">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tyJ-Wx-jmZ" customClass="LargeNavigationBarHeaderView" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="138"/>
<constraints>
<constraint firstAttribute="height" constant="138" id="toR-QS-r8U"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="title" value="📲 Safe Area Example"/>
<userDefinedRuntimeAttribute type="string" keyPath="prompt" value=" Gradient Loading Bar "/>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cQz-wE-hV5" userLabel="Wrapper View">
<rect key="frame" x="0.0" y="138" width="375" height="480"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="oWt-EZ-h2R" userLabel="Upper Stack View">
<rect key="frame" x="16" y="192" width="343" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6AO-o8-uCr" userLabel="Show Button" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="JJW-aV-Rqk"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Show Basic Bar"/>
<connections>
<action selector="showBasicBarButtonTouchUpInside:" destination="G0W-Ta-C7r" eventType="touchUpInside" id="Nbx-cD-Wmk"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="enr-bC-bdY" userLabel="Hide Button" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="179.5" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="tlC-vL-RSX"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Hide Basic Bar"/>
<connections>
<action selector="hideBasicBarButtonTouchUpInside:" destination="G0W-Ta-C7r" eventType="touchUpInside" id="Um3-56-3gL"/>
</connections>
</button>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="eno-YK-fo8" userLabel="Lower Stack View">
<rect key="frame" x="16" y="248" width="343" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="oLd-Dp-Iks" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="DIo-cN-15i"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Show Notch Bar"/>
<connections>
<action selector="showNotchBarButtonTouchUpInside:" destination="G0W-Ta-C7r" eventType="touchUpInside" id="Q1l-9E-ova"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="S86-Vf-cv9" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="179.5" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="oOd-1X-Klv"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Hide Notch Bar"/>
<connections>
<action selector="hideNotchBarButtonTouchUpInside:" destination="G0W-Ta-C7r" eventType="touchUpInside" id="nJm-HD-YTg"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="oWt-EZ-h2R" firstAttribute="bottom" secondItem="cQz-wE-hV5" secondAttribute="centerY" constant="-8" id="77C-lL-1fS"/>
<constraint firstItem="oWt-EZ-h2R" firstAttribute="leading" secondItem="cQz-wE-hV5" secondAttribute="leading" constant="16" id="Hjd-XM-aek"/>
<constraint firstAttribute="trailing" secondItem="oWt-EZ-h2R" secondAttribute="trailing" constant="16" id="ifg-oy-eiV"/>
<constraint firstAttribute="trailing" secondItem="eno-YK-fo8" secondAttribute="trailing" constant="16" id="o3h-uA-jbM"/>
<constraint firstItem="eno-YK-fo8" firstAttribute="top" secondItem="cQz-wE-hV5" secondAttribute="centerY" constant="8" id="rVX-yO-z4X"/>
<constraint firstItem="eno-YK-fo8" firstAttribute="leading" secondItem="cQz-wE-hV5" secondAttribute="leading" constant="16" id="zaJ-tE-bCY"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="tyJ-Wx-jmZ" firstAttribute="leading" secondItem="5Bk-7E-1bh" secondAttribute="leading" id="BGY-Fa-Ib2"/>
<constraint firstAttribute="trailing" secondItem="cQz-wE-hV5" secondAttribute="trailing" id="KmM-2A-jAw"/>
<constraint firstItem="cQz-wE-hV5" firstAttribute="top" secondItem="tyJ-Wx-jmZ" secondAttribute="bottom" id="NJ3-mJ-mLD"/>
<constraint firstItem="cQz-wE-hV5" firstAttribute="leading" secondItem="5Bk-7E-1bh" secondAttribute="leading" id="dID-N8-kru"/>
<constraint firstItem="tyJ-Wx-jmZ" firstAttribute="top" secondItem="4Ae-UH-dXd" secondAttribute="bottom" id="obq-xF-4qg"/>
<constraint firstItem="9TC-19-AMF" firstAttribute="top" secondItem="cQz-wE-hV5" secondAttribute="bottom" id="qfC-iN-Dv4"/>
<constraint firstAttribute="trailing" secondItem="tyJ-Wx-jmZ" secondAttribute="trailing" id="u0E-oe-0ko"/>
</constraints>
</view>
<tabBarItem key="tabBarItem" title="Safe Area Example" image="rectangle.and.arrow.up.right.and.arrow.down.left" catalog="system" id="VBS-L1-AHW"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="oYv-dl-Yai" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-214" y="940"/>
</scene>
<!--Interface Builder Example-->
<scene sceneID="NSv-IO-1Fu">
<objects>
<viewController title="Interface Builder Example" id="kcg-0d-bdI" customClass="InterfaceBuilderExampleViewController" customModule="GradientLoadingBar_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="znh-se-r1k"/>
<viewControllerLayoutGuide type="bottom" id="Ruy-Se-i9x"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="G47-Se-1Pm">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ei7-8i-vp4" customClass="LargeNavigationBarHeaderView" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="138"/>
<constraints>
<constraint firstAttribute="height" constant="138" id="z0S-oU-OR8"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="title" value="🎨 Interface Builder Example"/>
<userDefinedRuntimeAttribute type="string" keyPath="prompt" value=" Gradient Loading Bar "/>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mbb-4g-X9Q" userLabel="Wrapper View">
<rect key="frame" x="0.0" y="138" width="375" height="480"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Yze-Dk-6Ol" customClass="GradientActivityIndicatorView" customModule="GradientLoadingBar">
<rect key="frame" x="16" y="16" width="343" height="3"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="3" id="jPq-M8-Jzb"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="layer.cornerRadius" value="3"/>
</userDefinedRuntimeAttributes>
</view>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="B2P-Ta-wQl">
<rect key="frame" x="16" y="220" width="343" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FVq-l4-0tb" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="hLM-JF-3uL"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Show"/>
<connections>
<action selector="showButtonTouchUpInside:" destination="kcg-0d-bdI" eventType="touchUpInside" id="bBo-X1-9o5"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FWL-hp-DMs" customClass="BlueFilledButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="179.5" y="0.0" width="163.5" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="6jt-g9-DcI"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Hide"/>
<connections>
<action selector="hideButtonTouchUpInside:" destination="kcg-0d-bdI" eventType="touchUpInside" id="Zvk-pf-LDf"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="Yze-Dk-6Ol" secondAttribute="trailing" constant="16" id="7Ae-mT-LLE"/>
<constraint firstItem="B2P-Ta-wQl" firstAttribute="leading" secondItem="mbb-4g-X9Q" secondAttribute="leading" constant="16" id="8cK-1Z-B70"/>
<constraint firstItem="Yze-Dk-6Ol" firstAttribute="leading" secondItem="mbb-4g-X9Q" secondAttribute="leading" constant="16" id="S1n-Cp-vlD"/>
<constraint firstItem="Yze-Dk-6Ol" firstAttribute="top" secondItem="mbb-4g-X9Q" secondAttribute="top" constant="16" id="asL-T7-vTV"/>
<constraint firstItem="B2P-Ta-wQl" firstAttribute="centerY" secondItem="mbb-4g-X9Q" secondAttribute="centerY" id="p7F-Kt-bT7"/>
<constraint firstAttribute="trailing" secondItem="B2P-Ta-wQl" secondAttribute="trailing" constant="16" id="tr4-w5-D3I"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="mbb-4g-X9Q" secondAttribute="trailing" id="7JI-Cf-Lbi"/>
<constraint firstItem="mbb-4g-X9Q" firstAttribute="leading" secondItem="G47-Se-1Pm" secondAttribute="leading" id="ASY-d8-7lc"/>
<constraint firstItem="mbb-4g-X9Q" firstAttribute="top" secondItem="ei7-8i-vp4" secondAttribute="bottom" id="CIq-u8-sbE"/>
<constraint firstItem="ei7-8i-vp4" firstAttribute="top" secondItem="znh-se-r1k" secondAttribute="bottom" id="G4H-cR-m9r"/>
<constraint firstAttribute="trailing" secondItem="ei7-8i-vp4" secondAttribute="trailing" id="hCK-vS-hrZ"/>
<constraint firstItem="ei7-8i-vp4" firstAttribute="leading" secondItem="G47-Se-1Pm" secondAttribute="leading" id="hch-Ju-3QH"/>
<constraint firstItem="Ruy-Se-i9x" firstAttribute="top" secondItem="mbb-4g-X9Q" secondAttribute="bottom" id="zdn-6E-6mg"/>
</constraints>
</view>
<tabBarItem key="tabBarItem" title="Interface Builder" image="photo" catalog="system" id="q1l-pB-Q4T"/>
<connections>
<outlet property="gradientActivityIndicatorView" destination="Yze-Dk-6Ol" id="v8n-H3-yyR"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="4Yh-MD-f7b" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="537" y="940"/>
</scene>
<!--Advanced Example-->
<scene sceneID="7wr-48-QKh">
<objects>
<viewController id="l28-cg-vu8" customClass="AdvancedExampleViewController" customModule="GradientLoadingBar_Example" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="vvP-wW-eUz"/>
<viewControllerLayoutGuide type="bottom" id="9Nv-iX-Uwf"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="EZo-KN-FDG">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qZq-9D-wpV" customClass="LargeNavigationBarHeaderView" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="138"/>
<constraints>
<constraint firstAttribute="height" constant="138" id="St1-Xs-Hvy"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="title" value="🚀 Advanced Example"/>
<userDefinedRuntimeAttribute type="string" keyPath="prompt" value=" Gradient Loading Bar "/>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sJk-2n-yVK" userLabel="Wrapper View">
<rect key="frame" x="0.0" y="138" width="375" height="480"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="wzq-Eo-AIU">
<rect key="frame" x="16" y="194" width="343" height="92"/>
<subviews>
<button opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JKA-wf-nyj" userLabel="Toggle Programatically Button" customClass="BlueBorderedButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="sKv-MC-7kH"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Custom Superview"/>
<connections>
<action selector="toggleProgrammaticallyButtonTouchUpInside:" destination="l28-cg-vu8" eventType="touchUpInside" id="tZg-ye-xbi"/>
</connections>
</button>
<button opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Cbn-y8-lGV" userLabel="Toggle Custom Colors Button" customClass="BlueBorderedButton" customModule="GradientLoadingBar_Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="52" width="343" height="40"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="emu-Jk-zMc"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="13"/>
<state key="normal" title="Custom Colors"/>
<connections>
<action selector="toggleCustomColorsButtonTouchUpInside:" destination="l28-cg-vu8" eventType="touchUpInside" id="BZJ-7f-xNF"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="wzq-Eo-AIU" firstAttribute="leading" secondItem="sJk-2n-yVK" secondAttribute="leading" constant="16" id="139-Ak-t87"/>
<constraint firstItem="wzq-Eo-AIU" firstAttribute="centerY" secondItem="sJk-2n-yVK" secondAttribute="centerY" id="IqE-ns-FHJ"/>
<constraint firstAttribute="trailing" secondItem="wzq-Eo-AIU" secondAttribute="trailing" constant="16" id="fcI-El-SAu"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="sJk-2n-yVK" firstAttribute="top" secondItem="qZq-9D-wpV" secondAttribute="bottom" id="12P-NY-VtG"/>
<constraint firstItem="9Nv-iX-Uwf" firstAttribute="top" secondItem="sJk-2n-yVK" secondAttribute="bottom" id="3ZY-rc-hVf"/>
<constraint firstAttribute="trailing" secondItem="sJk-2n-yVK" secondAttribute="trailing" id="Hqt-KP-eSH"/>
<constraint firstItem="qZq-9D-wpV" firstAttribute="top" secondItem="vvP-wW-eUz" secondAttribute="bottom" id="IEz-PS-kaF"/>
<constraint firstItem="sJk-2n-yVK" firstAttribute="leading" secondItem="EZo-KN-FDG" secondAttribute="leading" id="ZPm-e3-QdJ"/>
<constraint firstAttribute="trailing" secondItem="qZq-9D-wpV" secondAttribute="trailing" id="kc6-MY-EP0"/>
<constraint firstItem="qZq-9D-wpV" firstAttribute="leading" secondItem="EZo-KN-FDG" secondAttribute="leading" id="piA-rQ-qe8"/>
</constraints>
</view>
<tabBarItem key="tabBarItem" title="Advanced Example" image="paperplane.fill" catalog="system" id="SyM-zw-tlg"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="customColorsButton" destination="Cbn-y8-lGV" id="1ag-73-Mlh"/>
<outlet property="programmaticallyButton" destination="JKA-wf-nyj" id="JRN-do-tSa"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="fkr-pn-u5J" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1273" y="940"/>
</scene>
<!--Tab Bar Controller-->
<scene sceneID="gvY-1y-2Wb">
<objects>
<tabBarController automaticallyAdjustsScrollViewInsets="NO" id="31S-8b-Niv" sceneMemberID="viewController">
<toolbarItems/>
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="BSb-DD-oqz">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="vXZ-lx-hvc" kind="relationship" relationship="viewControllers" id="tyB-Oz-z6h"/>
<segue destination="G0W-Ta-C7r" kind="relationship" relationship="viewControllers" id="hPt-bW-PMO"/>
<segue destination="kcg-0d-bdI" kind="relationship" relationship="viewControllers" id="aD4-eR-rCH"/>
<segue destination="l28-cg-vu8" kind="relationship" relationship="viewControllers" id="4rs-U7-89R"/>
<segue destination="uZS-9p-82D" kind="relationship" relationship="viewControllers" id="Dcr-pe-ish"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="AXu-pz-Fer" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="538" y="134"/>
</scene>
<!--Nav Bar Example-->
<scene sceneID="HFG-Xb-221">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="uZS-9p-82D" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Nav Bar Example" image="lightbulb.fill" catalog="system" id="r91-al-1Rr"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="zAv-AH-IlN">
<rect key="frame" x="0.0" y="0.0" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="sEh-Tc-lv1" kind="relationship" relationship="rootViewController" id="Jor-5M-Hj4"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yG3-j8-Ejc" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2009" y="940"/>
</scene>
</scenes>
<resources>
<image name="house" catalog="system" width="128" height="110"/>
<image name="lightbulb.fill" catalog="system" width="113" height="128"/>
<image name="paperplane.fill" catalog="system" width="128" height="121"/>
<image name="photo" catalog="system" width="128" height="93"/>
<image name="rectangle.and.arrow.up.right.and.arrow.down.left" catalog="system" width="121" height="128"/>
</resources>
</document>
@@ -1,28 +0,0 @@
//
// BasicExampleViewController.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 30.09.17.
// Copyright © 2017 Felix Mau. All rights reserved.
//
import UIKit
import GradientLoadingBar
class BasicExampleViewController: UIViewController {
// MARK: - Public methods
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
GradientLoadingBar.shared.fadeOut()
}
@IBAction private func showButtonTouchUpInside(_: Any) {
GradientLoadingBar.shared.fadeIn()
}
@IBAction private func hideButtonTouchUpInside(_: Any) {
GradientLoadingBar.shared.fadeOut()
}
}
@@ -1,16 +0,0 @@
//
// UIColor+CustomColors.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 29.08.18.
// Copyright © 2018 Felix Mau. All rights reserved.
//
import UIKit
extension UIColor {
/// Config that contains all our custom defined colors.
enum CustomColors {
static let blue = #colorLiteral(red: 0.2862745098, green: 0.5647058824, blue: 0.8862745098, alpha: 1)
}
}
-41
View File
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
</plist>
@@ -1,32 +0,0 @@
//
// InterfaceBuilderExampleViewController.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 12.10.19.
// Copyright © 2018 Felix Mau. All rights reserved.
//
import UIKit
import GradientLoadingBar
class InterfaceBuilderExampleViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var gradientActivityIndicatorView: GradientActivityIndicatorView!
// MARK: - Public methods
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
gradientActivityIndicatorView.fadeOut()
}
@IBAction private func showButtonTouchUpInside(_: Any) {
gradientActivityIndicatorView.fadeIn()
}
@IBAction private func hideButtonTouchUpInside(_: Any) {
gradientActivityIndicatorView.fadeOut()
}
}
@@ -1,64 +0,0 @@
//
// NavigationBarExampleViewController.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 29.08.18.
// Copyright © 2018 Felix Mau. All rights reserved.
//
import UIKit
import GradientLoadingBar
class NavigationBarExampleViewController: UIViewController {
// MARK: - Config
private enum Config {
/// The programatically applied height of the `GradientActivityIndicatorView`.
static let height: CGFloat = 3
}
// MARK: - Private properties
private let gradientProgressIndicatorView = GradientActivityIndicatorView()
// MARK: - Public methods
override func viewDidLoad() {
super.viewDidLoad()
setupGradientProgressIndicatorView()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
gradientProgressIndicatorView.fadeOut()
}
@IBAction private func showButtonTouchUpInside(_: Any) {
gradientProgressIndicatorView.fadeIn()
}
@IBAction private func hideButtonTouchUpInside(_: Any) {
gradientProgressIndicatorView.fadeOut()
}
// MARK: - Private methods
private func setupGradientProgressIndicatorView() {
guard let navigationBar = navigationController?.navigationBar else { return }
gradientProgressIndicatorView.fadeOut(duration: 0)
gradientProgressIndicatorView.translatesAutoresizingMaskIntoConstraints = false
navigationBar.addSubview(gradientProgressIndicatorView)
NSLayoutConstraint.activate([
gradientProgressIndicatorView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor),
gradientProgressIndicatorView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
gradientProgressIndicatorView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
gradientProgressIndicatorView.heightAnchor.constraint(equalToConstant: Config.height),
])
}
}
@@ -1,43 +0,0 @@
//
// SafeAreaExampleViewController.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 30.08.18.
// Copyright © 2018 Felix Mau. All rights reserved.
//
import UIKit
import GradientLoadingBar
class SafeAreaExampleViewController: UIViewController {
// MARK: - Private properties
private let gradientLoadingBar = GradientLoadingBar(isRelativeToSafeArea: false)
private let notchGradientLoadingBar = NotchGradientLoadingBar(isRelativeToSafeArea: false)
// MARK: - Public methods
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
gradientLoadingBar.fadeOut()
notchGradientLoadingBar.fadeOut()
}
@IBAction private func showBasicBarButtonTouchUpInside(_: Any) {
gradientLoadingBar.fadeIn()
}
@IBAction private func hideBasicBarButtonTouchUpInside(_: Any) {
gradientLoadingBar.fadeOut()
}
@IBAction private func showNotchBarButtonTouchUpInside(_: Any) {
notchGradientLoadingBar.fadeIn()
}
@IBAction private func hideNotchBarButtonTouchUpInside(_: Any) {
notchGradientLoadingBar.fadeOut()
}
}
@@ -1,37 +0,0 @@
//
// BlueBorderedButton.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 29.08.18.
// Copyright © 2018 Felix Mau. All rights reserved.
//
import UIKit
@IBDesignable
final class BlueBorderedButton: UIButton {
// MARK: - Initializer
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
// MARK: - Private methods
private func commonInit() {
backgroundColor = .clear
tintColor = UIColor.CustomColors.blue
layer.cornerRadius = 4
layer.borderColor = UIColor.CustomColors.blue.cgColor
layer.borderWidth = 1
}
}
@@ -1,41 +0,0 @@
//
// BlueFilledButton.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 29.08.18.
// Copyright © 2018 Felix Mau. All rights reserved.
//
import UIKit
@IBDesignable
final class BlueFilledButton: UIButton {
// MARK: - Initializer
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
commonInit()
}
// MARK: - Private methods
private func commonInit() {
tintColor = .white
backgroundColor = UIColor.CustomColors.blue
layer.cornerRadius = 4.0
}
}
@@ -1,85 +0,0 @@
//
// LargeNavigationBarHeaderView.swift
// GradientLoadingBar_Example
//
// Created by Felix Mau on 23.09.19.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import UIKit
@IBDesignable
final class LargeNavigationBarHeaderView: UIView {
// MARK: - Public properties
@IBInspectable var title: String = "" {
didSet {
titleLabel.text = title
}
}
@IBInspectable var prompt: String = "" {
didSet {
promptLabel.text = prompt
}
}
// MARK: - Private properties
private let titleLabel = UILabel(frame: .zero)
private let promptLabel = UILabel(frame: .zero)
// MARK: - Initializer
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
// MARK: - Private methods
private func commonInit() {
setupTitleLabel()
setupPromptLabel()
}
private func setupTitleLabel() {
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .largeTitle)
titleLabel.font = UIFont.systemFont(ofSize: fontDescriptor.pointSize, weight: .bold)
titleLabel.textColor = .label
titleLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
}
private func setupPromptLabel() {
promptLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
promptLabel.textAlignment = .center
promptLabel.textColor = .label
promptLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(promptLabel)
NSLayoutConstraint.activate([
promptLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
promptLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
promptLabel.topAnchor.constraint(equalTo: topAnchor, constant: 9.5),
])
}
}
@@ -0,0 +1,379 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
F545079B2F1283B200697C4F /* GradientLoadingBar in Frameworks */ = {isa = PBXBuildFile; productRef = F545079A2F1283B200697C4F /* GradientLoadingBar */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
F54507852F12839600697C4F /* GradientLoadingBarExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GradientLoadingBarExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
F54507872F12839600697C4F /* GradientLoadingBarExample */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = GradientLoadingBarExample;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
F54507822F12839600697C4F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F545079B2F1283B200697C4F /* GradientLoadingBar in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
F545077C2F12839600697C4F = {
isa = PBXGroup;
children = (
F54507872F12839600697C4F /* GradientLoadingBarExample */,
F54507862F12839600697C4F /* Products */,
);
sourceTree = "<group>";
};
F54507862F12839600697C4F /* Products */ = {
isa = PBXGroup;
children = (
F54507852F12839600697C4F /* GradientLoadingBarExample.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
F54507842F12839600697C4F /* GradientLoadingBarExample */ = {
isa = PBXNativeTarget;
buildConfigurationList = F54507902F12839700697C4F /* Build configuration list for PBXNativeTarget "GradientLoadingBarExample" */;
buildPhases = (
F54507812F12839600697C4F /* Sources */,
F54507822F12839600697C4F /* Frameworks */,
F54507832F12839600697C4F /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
F54507872F12839600697C4F /* GradientLoadingBarExample */,
);
name = GradientLoadingBarExample;
packageProductDependencies = (
F545079A2F1283B200697C4F /* GradientLoadingBar */,
);
productName = GradientLoadingBarExample;
productReference = F54507852F12839600697C4F /* GradientLoadingBarExample.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
F545077D2F12839600697C4F /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2620;
LastUpgradeCheck = 2620;
TargetAttributes = {
F54507842F12839600697C4F = {
CreatedOnToolsVersion = 26.2;
};
};
};
buildConfigurationList = F54507802F12839600697C4F /* Build configuration list for PBXProject "GradientLoadingBarExample" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = F545077C2F12839600697C4F;
minimizedProjectReferenceProxies = 1;
packageReferences = (
F54507992F1283B200697C4F /* XCLocalSwiftPackageReference "../../GradientLoadingBar" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = F54507862F12839600697C4F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
F54507842F12839600697C4F /* GradientLoadingBarExample */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
F54507832F12839600697C4F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
F54507812F12839600697C4F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
F545078E2F12839700697C4F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 7AP949PR8S;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
F545078F2F12839700697C4F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 7AP949PR8S;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
F54507912F12839700697C4F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 7AP949PR8S;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.7;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.GradientLoadingBarExample;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
XROS_DEPLOYMENT_TARGET = 26.2;
};
name = Debug;
};
F54507922F12839700697C4F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 7AP949PR8S;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
GENERATE_INFOPLIST_FILE = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.7;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.GradientLoadingBarExample;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
XROS_DEPLOYMENT_TARGET = 26.2;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
F54507802F12839600697C4F /* Build configuration list for PBXProject "GradientLoadingBarExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F545078E2F12839700697C4F /* Debug */,
F545078F2F12839700697C4F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F54507902F12839700697C4F /* Build configuration list for PBXNativeTarget "GradientLoadingBarExample" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F54507912F12839700697C4F /* Debug */,
F54507922F12839700697C4F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
F54507992F1283B200697C4F /* XCLocalSwiftPackageReference "../../GradientLoadingBar" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../../GradientLoadingBar;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
F545079A2F1283B200697C4F /* GradientLoadingBar */ = {
isa = XCSwiftPackageProductDependency;
productName = GradientLoadingBar;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = F545077D2F12839600697C4F /* Project object */;
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,85 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,40 @@
//
// ContentView.swift
// GradientLoadingBarExample
//
// Created by Felix Mau on 10.01.26.
// Copyright © 2026 Felix Mau. All rights reserved.
//
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
Tab("Basic Example", systemImage: SFSymbol.iPhoneGen3.rawValue) {
BasicExampleView()
}
Tab("Notch Example", systemImage: SFSymbol.iPhoneGen2.rawValue) {
NotchExampleView()
}
Tab("UIKit Example", systemImage: SFSymbol.squareStack.rawValue) {
UIKitExampleView()
}
Tab("SwiftUI Example", systemImage: SFSymbol.swift.rawValue) {
SwiftUIExampleView()
}
}
.overlay(alignment: .top) {
Text(" Gradient Loading Bar Example ")
.font(.caption)
.offset(x: 0, y: .space3)
.accessibilityHidden(true)
}
}
}
// MARK: - Preview
#Preview {
ContentView()
}
@@ -0,0 +1,83 @@
//
// BasicExampleView.swift
// GradientLoadingBarExample
//
// Created by Felix Mau on 10.01.26.
// Copyright © 2026 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
/// A basic example view demonstrating the usage of the `GradientLoadingBar`.
struct BasicExampleView: View {
private let relativeToSafeAreaLoadingBar = GradientLoadingBar()
private let ignoringSafeAreaLoadingBar = GradientLoadingBar(isRelativeToSafeArea: false)
var body: some View {
NavigationStack {
VStack(spacing: .space6) {
VStack(spacing: .space2) {
Text("Relative to Safe Area")
.font(.headline)
HStack(spacing: .space2) {
Button {
relativeToSafeAreaLoadingBar.fadeOut()
} label: {
Label("Hide", systemImage: SFSymbol.eyeSlash.rawValue)
.padding(.space1)
}
.buttonStyle(.borderedProminent)
Button {
relativeToSafeAreaLoadingBar.fadeIn()
} label: {
Label("Show", systemImage: SFSymbol.eye.rawValue)
.padding(.space1)
}
.buttonStyle(.borderedProminent)
}
}
VStack(spacing: .space2) {
Text("Ignores Safe Area")
.font(.headline)
HStack(spacing: .space2) {
Button {
ignoringSafeAreaLoadingBar.fadeOut()
} label: {
Label("Hide", systemImage: SFSymbol.eyeSlash.rawValue)
.padding(.space1)
}
.buttonStyle(.borderedProminent)
Button {
ignoringSafeAreaLoadingBar.fadeIn()
} label: {
Label("Show", systemImage: SFSymbol.eye.rawValue)
.padding(.space1)
}
.buttonStyle(.borderedProminent)
}
}
}
.padding(.space6)
.navigationTitle("Basic Example")
}
.onDisappear {
relativeToSafeAreaLoadingBar.fadeOut()
ignoringSafeAreaLoadingBar.fadeOut()
}
}
}
// MARK: - Preview
#Preview {
// Previews do not provide a key window, therefore button interactions do not display the loading bar.
// To view the loading bar in action, run the example application in the simulator.
BasicExampleView()
}
@@ -0,0 +1,58 @@
//
// NotchExampleView.swift
// GradientLoadingBarExample
//
// Created by Felix Mau on 10.01.26.
// Copyright © 2026 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
/// An example view demonstrating the usage of the `NotchGradientLoadingBar`
struct NotchExampleView: View {
private let notchGradientLoadingBar = NotchGradientLoadingBar()
var body: some View {
NavigationStack {
VStack(spacing: .space4) {
HStack(spacing: .space2) {
Button {
notchGradientLoadingBar.fadeOut()
} label: {
Label("Hide", systemImage: SFSymbol.eyeSlash.rawValue)
.padding(.space1)
}
.buttonStyle(.borderedProminent)
Button {
notchGradientLoadingBar.fadeIn()
} label: {
Label("Show", systemImage: SFSymbol.eye.rawValue)
.padding(.space1)
}
.buttonStyle(.borderedProminent)
}
Text("**Note:** In case the device doesn't have a notch, the loading bar will be displayed with a regular layout.")
.font(.footnote)
.multilineTextAlignment(.center)
.padding(.horizontal, .space2)
}
.padding(.space6)
.navigationTitle("Notch Example")
}
.onDisappear {
notchGradientLoadingBar.fadeOut()
}
}
}
// MARK: - Preview
#Preview {
// Previews do not provide a key window, therefore button interactions do not display the loading bar.
// To view the loading bar in action, run the example application in the simulator.
NotchExampleView()
}
@@ -0,0 +1,57 @@
//
// SwiftUIExampleView.swift
// GradientLoadingBarExample
//
// Created by Felix Mau on 10.01.26.
// Copyright © 2026 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
private enum Config {
/// The custom gradient colors we use.
///
/// Source: <https://color.adobe.com/Pink-Flamingo-color-theme-10343714/>
static let gradientColors = [
#colorLiteral(red: 0.9490196078, green: 0.3215686275, blue: 0.431372549, alpha: 1), #colorLiteral(red: 0.9450980392, green: 0.4784313725, blue: 0.5921568627, alpha: 1), #colorLiteral(red: 0.9529411765, green: 0.737254902, blue: 0.7843137255, alpha: 1), #colorLiteral(red: 0.4274509804, green: 0.8666666667, blue: 0.9490196078, alpha: 1), #colorLiteral(red: 0.7568627451, green: 0.9411764706, blue: 0.9568627451, alpha: 1),
].map(Color.init)
}
/// A SwiftUI view demonstrating the usage of the `GradientLoadingBarView`,
/// showcasing both default and custom gradient colors.
struct SwiftUIExampleView: View {
var body: some View {
NavigationStack {
VStack(spacing: .space6) {
VStack(spacing: .space2) {
Text("Default Gradient Colors")
.font(.callout)
GradientLoadingBarView()
.frame(height: 3)
.clipShape(RoundedRectangle(cornerRadius: 1.5))
}
VStack(spacing: .space2) {
Text("Custom Gradient Colors")
.font(.callout)
GradientLoadingBarView(
gradientColors: Config.gradientColors,
)
.frame(height: 3)
.clipShape(RoundedRectangle(cornerRadius: 1.5))
}
}
.padding(.space6)
.navigationTitle("SwiftUI Example")
}
}
}
// MARK: - Preview
#Preview {
SwiftUIExampleView()
}
@@ -0,0 +1,109 @@
//
// UIKitExampleView.swift
// GradientLoadingBarExample
//
// Created by Felix Mau on 10.01.26.
// Copyright © 2026 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
import UIKit
private enum Config {
/// The custom gradient colors we use.
///
/// Source: <https://color.adobe.com/Pink-Flamingo-color-theme-10343714/>
static let gradientColors = [
#colorLiteral(red: 0.9490196078, green: 0.3215686275, blue: 0.431372549, alpha: 1), #colorLiteral(red: 0.9450980392, green: 0.4784313725, blue: 0.5921568627, alpha: 1), #colorLiteral(red: 0.9529411765, green: 0.737254902, blue: 0.7843137255, alpha: 1), #colorLiteral(red: 0.4274509804, green: 0.8666666667, blue: 0.9490196078, alpha: 1), #colorLiteral(red: 0.7568627451, green: 0.9411764706, blue: 0.9568627451, alpha: 1),
]
}
/// A SwiftUI view that demonstrates the usage of `GradientActivityIndicatorView` from UIKit.
struct UIKitExampleView: View {
var body: some View {
NavigationStack {
UIKitExampleViewControllerRepresentable()
.navigationTitle("UIKit Example")
}
}
}
// MARK: - Supporting Types
/// A UIViewControllerRepresentable that wraps the `UIKitExampleViewController`.
private struct UIKitExampleViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context _: Context) -> UIViewController {
UIKitExampleViewController()
}
func updateUIViewController(_: UIViewController, context _: Context) {}
}
/// A UIViewController that showcases the `GradientActivityIndicatorView`
/// with default and custom gradient colors.
private final class UIKitExampleViewController: UIViewController {
// MARK: - Private Properties
private let contentStackView = UIStackView()
private let defaultGradientColorsLabel = UILabel()
private let defaultGradientActivityIndicatorView = GradientActivityIndicatorView()
private let customGradientColorsLabel = UILabel()
private let customGradientActivityIndicatorView = GradientActivityIndicatorView()
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setUpSubviews()
setUpConstraints()
}
// MARK: - Private Methods
private func setUpSubviews() {
contentStackView.axis = .vertical
contentStackView.alignment = .fill
contentStackView.spacing = .space2
contentStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(contentStackView)
defaultGradientColorsLabel.text = "Default Gradient Colors"
defaultGradientColorsLabel.textAlignment = .center
defaultGradientColorsLabel.font = .preferredFont(forTextStyle: .callout)
contentStackView.addArrangedSubview(defaultGradientColorsLabel)
contentStackView.addArrangedSubview(defaultGradientActivityIndicatorView)
contentStackView.setCustomSpacing(.space6, after: defaultGradientActivityIndicatorView)
customGradientColorsLabel.text = "Custom Gradient Colors"
customGradientColorsLabel.textAlignment = .center
customGradientColorsLabel.font = .preferredFont(forTextStyle: .callout)
contentStackView.addArrangedSubview(customGradientColorsLabel)
customGradientActivityIndicatorView.gradientColors = Config.gradientColors
contentStackView.addArrangedSubview(customGradientActivityIndicatorView)
}
private func setUpConstraints() {
NSLayoutConstraint.activate([
contentStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: .space6),
contentStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -.space6),
contentStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
defaultGradientActivityIndicatorView.heightAnchor.constraint(equalToConstant: 3),
customGradientActivityIndicatorView.heightAnchor.constraint(equalToConstant: 3),
])
}
}
// MARK: - Preview
#Preview {
UIKitExampleView()
}
@@ -0,0 +1,18 @@
//
// GradientLoadingBarExampleApp.swift
// GradientLoadingBarExample
//
// Created by Felix Mau on 10.01.26.
// Copyright © 2026 Felix Mau. All rights reserved.
//
import SwiftUI
@main
struct GradientLoadingBarExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@@ -0,0 +1,36 @@
//
// CGFloat+Spacing.swift
// GradientLoadingBarExample
//
// Created by Felix Mau on 10.01.26.
// Copyright © 2026 Felix Mau. All rights reserved.
//
import CoreGraphics
/// Standard spacing values based on a 4-point grid.
///
/// These constants provide a shared spacing scale to ensure visual consistency across layouts.
/// Values are expressed in points and increase in 4-point increments.
///
/// Naming follows the pattern `spaceN`, where `N` represents the number of 4-point units (e.g. `space3` = 12 points).
///
/// ### Usage
///
/// ```swift
/// VStack(spacing: .space4) {
/// Text("Title")
/// Text("Subtitle")
/// }
/// ```
extension CGFloat {
static let space0: CGFloat = 0
static let space1: CGFloat = 4
static let space2: CGFloat = 8
static let space3: CGFloat = 12
static let space4: CGFloat = 16
static let space5: CGFloat = 20
static let space6: CGFloat = 24
static let space8: CGFloat = 32
static let space10: CGFloat = 40
}
@@ -0,0 +1,58 @@
//
// SFSymbol.swift
// GradientLoadingBarExample
//
// Created by Felix Mau on 10.01.26.
// Copyright © 2026 Felix Mau. All rights reserved.
//
import SwiftUI
import UIKit
/// SF Symbols used by the example application.
///
/// Using an enum avoids hard-coded string literals and improves discoverability, autocompletion and compile-time safety.
/// The raw value of each case corresponds to the SF Symbols system name.
enum SFSymbol: String {
case eye
case eyeSlash = "eye.slash"
case iPhoneGen2 = "iphone.gen2"
case iPhoneGen3 = "iphone.gen3"
case squareStack = "square.stack"
case swift
}
// MARK: - Helper
extension Image {
/// Creates a SwiftUI `Image` from an `SFSymbol`.
///
/// ### Usage
///
/// ```swift
/// Image(sfSymbol: .house)
/// ```
///
/// - Parameter sfSymbol: The SF Symbol to display.
init(sfSymbol: SFSymbol) {
self.init(systemName: sfSymbol.rawValue)
}
}
extension UIImage {
/// Creates a `UIImage` from an `SFSymbol`.
///
/// ### Usage
///
/// ```swift
/// let image = UIImage(sfSymbol: .plus)
/// ```
///
/// - Parameter sfSymbol: The SF Symbol to display.
///
/// - Returns: A system image, or `nil` if the symbol name is invalid or
/// unavailable on the current OS version.
convenience init?(sfSymbol: SFSymbol) {
self.init(systemName: sfSymbol.rawValue)
}
}
-49
View File
@@ -1,49 +0,0 @@
# Fixes warning `Your project does not explicitly specify the CocoaPods master specs repo`.
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '13.0'
use_frameworks!
target 'GradientLoadingBar_Example' do
pod 'GradientLoadingBar', :path => '../'
# Development pods.
pod 'SwiftFormat/CLI', '~> 0.41'
pod 'SwiftLint', '~> 0.42'
pod 'SwiftConfigurationFiles', :git => 'https://github.com/fxm90/SwiftConfigurationFiles.git'
target 'GradientLoadingBar_Tests' do
inherit! :search_paths
# Pods for testing.
end
target 'GradientLoadingBar_SnapshotTests' do
inherit! :search_paths
# Pods for snapshot testing.
pod 'SnapshotTesting', '~> 1.7', :inhibit_warnings => true
end
post_install do |installer|
# Fix issues with interface builder and cocoapods.
# Source: https://github.com/CocoaPods/CocoaPods/issues/7606#issuecomment-484294739
installer.pods_project.build_configurations.each do |config|
next unless config.name == 'Debug'
config.build_settings['LD_RUNPATH_SEARCH_PATHS'] = [
'$(FRAMEWORK_SEARCH_PATHS)'
]
end
# We support all iOS versions starting from 9.0. Therefore we manually decrease the deployment target for `GradientLoadingBar` here,
# so other (development / testing related pods) can use a higher deployment target, which is defined using `platform :ios` above.
installer.pods_project.targets.each do |target|
next unless target.name == 'GradientLoadingBar'
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
end
end
end
end
-45
View File
@@ -1,45 +0,0 @@
PODS:
- GradientLoadingBar (2.2.2):
- LightweightObservable (~> 2.1)
- LightweightObservable (2.1.2)
- SnapshotTesting (1.9.0)
- SwiftConfigurationFiles (0.0.1)
- SwiftFormat/CLI (0.48.13)
- SwiftLint (0.44.0)
DEPENDENCIES:
- GradientLoadingBar (from `../`)
- SnapshotTesting (~> 1.7)
- SwiftConfigurationFiles (from `https://github.com/fxm90/SwiftConfigurationFiles.git`)
- SwiftFormat/CLI (~> 0.41)
- SwiftLint (~> 0.42)
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- LightweightObservable
- SnapshotTesting
- SwiftFormat
- SwiftLint
EXTERNAL SOURCES:
GradientLoadingBar:
:path: "../"
SwiftConfigurationFiles:
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
CHECKOUT OPTIONS:
SwiftConfigurationFiles:
:commit: f6dea18948c1b667043964c2539b9008d2e6beed
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
SPEC CHECKSUMS:
GradientLoadingBar: 1592b9e0825057fe100a6364b6af6b19556825ab
LightweightObservable: c5ac85423a5edbed9a920b4d5c7b8f94ab3688c4
SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b
SwiftConfigurationFiles: 1cf2228a911ebed9f42f8dec077bb634f04ca6c8
SwiftFormat: 0c4b53c1acb503e2a743ecd9a18f51958d41d40a
SwiftLint: e96c0a8c770c7ebbc4d36c55baf9096bb65c4584
PODFILE CHECKSUM: 36b90780cb508f9eaca4f0ac59660e3e748de601
COCOAPODS: 1.10.1
-19
View File
@@ -1,19 +0,0 @@
Copyright (c) 2019-2020 Felix Mau <me@felix.hamburg>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -1,46 +0,0 @@
//
// Disposable.swift
// LightweightObservable
//
// Created by Felix Mau on 11.02.19.
// Copyright © 2019 Felix Mau. All rights reserved.
//
import Foundation
/// Helper to allow storing multiple disposables and matching name from [RxSwift](https://github.com/ReactiveX/RxSwift).
public typealias DisposeBag = [Disposable]
/// Executes a given closure on it's deallocation.
public final class Disposable {
// MARK: - Types
/// Type for closure to be executed on deallocation.
public typealias Dispose = () -> Void
// MARK: - Private properties
/// Closure to be executed on deallocation.
private let dispose: Dispose
// MARK: - Initializer
/// Creates a new instance.
///
/// - Parameter dispose: The closure that is executed on deallocation.
public init(_ dispose: @escaping Dispose) {
self.dispose = dispose
}
/// Executes our closure.
deinit {
dispose()
}
// MARK: - Public methods
/// Adds the current instance to the given array of disposables.
public func disposed(by bag: inout DisposeBag) {
bag.append(self)
}
}
@@ -1,62 +0,0 @@
//
// Observable+Equatable.swift
// LightweightObservable
//
// Created by Felix Mau on 01.05.19.
// Copyright © 2019 Felix Mau. All rights reserved.
//
import Foundation
/// Additional helper methods for an observable that that underlying type conforms to `Equatable`.
public extension Observable where T: Equatable {
// MARK: - Types
/// The type for the filter closure.
///
/// - Parameters:
/// - value: The current (new) value.
/// - oldValue: The previous (old) value.
///
/// - Returns: `true` if the filter matches, otherwise `false`.
typealias Filter = (_ value: Value, _ oldValue: OldValue) -> Bool
// MARK: - Public methods
/// Informs the given observer on changes to our `value`, only if the given filter matches.
///
/// - Parameters:
/// - filter: The closure, that must return `true` in order for the observer to be notified.
/// - observer: The closure that is notified on changes if the filter succeeds.
///
/// - Note:
/// The `oldValue` of the observer always contains the previous value that passed the filter! E.g. if you filter for even numbers:
/// ```
/// | Emitted values: | 0 | 1 | 2 | 3 | 4 | 5 |
/// | -------------------------- | -------- | -------- | -------- | -------- | -------- | -------- |
/// | filter(value, oldValue): | (0, nil) | (1, 0) | (2, 1) | (3, 2) | (4, 3) | (5, 4) |
/// | result: | true | false | true | false | true | false |
/// | observer(value, oldValue): | (0, nil) | | (2, 0) | | (4, 2) | |
/// ```
func subscribe(filter: @escaping Filter, observer: @escaping Observer) -> Disposable {
// As we're only calling the observer after the given `filter` succeeds, we need to save the last `newValue` that passed the filter.
// Then we can pass this value as the correct `oldValue` on the next call to the given `observer`.
var filteredOldValue: OldValue = nil
return subscribe { newValue, oldValue in
// For having a correct working filter we need to pass in the current `oldValue`.
if filter(newValue, oldValue) {
observer(newValue, filteredOldValue)
filteredOldValue = newValue
}
}
}
/// Informs the given observer on **distinct** changes to our `value`.
///
/// - Parameter observer: The closure that is notified on changes.
func subscribeDistinct(_ observer: @escaping Observer) -> Disposable {
subscribe(filter: { $0 != $1 },
observer: observer)
}
}
@@ -1,37 +0,0 @@
//
// Observable+KeyPath.swift
// LightweightObservable
//
// Created by Felix Mau on 03.01.20.
// Copyright © 2020 Felix Mau. All rights reserved.
//
import Foundation
/// Additional helper methods for binding an observable to a property using Swifts `KeyPath` feature.
public extension Observable {
/// Updates the property at the given key-path on changes to our property `value`.
///
/// - Parameters:
/// - keyPath: The key-path that indicates the property to assign.
/// - object: The object containing the property to update.
func bind<Root: AnyObject>(to keyPath: ReferenceWritableKeyPath<Root, Value>, on object: Root) -> Disposable {
subscribe { newValue, _ in
object[keyPath: keyPath] = newValue
}
}
/// Updates the property at the given key-path on changes to our property `value`.
///
/// - Parameters:
/// - keyPath: The key-path that indicates the property to assign.
/// - object: The object containing the property to update.
///
/// - Note: We need to explicitly define this method for an optional type of `Value`, as otherwise we e.g. could not bind a `String` to the
/// optional string property `text` of an `UILabel`
func bind<Root>(to keyPath: ReferenceWritableKeyPath<Root, Value?>, on object: Root) -> Disposable {
subscribe { newValue, _ in
object[keyPath: keyPath] = newValue
}
}
}
@@ -1,167 +0,0 @@
//
// Observable.swift
// LightweightObservable
//
// Created by Felix Mau on 11.02.19.
// Copyright © 2019 Felix Mau. All rights reserved.
//
import Foundation
/// A lightweight implementation of an observable sequence that you can subscribe to.
///
/// - Note: Partly based on [roberthein/Observable](https://github.com/roberthein/Observable).
///
/// - Remark: I'd prefer having a protocol definition here, but casting an instance with a generic (e.g. `Variable<Int>(0)`) to a protocol
/// with an associated type (`Observable<Int>`) doesn't work yet. Therefore we use an "abstract" class as a workaround.
public class Observable<T> {
// MARK: - Types
/// The type for the new value of the observable.
public typealias Value = T
/// The type for the previous value of the observable.
public typealias OldValue = T?
/// The type for the closure to executed on change of the observable.
///
/// - Parameters:
/// - value: The current (new) value.
/// - oldValue: The previous (old) value.
public typealias Observer = (_ value: Value, _ oldValue: OldValue) -> Void
/// We store all observers within a dictionary, for which this is the type of the key.
private typealias Index = UInt
// MARK: - Public properties
/// The current (readonly) value of the observable (if available).
///
/// - Note: We're using a computed property here, cause we need to override this property without nullability in the subclass `Variable`.
///
/// - Attention: It's always better to subscribe to a given observable! This **shortcut** should only be used during **testing**.
public var value: Value? {
fatalError("⚠️ Trying to access an abstract property! Subclasses need to overwrite and implement this computed property.")
}
// MARK: - Private properties
/// The index of the last inserted observer.
private var lastIndex: Index = 0
/// Map with all **active** observers.
private var observers = [Index: Observer]()
// MARK: - Initalizer
/// Initializes a new observable.
///
/// - Note: Declared `fileprivate` in order to prevent directly initializing an observable, that can't emit values.
fileprivate init() {}
// MARK: - Public methods
/// Informs the given observer on changes to our underlying property `value`.
///
/// - Parameter observer: The closure that is notified on changes.
public func subscribe(_ observer: @escaping Observer) -> Disposable {
let indexForNewObserver = lastIndex + 1
observers[indexForNewObserver] = observer
lastIndex = indexForNewObserver
// Return a disposable, that removes the entry for this observer on it's deallocation.
return Disposable { [weak self] in
self?.observers[indexForNewObserver] = nil
}
}
// MARK: - Private methods
fileprivate func notifyObserver(with value: Value, from oldValue: OldValue) {
observers.values.forEach { observer in
observer(value, oldValue)
}
}
}
/// Starts empty and only emits new elements to subscribers.
public final class PublishSubject<T>: Observable<T> {
// MARK: - Public properties
/// The current (readonly) value of the observable (if available).
override public var value: Value? {
_value
}
// MARK: - Private properties
/// The storage for our computed property.
///
/// - Note: Workaround for compiler error `Cannot override with a stored property 'value'`.
private var _value: Value?
// MARK: - Initializer
/// Initializes a new publish subject.
///
/// - Note: As we've made the initializer to the super class `Observable` fileprivate, we must override it here to allow public access.
override public init() {
super.init()
}
// MARK: - Public methods
/// Updates the publish subject using the given value.
public func update(_ value: Value) {
let oldValue = _value
_value = value
// We inform the observer here instead of using `didSet` on `_value` to prevent unwrapping an optional (`_value` is nullable, as we're starting empty).
// Unwrapping lead to issues / crashes on having an underlying optional type, e.g. `PublishSubject<Int?>`.
notifyObserver(with: value, from: oldValue)
}
}
/// Starts with an initial value and replays it or the latest element to new subscribers.
public final class Variable<T>: Observable<T> {
// MARK: - Public properties
/// The current (read- and writeable) value of the variable.
override public var value: Value {
get { _value }
set { _value = newValue }
}
// MARK: - Private properties
/// The storage for our computed property.
///
/// - Note: Workaround for compiler error `Cannot override with a stored property 'value'`.
private var _value: Value {
didSet {
notifyObserver(with: value, from: oldValue)
}
}
// MARK: - Initializer
/// Initializes a new variable with the given value.
///
/// - Note: We keep the initializer to the super class `Observable` fileprivate in order to verify always having a value.
public init(_ value: Value) {
_value = value
super.init()
}
// MARK: - Public methods
override public func subscribe(_ observer: @escaping Observer) -> Disposable {
let disposable = super.subscribe(observer)
// A variable should inform the observer with the initial value.
observer(_value, nil)
return disposable
}
}
-294
View File
@@ -1,294 +0,0 @@
![Header][header]
<p align="center">
<img src="https://img.shields.io/badge/Swift-5.0-green.svg?style=flat" alt="Swift Version" />
<img src="http://img.shields.io/travis/com/fxm90/LightweightObservable.svg?style=flat" alt="CI Status" />
<img src="https://img.shields.io/codecov/c/github/fxm90/LightweightObservable.svg?style=flat" alt="Code Coverage" />
<img src="https://img.shields.io/cocoapods/v/LightweightObservable.svg?style=flat" alt="Version" />
<img src="https://img.shields.io/cocoapods/l/LightweightObservable.svg?style=flat" alt="License" />
<img src="https://img.shields.io/cocoapods/p/LightweightObservable.svg?style=flat" alt="Platform" />
</p>
## Features
Lightweight Observable is a simple implementation of an observable sequence that you can subscribe to. The framework is designed to be minimal meanwhile convenient. The entire code is only ~100 lines (excluding comments). With Lightweight Observable you can easily set up UI-Bindings in an MVVM application, handle asynchronous network calls and a lot more.
##### Credits
The code was heavily influenced by [roberthein/observable](https://github.com/roberthein/Observable). However I needed something that was syntactically closer to [RxSwift](https://github.com/ReactiveX/RxSwift), which is why I came up with this code, and for re-usability reasons afterwards moved it into a CocoaPod.
##### Migration Guide
If you want to update from version 1.x.x, please have a look at the [Lightweight Observable 2.0 Migration Guide
](Documentation/Lightweight%20Observable%202.0%20Migration%20Guide.md)
### Example
To run the example project, clone the repo, and open the workspace from the Example directory.
### Requirements
- Swift 5.0
- Xcode 10.2+
- iOS 9.0+
### Integration
##### CocoaPods
[CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Lightweight Observable into your Xcode project using CocoaPods, specify it in your `Podfile`:
```ruby
pod 'LightweightObservable', '~> 2.0'
```
##### Carthage
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate Lightweight Observable into your Xcode project using Carthage, specify it in your `Cartfile`:
```ogdl
github "fxm90/LightweightObservable" ~> 2.0
```
Run carthage update to build the framework and drag the built `LightweightObservable.framework` into your Xcode project.
##### Swift Package Manager
The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Lightweight Observable does support its use on supported platforms.
Once you have your Swift package set up, adding Lightweight Observable as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
```swift
dependencies: [
.package(url: "https://github.com/fxm90/LightweightObservable", from: "2.0.0")
]
```
### How to use
The framework provides three classes `Observable`, `PublishSubject` and `Variable`:
- `Observable`: An observable sequence that you can subscribe to, but not change the underlying value (immutable). This is useful to avoid side-effects on an internal API.
- `PublishSubject`: Subclass of `Observable` that starts empty and only emits new elements to subscribers (mutable).
- `Variable`: Subclass of `Observable` that starts with an initial value and replays it or the latest element to new subscribers (mutable).
#### Create and update a `PublishSubject`
A `PublishSubject` starts empty and only emits new elements to subscribers.
```swift
let userLocationSubject = PublishSubject<CLLocation>()
// ...
userLocationSubject.update(receivedUserLocation)
```
#### Create and update a `Variable`
A `Variable` starts with an initial value and replays it or the latest element to new subscribers.
```swift
let formattedTimeSubject = Variable("4:20 PM")
// ...
formattedTimeSubject.value = "4:21 PM"
```
#### Create an `Observable`
Initializing an observable directly is not possible, as this would lead to a sequence that will never change. Instead you need to cast a `PublishSubject` or a `Variable` to an Observable.
```swift
var formattedTime: Observable<String> {
formattedTimeSubject
}
```
```swift
lazy var formattedTime: Observable<String> = formattedTimeSubject
```
#### Subscribe to changes
A subscriber will be informed at different times, depending on the corresponding subclass of the observable:
- `PublishSubject`: Starts empty and only emits new elements to subscribers.
- `Variable`: Starts with an initial value and replays it or the latest element to new subscribers.
##### Closure based subscription
**Declaration**
```swift
func subscribe(_ observer: @escaping Observer) -> Disposable
```
Use this method to subscribe to an observable via a closure:
```swift
formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}
```
Please notice that the old value (`oldFormattedTime`) is an optional of the underlying type, as we might not have this value on the initial call to the subscriber.
**Important:** To avoid retain cycles and/or crashes, **always** use `[weak self]` when self is needed by an observer.
##### - KeyPath based subscription
**Declaration**
```swift
func bind<Root: AnyObject>(to keyPath: ReferenceWritableKeyPath<Root, Value>, on object: Root) -> Disposable
```
It is also possible to use Swift's KeyPath feature to bind an observable directly to a property:
```swift
formattedTime.bind(to: \.text, on: timeLabel)
```
#### Memory Management (`Disposable` / `DisposeBag`)
When you subscribe to an `Observable` the method returns a `Disposable`, which is basically a reference to the new subscription.
We need to maintain it, in order to properly control the life-cycle of that subscription.
Let me explain you why in a little example:
> Imagine having a MVVM application using a service layer for network calls. A service is used as a singleton across the entire app.
>
> The view-model has a reference to a service and subscribes to an observable property of this service. The subscription-closure is now saved inside the observable property on the service.
>
> If the view-model gets deallocated (e.g. due to a dismissed view-controller), without noticing the observable property somehow, the subscription-closure would continue to be alive.
>
> As a workaround, we store the returned disposable from the subscription on the view-model. On deallocation of the disposable, it automatically informs the observable property to remove the referenced subscription closure.
In case you only use a single subscriber you can store the returned `Disposable` to a variable:
```swift
// MARK: - Using `subscribe(_:)`
let disposable = formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}
// MARK: - Using a `bind(to:on:)`
let disposable = dateTimeViewModel
.formattedTime
.bind(to: \.text, on: timeLabel)
```
In case you're having multiple observers, you can store all returned `Disposable` in an array of `Disposable`. (To match the syntax from [RxSwift](https://github.com/ReactiveX/RxSwift), this pod contains a typealias called `DisposeBag`, which is an array of `Disposable`).
```swift
var disposeBag = DisposeBag()
// MARK: - Using `subscribe(_:)`
formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}.disposed(by: &disposeBag)
formattedDate.subscribe { [weak self] newFormattedDate, oldFormattedDate in
self?.dateLabel.text = newFormattedDate
}.disposed(by: &disposeBag)
// MARK: - Using a `bind(to:on:)`
formattedTime
.bind(to: \.text, on: timeLabel)
.disposed(by: &disposeBag)
formattedDate
.bind(to: \.text, on: dateLabel)
.disposed(by: &disposeBag)
```
A `DisposeBag` is exactly what it says it is, a bag (or array) of disposables.
#### Observing `Equatable` values
If you create an Observable which underlying type conforms to `Equatable` you can subscribe to changes using a specific filter. Therefore this pod contains the method:
```swift
typealias Filter = (NewValue, OldValue) -> Bool
func subscribe(filter: @escaping Filter, observer: @escaping Observer) -> Disposable {}
```
Using this method, the observer will only be notified on changes if the corresponding filter matches.
This pod comes with one predefined filter method, called `subscribeDistinct`. Subscribing to an observable using this method, will only notify the observer if the new value is different from the old value. This is useful to prevent unnecessary UI-Updates.
Feel free to add more filters, by extending the `Observable` like this:
```swift
extension Observable where T: Equatable {}
```
#### Getting the current value synchronously
You can get the current value of the `Observable` by accessing the property `value`. However it is always better to subscribe to a given observable! This **shortcut** should only be used during **testing**.
```swift
XCTAssertEqual(viewModel.formattedTime.value, "4:20")
```
### Sample code
Using the given approach, your view-model could look like this:
```swift
class TimeViewModel {
// MARK: - Public properties
/// The current time as a formatted string (**immutable**).
var formattedTime: Observable<String> {
formattedTimeSubject
}
// MARK: - Private properties
/// The current time as a formatted string (**mutable**).
private let formattedTimeSubject: Variable<String> = Variable("\(Date())")
private var timer: Timer?
// MARK: - Initializer
init() {
// Update variable with current time every second.
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] _ in
self?.formattedTimeSubject.value = "\(Date())"
})
}
```
And your view controller like this:
```swift
class TimeViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var timeLabel: UILabel!
// MARK: - Private properties
/// The view model calculating the current time.
private let timeViewModel = TimeViewModel()
/// The dispose bag for this view controller. On it's deallocation, it removes the
/// subscription-closures from the corresponding observable-properties.
private var disposeBag = DisposeBag()
// MARK: - Public methods
override func viewDidLoad() {
super.viewDidLoad()
timeViewModel
.formattedTime
.bind(to: \.text, on: timeLabel)
.disposed(by: &disposeBag)
}
```
Feel free to check out the example application as well for a better understanding of this approach 🙂
### Author
Felix Mau (me(@)felix.hamburg)
### License
LightweightObservable is available under the MIT license. See the LICENSE file for more info.
[header]: Assets/header.png
@@ -1,30 +0,0 @@
{
"name": "GradientLoadingBar",
"version": "2.2.2",
"summary": "A customizable animated gradient loading bar.",
"description": "A customizable animated gradient loading bar.\nInspired by https://codepen.io/marcobiedermann/pen/LExXWW",
"homepage": "https://github.com/fxm90/GradientLoadingBar",
"screenshots": "https://raw.githubusercontent.com/fxm90/GradientLoadingBar/master/Assets/screen.gif",
"license": {
"type": "MIT",
"file": "LICENSE"
},
"authors": {
"Felix Mau": "contact@felix.hamburg"
},
"source": {
"git": "https://github.com/fxm90/GradientLoadingBar.git",
"tag": "2.2.2"
},
"swift_versions": "5.1",
"platforms": {
"ios": "9.0"
},
"source_files": "GradientLoadingBar/Classes/**/*",
"dependencies": {
"LightweightObservable": [
"~> 2.1"
]
},
"swift_version": "5.1"
}
@@ -1,25 +0,0 @@
{
"name": "SwiftConfigurationFiles",
"version": "0.0.1",
"summary": "Repository containing configuration files for Swift development, e.g. for tools like SwiftLint or SwiftFormat.",
"homepage": "https://github.com/fxm90/Swift-Configuration-Files",
"license": {
"type": "MIT",
"file": "LICENSE"
},
"authors": {
"Felix Mau": "me@felix.hamburg"
},
"source": {
"git": "https://github.com/fxm90/Swift-Configuration-Files.git",
"tag": "0.0.1"
},
"social_media_url": "https://twitter.com/_fxm90",
"preserve_paths": ".*",
"platforms": {
"osx": null,
"ios": null,
"tvos": null,
"watchos": null
}
}
-45
View File
@@ -1,45 +0,0 @@
PODS:
- GradientLoadingBar (2.2.2):
- LightweightObservable (~> 2.1)
- LightweightObservable (2.1.2)
- SnapshotTesting (1.9.0)
- SwiftConfigurationFiles (0.0.1)
- SwiftFormat/CLI (0.48.13)
- SwiftLint (0.44.0)
DEPENDENCIES:
- GradientLoadingBar (from `../`)
- SnapshotTesting (~> 1.7)
- SwiftConfigurationFiles (from `https://github.com/fxm90/SwiftConfigurationFiles.git`)
- SwiftFormat/CLI (~> 0.41)
- SwiftLint (~> 0.42)
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- LightweightObservable
- SnapshotTesting
- SwiftFormat
- SwiftLint
EXTERNAL SOURCES:
GradientLoadingBar:
:path: "../"
SwiftConfigurationFiles:
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
CHECKOUT OPTIONS:
SwiftConfigurationFiles:
:commit: f6dea18948c1b667043964c2539b9008d2e6beed
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
SPEC CHECKSUMS:
GradientLoadingBar: 1592b9e0825057fe100a6364b6af6b19556825ab
LightweightObservable: c5ac85423a5edbed9a920b4d5c7b8f94ab3688c4
SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b
SwiftConfigurationFiles: 1cf2228a911ebed9f42f8dec077bb634f04ca6c8
SwiftFormat: 0c4b53c1acb503e2a743ecd9a18f51958d41d40a
SwiftLint: e96c0a8c770c7ebbc4d36c55baf9096bb65c4584
PODFILE CHECKSUM: 36b90780cb508f9eaca4f0ac59660e3e748de601
COCOAPODS: 1.10.1
File diff suppressed because it is too large Load Diff
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A29550F398D14038E3DD88D91573885C"
BuildableName = "GradientLoadingBar.framework"
BlueprintName = "GradientLoadingBar"
ReferencedContainer = "container:Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A29550F398D14038E3DD88D91573885C"
BuildableName = "GradientLoadingBar.framework"
BlueprintName = "GradientLoadingBar"
ReferencedContainer = "container:Pods.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Point-Free, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-244
View File
@@ -1,244 +0,0 @@
# 📸 SnapshotTesting
[![Swift 5.1](https://img.shields.io/badge/swift-5.1-ED523F.svg?style=flat)](https://swift.org/download/)
[![CI](https://github.com/pointfreeco/swift-snapshot-testing/workflows/CI/badge.svg)](https://actions-badge.atrox.dev/pointfreeco/swift-snapshot-testing/goto)
[![@pointfreeco](https://img.shields.io/badge/contact-@pointfreeco-5AA9E7.svg?style=flat)](https://twitter.com/pointfreeco)
Delightful Swift snapshot testing.
<!--
![An example of a snapshot failure in Xcode.](.github/snapshot-test-1.png)
-->
## Usage
Once [installed](#installation), _no additional configuration is required_. You can import the `SnapshotTesting` module and call the `assertSnapshot` function.
``` swift
import SnapshotTesting
import XCTest
class MyViewControllerTests: XCTestCase {
func testMyViewController() {
let vc = MyViewController()
assertSnapshot(matching: vc, as: .image)
}
}
```
When an assertion first runs, a snapshot is automatically recorded to disk and the test will fail, printing out the file path of any newly-recorded reference.
> 🛑 failed - No reference was found on disk. Automatically recorded snapshot: …
>
> open "…/MyAppTests/\_\_Snapshots\_\_/MyViewControllerTests/testMyViewController.png"
>
> Re-run "testMyViewController" to test against the newly-recorded snapshot.
Repeat test runs will load this reference and compare it with the runtime value. If they don't match, the test will fail and describe the difference. Failures can be inspected from Xcode's Report Navigator or by inspecting the file URLs of the failure.
You can record a new reference by setting the `record` parameter to `true` on the assertion or setting `isRecording` globally.
``` swift
assertSnapshot(matching: vc, as: .image, record: true)
// or globally
isRecording = true
assertSnapshot(matching: vc, as: .image)
```
## Snapshot Anything
While most snapshot testing libraries in the Swift community are limited to `UIImage`s of `UIView`s, SnapshotTesting can work with _any_ format of _any_ value on _any_ Swift platform!
The `assertSnapshot` function accepts a value and any snapshot strategy that value supports. This means that a [view](Documentation/Available-Snapshot-Strategies.md#uiview) or [view controller](Documentation/Available-Snapshot-Strategies.md#uiviewcontroller) can be tested against an image representation _and_ against a textual representation of its properties and subview hierarchy.
``` swift
assertSnapshot(matching: vc, as: .image)
assertSnapshot(matching: vc, as: .recursiveDescription)
```
View testing is [highly configurable](Documentation/Available-Snapshot-Strategies.md#uiviewcontroller). You can override trait collections (for specific size classes and content size categories) and generate device-agnostic snapshots, all from a single simulator.
``` swift
assertSnapshot(matching: vc, as: .image(on: .iPhoneSe))
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneSe))
assertSnapshot(matching: vc, as: .image(on: .iPhoneSe(.landscape)))
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneSe(.landscape)))
assertSnapshot(matching: vc, as: .image(on: .iPhoneX))
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneX))
assertSnapshot(matching: vc, as: .image(on: .iPadMini(.portrait)))
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPadMini(.portrait)))
```
> ⚠️ Warning: Snapshots must be compared using a simulator with the same OS, device gamut, and scale as the simulator that originally took the reference to avoid discrepancies between images.
Better yet, SnapshotTesting isn't limited to views and view controllers! There are [a number of available snapshot strategies](Documentation/Available-Snapshot-Strategies.md) to choose from.
For example, you can snapshot test URL requests (_e.g._, those that your API client prepares).
``` swift
assertSnapshot(matching: urlRequest, as: .raw)
// POST http://localhost:8080/account
// Cookie: pf_session={"userId":"1"}
//
// email=blob%40pointfree.co&name=Blob
```
And you can snapshot test `Encodable` values against their JSON _and_ property list representations.
``` swift
assertSnapshot(matching: user, as: .json)
// {
// "bio" : "Blobbed around the world.",
// "id" : 1,
// "name" : "Blobby"
// }
assertSnapshot(matching: user, as: .plist)
// <?xml version="1.0" encoding="UTF-8"?>
// <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
// <plist version="1.0">
// <dict>
// <key>bio</key>
// <string>Blobbed around the world.</string>
// <key>id</key>
// <integer>1</integer>
// <key>name</key>
// <string>Blobby</string>
// </dict>
// </plist>
```
In fact, _[any](Documentation/Available-Snapshot-Strategies.md#any)_ value can be snapshot-tested by default using its [mirror](https://developer.apple.com/documentation/swift/mirror)!
``` swift
assertSnapshot(matching: user, as: .dump)
// ▿ User
// - bio: "Blobbed around the world."
// - id: 1
// - name: "Blobby"
```
If your data can be represented as an image, text, or data, you can write a snapshot test for it! Check out [all of the snapshot strategies](Documentation/Available-Snapshot-Strategies.md) that ship with SnapshotTesting and [learn how to define your own custom strategies](Documentation/Defining-Custom-Snapshot-Strategies.md).
## Installation
### Xcode 11
> ⚠️ Warning: By default, Xcode will try to add the SnapshotTesting package to your project's main application/framework target. Please ensure that SnapshotTesting is added to a _test_ target instead, as documented in the last step, below.
1. From the **File** menu, navigate through **Swift Packages** and select **Add Package Dependency…**.
2. Enter package repository URL: `https://github.com/pointfreeco/swift-snapshot-testing.git`
3. Confirm the version and let Xcode resolve the package
4. On the final dialog, update SnapshotTesting's **Add to Target** column to a test target that will contain snapshot tests (if you have more than one test target, you can later add SnapshotTesting to them by manually linking the library in its build phase)
### Swift Package Manager
If you want to use SnapshotTesting in any other project that uses [SwiftPM](https://swift.org/package-manager/), add the package as a dependency in `Package.swift`:
```swift
dependencies: [
.package(name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.8.1"),
]
```
Next, add `SnapshotTesting` as a dependency of your test target:
```swift
targets: [
.target(name: "MyApp", dependencies: [], path: "Sources"),
.testTarget(name: "MyAppTests", dependencies: ["MyApp", "SnapshotTesting"])
]
```
### Carthage
If you use [Carthage](https://github.com/Carthage/Carthage), you can add the following dependency to your `Cartfile`:
``` ruby
github "pointfreeco/swift-snapshot-testing" ~> 1.8.0
```
> ⚠️ Warning: Carthage instructs you to drag frameworks into your Xcode project. Xcode may automatically attempt to link these frameworks to your app target. `SnapshotTesting.framework` is only compatible with test targets, so when you first add it to your project:
>
> 1. Remove `SnapshotTesting.framework` from any non-test target it may have been added to.
> 2. Add `SnapshotTesting.framework` to any applicable test targets.
> 3. Add a **New Copy Build Phase** to any applicable test targets with **Destination** set to "Frameworks", and add `SnapshotTesting.framework` as an item to this phase.
> 4. Do _not_ add `SnapshotTesting.framework` to the "Input Files" or "Output Files" of your app target's Carthage `copy-frameworks` **Run Script Phase**.
>
> See Carthage's "[Adding frameworks to unit tests or a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-unit-tests-or-a-framework)" documentation for more.
### CocoaPods
If your project uses [CocoaPods](https://cocoapods.org), add the pod to any applicable test targets in your `Podfile`:
```ruby
target 'MyAppTests' do
pod 'SnapshotTesting', '~> 1.8.1'
end
```
## Features
- [**Dozens of snapshot strategies**](Documentation/Available-Snapshot-Strategies.md). Snapshot testing isn't just for `UIView`s and `CALayer`s. Write snapshots against _any_ value.
- [**Write your own snapshot strategies**](Documentation/Defining-Custom-Snapshot-Strategies.md). If you can convert it to an image, string, data, or your own diffable format, you can snapshot test it! Build your own snapshot strategies from scratch or transform existing ones.
- **No configuration required.** Don't fuss with scheme settings and environment variables. Snapshots are automatically saved alongside your tests.
- **More hands-off.** New snapshots are recorded whether `isRecording` mode is `true` or not.
- **Subclass-free.** Assert from any XCTest case or Quick spec.
- **Device-agnostic snapshots.** Render views and view controllers for specific devices and trait collections from a single simulator.
- **First-class Xcode support.** Image differences are captured as XCTest attachments. Text differences are rendered in inline error messages.
- **Supports any platform that supports Swift.** Write snapshot tests for iOS, Linux, macOS, and tvOS.
- **SceneKit, SpriteKit, and WebKit support.** Most snapshot testing libraries don't support these view subclasses.
- **`Codable` support**. Snapshot encodable data structures into their [JSON](Documentation/Available-Snapshot-Strategies.md#json) and [property list](Documentation/Available-Snapshot-Strategies.md#plist) representations.
- **Custom diff tool integration**.
## Plug-ins
- [swift-snapshot-testing-nimble](https://github.com/Killectro/swift-snapshot-testing-nimble) adds [Nimble](https://github.com/Quick/Nimble) matchers for SnapshotTesting.
- [swift-html](https://github.com/pointfreeco/swift-html) is a Swift DSL for type-safe, extensible, and transformable HTML documents and includes an `HtmlSnapshotTesting` module to snapshot test its HTML documents.
- [GRDBSnapshotTesting](https://github.com/SebastianOsinski/GRDBSnapshotTesting) adds snapshot strategy for testing SQLite database migrations made with [GRDB](https://github.com/groue/GRDB.swift).
- [AccessibilitySnapshot](https://github.com/cashapp/AccessibilitySnapshot) adds easy regression testing for iOS accessibility.
- [AccessibilitySnapshotColorBlindness](https://github.com/Sherlouk/AccessibilitySnapshotColorBlindness) adds snapshot strategies for color blindness simulation on iOS views, view controllers and images.
Have you written your own SnapshotTesting plug-in? [Add it here](https://github.com/pointfreeco/swift-snapshot-testing/edit/master/README.md) and submit a pull request!
## Related Tools
- [`iOSSnapshotTestCase`](https://github.com/uber/ios-snapshot-test-case/) helped introduce screen shot testing to a broad audience in the iOS community. Experience with it inspired the creation of this library.
- [Jest](https://jestjs.io) brought generalized snapshot testing to the JavaScript community with a polished user experience. Several features of this library (diffing, automatically capturing new snapshots) were directly influenced.
## Learn More
SnapshotTesting was designed with [witness-oriented programming](https://www.pointfree.co/episodes/ep39-witness-oriented-library-design).
This concept (and more) are explored thoroughly in a series of episodes on [Point-Free](https://www.pointfree.co), a video series exploring functional programming and Swift hosted by [Brandon Williams](https://twitter.com/mbrandonw) and [Stephen Celis](https://twitter.com/stephencelis).
Witness-oriented programming and the design of this library was explored in the following [Point-Free](https://www.pointfree.co) episodes:
- [Episode 33](https://www.pointfree.co/episodes/ep33-protocol-witnesses-part-1): Protocol Witnesses: Part 1
- [Episode 34](https://www.pointfree.co/episodes/ep34-protocol-witnesses-part-1): Protocol Witnesses: Part 2
- [Episode 35](https://www.pointfree.co/episodes/ep35-advanced-protocol-witnesses-part-1): Advanced Protocol Witnesses: Part 1
- [Episode 36](https://www.pointfree.co/episodes/ep36-advanced-protocol-witnesses-part-2): Advanced Protocol Witnesses: Part 2
- [Episode 37](https://www.pointfree.co/episodes/ep37-protocol-oriented-library-design-part-1): Protocol-Oriented Library Design: Part 1
- [Episode 38](https://www.pointfree.co/episodes/ep38-protocol-oriented-library-design-part-2): Protocol-Oriented Library Design: Part 2
- [Episode 39](https://www.pointfree.co/episodes/ep39-witness-oriented-library-design): Witness-Oriented Library Design
- [Episode 40](https://www.pointfree.co/episodes/ep40-async-functional-refactoring): Async Functional Refactoring
- [Episode 41](https://www.pointfree.co/episodes/ep41-a-tour-of-snapshot-testing): A Tour of Snapshot Testing 🆓
<a href="https://www.pointfree.co/episodes/ep41-a-tour-of-snapshot-testing">
<img alt="video poster image" src="https://d1hf1soyumxcgv.cloudfront.net/0041-tour-of-snapshot-testing/0041-poster.jpg" width="480">
</a>
## License
This library is released under the MIT license. See [LICENSE](LICENSE) for details.
@@ -1,326 +0,0 @@
import XCTest
/// Asserts that a given value matches a string literal.
///
/// Note: Empty `reference` will be replaced automatically with generated output.
///
/// Usage:
/// ```
/// _assertInlineSnapshot(matching: value, as: .dump, with: """
/// """)
/// ```
///
/// - Parameters:
/// - value: A value to compare against a reference.
/// - snapshotting: A strategy for serializing, deserializing, and comparing values.
/// - recording: Whether or not to record a new reference.
/// - timeout: The amount of time a snapshot must be generated in.
/// - reference: The expected output of snapshotting.
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
public func _assertInlineSnapshot<Value>(
matching value: @autoclosure () throws -> Value,
as snapshotting: Snapshotting<Value, String>,
record recording: Bool = false,
timeout: TimeInterval = 5,
with reference: String,
file: StaticString = #file,
testName: String = #function,
line: UInt = #line
) {
let failure = _verifyInlineSnapshot(
matching: try value(),
as: snapshotting,
record: recording,
timeout: timeout,
with: reference,
file: file,
testName: testName,
line: line
)
guard let message = failure else { return }
XCTFail(message, file: file, line: line)
}
/// Verifies that a given value matches a string literal.
///
/// Third party snapshot assert helpers can be built on top of this function. Simply invoke `verifyInlineSnapshot` with your own arguments, and then invoke `XCTFail` with the string returned if it is non-`nil`.
///
/// - Parameters:
/// - value: A value to compare against a reference.
/// - snapshotting: A strategy for serializing, deserializing, and comparing values.
/// - recording: Whether or not to record a new reference.
/// - timeout: The amount of time a snapshot must be generated in.
/// - reference: The expected output of snapshotting.
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
/// - Returns: A failure message or, if the value matches, nil.
public func _verifyInlineSnapshot<Value>(
matching value: @autoclosure () throws -> Value,
as snapshotting: Snapshotting<Value, String>,
record recording: Bool = false,
timeout: TimeInterval = 5,
with reference: String,
file: StaticString = #file,
testName: String = #function,
line: UInt = #line
)
-> String? {
let recording = recording || isRecording
do {
let tookSnapshot = XCTestExpectation(description: "Took snapshot")
var optionalDiffable: String?
snapshotting.snapshot(try value()).run { b in
optionalDiffable = b
tookSnapshot.fulfill()
}
let result = XCTWaiter.wait(for: [tookSnapshot], timeout: timeout)
switch result {
case .completed:
break
case .timedOut:
return """
Exceeded timeout of \(timeout) seconds waiting for snapshot.
This can happen when an asynchronously rendered view (like a web view) has not loaded. \
Ensure that every subview of the view hierarchy has loaded to avoid timeouts, or, if a \
timeout is unavoidable, consider setting the "timeout" parameter of "assertSnapshot" to \
a higher value.
"""
case .incorrectOrder, .invertedFulfillment, .interrupted:
return "Couldn't snapshot value"
@unknown default:
return "Couldn't snapshot value"
}
let trimmingChars = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: "\u{FEFF}"))
guard let diffable = optionalDiffable?.trimmingCharacters(in: trimmingChars) else {
return "Couldn't snapshot value"
}
let trimmedReference = reference.trimmingCharacters(in: .whitespacesAndNewlines)
// Always perform diff, and return early on success!
guard let (failure, attachments) = snapshotting.diffing.diff(trimmedReference, diffable) else {
return nil
}
// If that diff failed, we either record or fail.
if recording || trimmedReference.isEmpty {
let fileName = "\(file)"
let sourceCodeFilePath = URL(fileURLWithPath: fileName, isDirectory: false)
let sourceCode = try String(contentsOf: sourceCodeFilePath)
var newRecordings = recordings
let modifiedSource = try writeInlineSnapshot(
&newRecordings,
Context(
sourceCode: sourceCode,
diffable: diffable,
fileName: fileName,
lineIndex: Int(line)
)
).sourceCode
try modifiedSource
.data(using: String.Encoding.utf8)?
.write(to: sourceCodeFilePath)
if newRecordings != recordings {
recordings = newRecordings
/// If no other recording has been made, then fail!
return """
No reference was found inline. Automatically recorded snapshot.
Re-run "\(sanitizePathComponent(testName))" to test against the newly-recorded snapshot.
"""
} else {
/// There is already an failure in this file,
/// and we don't want to write to the wrong place.
return nil
}
}
/// Did not successfully record, so we will fail.
if !attachments.isEmpty {
#if !os(Linux)
if ProcessInfo.processInfo.environment.keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
XCTContext.runActivity(named: "Attached Failure Diff") { activity in
attachments.forEach {
activity.add($0)
}
}
}
#endif
}
return """
Snapshot does not match reference.
\(failure.trimmingCharacters(in: .whitespacesAndNewlines))
"""
} catch {
return error.localizedDescription
}
}
internal typealias Recordings = [String: [FileRecording]]
internal struct Context {
let sourceCode: String
let diffable: String
let fileName: String
// First line of a file is line 1 (as with the #line macro)
let lineIndex: Int
func setSourceCode(_ newSourceCode: String) -> Context {
return Context(
sourceCode: newSourceCode,
diffable: diffable,
fileName: fileName,
lineIndex: lineIndex
)
}
}
internal func writeInlineSnapshot(
_ recordings: inout Recordings,
_ context: Context
) throws -> Context {
var sourceCodeLines = context.sourceCode
.split(separator: "\n", omittingEmptySubsequences: false)
let otherRecordings = recordings[context.fileName, default: []]
let otherRecordingsAboveThisLine = otherRecordings.filter { $0.line < context.lineIndex }
let offsetStartIndex = otherRecordingsAboveThisLine.reduce(context.lineIndex) { $0 + $1.difference }
let functionLineIndex = offsetStartIndex - 1
var lineCountDifference = 0
// Convert `""` to multi-line literal
if sourceCodeLines[functionLineIndex].hasSuffix(emptyStringLiteralWithCloseBrace) {
// Convert:
// _assertInlineSnapshot(matching: value, as: .dump, with: "")
// to:
// _assertInlineSnapshot(matching: value, as: .dump, with: """
// """)
var functionCallLine = sourceCodeLines.remove(at: functionLineIndex)
functionCallLine.removeLast(emptyStringLiteralWithCloseBrace.count)
let indentText = indentation(of: functionCallLine)
sourceCodeLines.insert(contentsOf: [
functionCallLine + multiLineStringLiteralTerminator,
indentText + multiLineStringLiteralTerminator + ")",
] as [String.SubSequence], at: functionLineIndex)
lineCountDifference += 1
}
/// If they haven't got a multi-line literal by now, then just fail.
guard sourceCodeLines[functionLineIndex].hasSuffix(multiLineStringLiteralTerminator) else {
struct InlineError: LocalizedError {
var errorDescription: String? {
return """
To use inline snapshots, please convert the "with" argument to a multi-line literal.
"""
}
}
throw InlineError()
}
/// Find the end of multi-line literal and replace contents with recording.
if let multiLineLiteralEndIndex = sourceCodeLines[offsetStartIndex...].firstIndex(where: { $0.hasClosingMultilineStringDelimiter() }) {
let diffableLines = context.diffable.split(separator: "\n")
// Add #'s to the multiline string literal if needed
let numberSigns: String
if context.diffable.hasEscapedSpecialCharactersLiteral() {
numberSigns = String(repeating: "#", count: context.diffable.numberOfNumberSignsNeeded())
} else if nil != diffableLines.first(where: { $0.endsInBackslash() }) {
// We want to avoid \ being interpreted as an escaped newline in the recorded inline snapshot
numberSigns = "#"
} else {
numberSigns = ""
}
let multiLineStringLiteralTerminatorPre = numberSigns + multiLineStringLiteralTerminator
let multiLineStringLiteralTerminatorPost = multiLineStringLiteralTerminator + numberSigns
// Update opening (#...)"""
sourceCodeLines[functionLineIndex].replaceFirstOccurrence(
of: extendedOpeningStringDelimitersPattern,
with: multiLineStringLiteralTerminatorPre
)
// Update closing """(#...)
sourceCodeLines[multiLineLiteralEndIndex].replaceFirstOccurrence(
of: extendedClosingStringDelimitersPattern,
with: multiLineStringLiteralTerminatorPost
)
/// Convert actual value to Lines to insert
let indentText = indentation(of: sourceCodeLines[multiLineLiteralEndIndex])
let newDiffableLines = context.diffable
.split(separator: "\n", omittingEmptySubsequences: false)
.map { Substring(indentText + $0) }
lineCountDifference += newDiffableLines.count - (multiLineLiteralEndIndex - offsetStartIndex)
let fileRecording = FileRecording(line: context.lineIndex, difference: lineCountDifference)
/// Insert the lines
sourceCodeLines.replaceSubrange(offsetStartIndex..<multiLineLiteralEndIndex, with: newDiffableLines)
recordings[context.fileName, default: []].append(fileRecording)
return context.setSourceCode(sourceCodeLines.joined(separator: "\n"))
}
return context.setSourceCode(sourceCodeLines.joined(separator: "\n"))
}
internal struct FileRecording: Equatable {
let line: Int
let difference: Int
}
private func indentation<S: StringProtocol>(of str: S) -> String {
var count = 0
for char in str {
guard char == " " else { break }
count += 1
}
return String(repeating: " ", count: count)
}
fileprivate extension Substring {
mutating func replaceFirstOccurrence(of pattern: String, with newString: String) {
let newString = replacingOccurrences(of: pattern, with: newString, options: .regularExpression)
self = Substring(newString)
}
func hasOpeningMultilineStringDelimiter() -> Bool {
return range(of: extendedOpeningStringDelimitersPattern, options: .regularExpression) != nil
}
func hasClosingMultilineStringDelimiter() -> Bool {
return range(of: extendedClosingStringDelimitersPattern, options: .regularExpression) != nil
}
func endsInBackslash() -> Bool {
if let lastChar = last {
return lastChar == Character(#"\"#)
}
return false
}
}
private let emptyStringLiteralWithCloseBrace = "\"\")"
private let multiLineStringLiteralTerminator = "\"\"\""
private let extendedOpeningStringDelimitersPattern = #"#{0,}\"\"\""#
private let extendedClosingStringDelimitersPattern = ##"\"\"\"#{0,}"##
// When we modify a file, the line numbers reported by the compiler through #line are no longer
// accurate. With the FileRecording values we keep track of we modify the files so we can adjust
// line numbers.
private var recordings: Recordings = [:]
@@ -1,313 +0,0 @@
import XCTest
/// Enhances failure messages with a command line diff tool expression that can be copied and pasted into a terminal.
///
/// diffTool = "ksdiff"
public var diffTool: String? = nil
/// Whether or not to record all new references.
public var isRecording = false
/// Whether or not to record all new references.
/// Due to a name clash in Xcode 12, this has been renamed to `isRecording`.
@available(*, deprecated, renamed: "isRecording")
public var record: Bool {
get { isRecording }
set { isRecording = newValue }
}
/// Asserts that a given value matches a reference on disk.
///
/// - Parameters:
/// - value: A value to compare against a reference.
/// - snapshotting: A strategy for serializing, deserializing, and comparing values.
/// - name: An optional description of the snapshot.
/// - recording: Whether or not to record a new reference.
/// - timeout: The amount of time a snapshot must be generated in.
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
public func assertSnapshot<Value, Format>(
matching value: @autoclosure () throws -> Value,
as snapshotting: Snapshotting<Value, Format>,
named name: String? = nil,
record recording: Bool = false,
timeout: TimeInterval = 5,
file: StaticString = #file,
testName: String = #function,
line: UInt = #line
) {
let failure = verifySnapshot(
matching: try value(),
as: snapshotting,
named: name,
record: recording,
timeout: timeout,
file: file,
testName: testName,
line: line
)
guard let message = failure else { return }
XCTFail(message, file: file, line: line)
}
/// Asserts that a given value matches references on disk.
///
/// - Parameters:
/// - value: A value to compare against a reference.
/// - snapshotting: A dictionary of names and strategies for serializing, deserializing, and comparing values.
/// - recording: Whether or not to record a new reference.
/// - timeout: The amount of time a snapshot must be generated in.
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
public func assertSnapshots<Value, Format>(
matching value: @autoclosure () throws -> Value,
as strategies: [String: Snapshotting<Value, Format>],
record recording: Bool = false,
timeout: TimeInterval = 5,
file: StaticString = #file,
testName: String = #function,
line: UInt = #line
) {
try? strategies.forEach { name, strategy in
assertSnapshot(
matching: try value(),
as: strategy,
named: name,
record: recording,
timeout: timeout,
file: file,
testName: testName,
line: line
)
}
}
/// Asserts that a given value matches references on disk.
///
/// - Parameters:
/// - value: A value to compare against a reference.
/// - snapshotting: An array of strategies for serializing, deserializing, and comparing values.
/// - recording: Whether or not to record a new reference.
/// - timeout: The amount of time a snapshot must be generated in.
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
public func assertSnapshots<Value, Format>(
matching value: @autoclosure () throws -> Value,
as strategies: [Snapshotting<Value, Format>],
record recording: Bool = false,
timeout: TimeInterval = 5,
file: StaticString = #file,
testName: String = #function,
line: UInt = #line
) {
try? strategies.forEach { strategy in
assertSnapshot(
matching: try value(),
as: strategy,
record: recording,
timeout: timeout,
file: file,
testName: testName,
line: line
)
}
}
/// Verifies that a given value matches a reference on disk.
///
/// Third party snapshot assert helpers can be built on top of this function. Simply invoke `verifySnapshot` with your own arguments, and then invoke `XCTFail` with the string returned if it is non-`nil`. For example, if you want the snapshot directory to be determined by an environment variable, you can create your own assert helper like so:
///
/// public func myAssertSnapshot<Value, Format>(
/// matching value: @autoclosure () throws -> Value,
/// as snapshotting: Snapshotting<Value, Format>,
/// named name: String? = nil,
/// record recording: Bool = false,
/// timeout: TimeInterval = 5,
/// file: StaticString = #file,
/// testName: String = #function,
/// line: UInt = #line
/// ) {
///
/// let snapshotDirectory = ProcessInfo.processInfo.environment["SNAPSHOT_REFERENCE_DIR"]! + "/" + #file
/// let failure = verifySnapshot(
/// matching: value,
/// as: snapshotting,
/// named: name,
/// record: recording,
/// snapshotDirectory: snapshotDirectory,
/// timeout: timeout,
/// file: file,
/// testName: testName
/// )
/// guard let message = failure else { return }
/// XCTFail(message, file: file, line: line)
/// }
///
/// - Parameters:
/// - value: A value to compare against a reference.
/// - snapshotting: A strategy for serializing, deserializing, and comparing values.
/// - name: An optional description of the snapshot.
/// - recording: Whether or not to record a new reference.
/// - snapshotDirectory: Optional directory to save snapshots. By default snapshots will be saved in a directory with the same name as the test file, and that directory will sit inside a directory `__Snapshots__` that sits next to your test file.
/// - timeout: The amount of time a snapshot must be generated in.
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
/// - Returns: A failure message or, if the value matches, nil.
public func verifySnapshot<Value, Format>(
matching value: @autoclosure () throws -> Value,
as snapshotting: Snapshotting<Value, Format>,
named name: String? = nil,
record recording: Bool = false,
snapshotDirectory: String? = nil,
timeout: TimeInterval = 5,
file: StaticString = #file,
testName: String = #function,
line: UInt = #line
)
-> String? {
let recording = recording || isRecording
do {
let fileUrl = URL(fileURLWithPath: "\(file)", isDirectory: false)
let fileName = fileUrl.deletingPathExtension().lastPathComponent
let snapshotDirectoryUrl = snapshotDirectory.map { URL(fileURLWithPath: $0, isDirectory: true) }
?? fileUrl
.deletingLastPathComponent()
.appendingPathComponent("__Snapshots__")
.appendingPathComponent(fileName)
let identifier: String
if let name = name {
identifier = sanitizePathComponent(name)
} else {
let counter = counterQueue.sync { () -> Int in
let key = snapshotDirectoryUrl.appendingPathComponent(testName)
counterMap[key, default: 0] += 1
return counterMap[key]!
}
identifier = String(counter)
}
let testName = sanitizePathComponent(testName)
let snapshotFileUrl = snapshotDirectoryUrl
.appendingPathComponent("\(testName).\(identifier)")
.appendingPathExtension(snapshotting.pathExtension ?? "")
let fileManager = FileManager.default
try fileManager.createDirectory(at: snapshotDirectoryUrl, withIntermediateDirectories: true)
let tookSnapshot = XCTestExpectation(description: "Took snapshot")
var optionalDiffable: Format?
snapshotting.snapshot(try value()).run { b in
optionalDiffable = b
tookSnapshot.fulfill()
}
let result = XCTWaiter.wait(for: [tookSnapshot], timeout: timeout)
switch result {
case .completed:
break
case .timedOut:
return """
Exceeded timeout of \(timeout) seconds waiting for snapshot.
This can happen when an asynchronously rendered view (like a web view) has not loaded. \
Ensure that every subview of the view hierarchy has loaded to avoid timeouts, or, if a \
timeout is unavoidable, consider setting the "timeout" parameter of "assertSnapshot" to \
a higher value.
"""
case .incorrectOrder, .invertedFulfillment, .interrupted:
return "Couldn't snapshot value"
@unknown default:
return "Couldn't snapshot value"
}
guard var diffable = optionalDiffable else {
return "Couldn't snapshot value"
}
guard !recording, fileManager.fileExists(atPath: snapshotFileUrl.path) else {
try snapshotting.diffing.toData(diffable).write(to: snapshotFileUrl)
return recording
? """
Record mode is on. Turn record mode off and re-run "\(testName)" to test against the newly-recorded snapshot.
open "\(snapshotFileUrl.path)"
Recorded snapshot: …
"""
: """
No reference was found on disk. Automatically recorded snapshot: …
open "\(snapshotFileUrl.path)"
Re-run "\(testName)" to test against the newly-recorded snapshot.
"""
}
let data = try Data(contentsOf: snapshotFileUrl)
let reference = snapshotting.diffing.fromData(data)
#if os(iOS) || os(tvOS)
// If the image generation fails for the diffable part use the reference
if let localDiff = diffable as? UIImage, localDiff.size == .zero {
diffable = reference
}
#endif
guard let (failure, attachments) = snapshotting.diffing.diff(reference, diffable) else {
return nil
}
let artifactsUrl = URL(
fileURLWithPath: ProcessInfo.processInfo.environment["SNAPSHOT_ARTIFACTS"] ?? NSTemporaryDirectory(), isDirectory: true
)
let artifactsSubUrl = artifactsUrl.appendingPathComponent(fileName)
try fileManager.createDirectory(at: artifactsSubUrl, withIntermediateDirectories: true)
let failedSnapshotFileUrl = artifactsSubUrl.appendingPathComponent(snapshotFileUrl.lastPathComponent)
try snapshotting.diffing.toData(diffable).write(to: failedSnapshotFileUrl)
if !attachments.isEmpty {
#if !os(Linux)
if ProcessInfo.processInfo.environment.keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
XCTContext.runActivity(named: "Attached Failure Diff") { activity in
attachments.forEach {
activity.add($0)
}
}
}
#endif
}
let diffMessage = diffTool
.map { "\($0) \"\(snapshotFileUrl.path)\" \"\(failedSnapshotFileUrl.path)\"" }
?? "@\(minus)\n\"\(snapshotFileUrl.path)\"\n@\(plus)\n\"\(failedSnapshotFileUrl.path)\""
return """
Snapshot does not match reference.
\(diffMessage)
\(failure.trimmingCharacters(in: .whitespacesAndNewlines))
"""
} catch {
return error.localizedDescription
}
}
// MARK: - Private
private let counterQueue = DispatchQueue(label: "co.pointfree.SnapshotTesting.counter")
private var counterMap: [URL: Int] = [:]
func sanitizePathComponent(_ string: String) -> String {
return string
.replacingOccurrences(of: "\\W+", with: "-", options: .regularExpression)
.replacingOccurrences(of: "^-|-$", with: "", options: .regularExpression)
}
@@ -1,39 +0,0 @@
/// A wrapper around an asynchronous operation.
///
/// Snapshot strategies may utilize this type to create snapshots in an asynchronous fashion.
///
/// For example, WebKit's `WKWebView` offers a callback-based API for taking image snapshots (`takeSnapshot`). `Async` allows us to build a value that can pass its callback along to the scope in which the image has been created.
///
/// Async<UIImage> { callback in
/// webView.takeSnapshot(with: nil) { image, error in
/// callback(image!)
/// }
/// }
public struct Async<Value> {
public let run: (@escaping (Value) -> Void) -> Void
/// Creates an asynchronous operation.
///
/// - Parameters:
/// - run: A function that, when called, can hand a value to a callback.
/// - callback: A function that can be called with a value.
public init(run: @escaping (_ callback: @escaping (Value) -> Void) -> Void) {
self.run = run
}
/// Wraps a pure value in an asynchronous operation.
///
/// - Parameter value: A value to be wrapped in an asynchronous operation.
public init(value: Value) {
self.init { callback in callback(value) }
}
/// Transforms an Async<Value> into an Async<NewValue> with a function `(Value) -> NewValue`.
///
/// - Parameter f: A transformation to apply to the value wrapped by the async value.
public func map<NewValue>(_ f: @escaping (Value) -> NewValue) -> Async<NewValue> {
return .init { callback in
self.run { a in callback(f(a)) }
}
}
}
@@ -1,11 +0,0 @@
#if os(macOS)
import Cocoa
typealias Image = NSImage
typealias ImageView = NSImageView
typealias View = NSView
#elseif os(iOS) || os(tvOS)
import UIKit
typealias Image = UIImage
typealias ImageView = UIImageView
typealias View = UIView
#endif
File diff suppressed because it is too large Load Diff
@@ -1,57 +0,0 @@
import Foundation
extension String {
/// Checks whether the string has escaped special character literals or not.
///
/// This method won't detect an unescaped special character.
/// For example, this method will return true for "\\n" or #"\n"#, but false for "\n"
///
/// The following are the special character literals that this methods looks for:
/// The escaped special characters \0 (null character), \\ (backslash),
/// \t (horizontal tab), \n (line feed), \r (carriage return),
/// \" (double quotation mark) and \' (single quotation mark),
/// An arbitrary Unicode scalar value, written as \u{n},
/// where n is a 18 digit hexadecimal number (Unicode is discussed in Unicode below)
/// The character sequence "#
///
/// - Returns: True if the string has any special character literals, false otherwise.
func hasEscapedSpecialCharactersLiteral() -> Bool {
let multilineLiteralAndNumberSign = ##"""
"""#
"""##
let patterns = [
// Matches \u{n} where n is a 18 digit hexadecimal number
try? NSRegularExpression(pattern: #"\\u\{[a-fA-f0-9]{1,8}\}"#, options: .init()),
try? NSRegularExpression(pattern: #"\0"#, options: .ignoreMetacharacters),
try? NSRegularExpression(pattern: #"\\"#, options: .ignoreMetacharacters),
try? NSRegularExpression(pattern: #"\t"#, options: .ignoreMetacharacters),
try? NSRegularExpression(pattern: #"\n"#, options: .ignoreMetacharacters),
try? NSRegularExpression(pattern: #"\r"#, options: .ignoreMetacharacters),
try? NSRegularExpression(pattern: #"\""#, options: .ignoreMetacharacters),
try? NSRegularExpression(pattern: #"\'"#, options: .ignoreMetacharacters),
try? NSRegularExpression(pattern: multilineLiteralAndNumberSign, options: .ignoreMetacharacters),
]
let matches = patterns.compactMap { $0?.firstMatch(in: self, options: .init(), range: NSRange.init(location: 0, length: self.count)) }
return matches.count > 0
}
/// This method calculates how many number signs (#) we need to add around a string
/// literal to properly escape its content.
///
/// Multiple # are needed when the literal contains "#, "##, "### ...
///
/// - Returns: The number of "number signs(#)" needed around a string literal.
/// When there is no "#, ... return 1
func numberOfNumberSignsNeeded() -> Int {
let pattern = try! NSRegularExpression(pattern: ##""#{1,}"##, options: .init())
let matches = pattern.matches(in: self, options: .init(), range: NSRange.init(location: 0, length: self.count))
// If we have "## then the length of the match is 3,
// which is also the number of "number signs (#)" we need to add
// before and after the string literal
return matches.map { $0.range.length }.max() ?? 1
}
}
@@ -1,891 +0,0 @@
#if os(iOS) || os(macOS) || os(tvOS)
#if os(macOS)
import Cocoa
#endif
import SceneKit
import SpriteKit
#if os(iOS) || os(tvOS)
import UIKit
#endif
#if os(iOS) || os(macOS)
import WebKit
#endif
#if os(iOS) || os(tvOS)
public struct ViewImageConfig {
public enum Orientation {
case landscape
case portrait
}
public enum TabletOrientation {
public enum PortraitSplits {
case oneThird
case twoThirds
case full
}
public enum LandscapeSplits {
case oneThird
case oneHalf
case twoThirds
case full
}
case landscape(splitView: LandscapeSplits)
case portrait(splitView: PortraitSplits)
}
public var safeArea: UIEdgeInsets
public var size: CGSize?
public var traits: UITraitCollection
public init(
safeArea: UIEdgeInsets = .zero,
size: CGSize? = nil,
traits: UITraitCollection = .init()
) {
self.safeArea = safeArea
self.size = size
self.traits = traits
}
#if os(iOS)
public static let iPhoneSe = ViewImageConfig.iPhoneSe(.portrait)
public static func iPhoneSe(_ orientation: Orientation) -> ViewImageConfig {
let safeArea: UIEdgeInsets
let size: CGSize
switch orientation {
case .landscape:
safeArea = .zero
size = .init(width: 568, height: 320)
case .portrait:
safeArea = .init(top: 20, left: 0, bottom: 0, right: 0)
size = .init(width: 320, height: 568)
}
return .init(safeArea: safeArea, size: size, traits: .iPhoneSe(orientation))
}
public static let iPhone8 = ViewImageConfig.iPhone8(.portrait)
public static func iPhone8(_ orientation: Orientation) -> ViewImageConfig {
let safeArea: UIEdgeInsets
let size: CGSize
switch orientation {
case .landscape:
safeArea = .zero
size = .init(width: 667, height: 375)
case .portrait:
safeArea = .init(top: 20, left: 0, bottom: 0, right: 0)
size = .init(width: 375, height: 667)
}
return .init(safeArea: safeArea, size: size, traits: .iPhone8(orientation))
}
public static let iPhone8Plus = ViewImageConfig.iPhone8Plus(.portrait)
public static func iPhone8Plus(_ orientation: Orientation) -> ViewImageConfig {
let safeArea: UIEdgeInsets
let size: CGSize
switch orientation {
case .landscape:
safeArea = .zero
size = .init(width: 736, height: 414)
case .portrait:
safeArea = .init(top: 20, left: 0, bottom: 0, right: 0)
size = .init(width: 414, height: 736)
}
return .init(safeArea: safeArea, size: size, traits: .iPhone8Plus(orientation))
}
public static let iPhoneX = ViewImageConfig.iPhoneX(.portrait)
public static func iPhoneX(_ orientation: Orientation) -> ViewImageConfig {
let safeArea: UIEdgeInsets
let size: CGSize
switch orientation {
case .landscape:
safeArea = .init(top: 0, left: 44, bottom: 24, right: 44)
size = .init(width: 812, height: 375)
case .portrait:
safeArea = .init(top: 44, left: 0, bottom: 34, right: 0)
size = .init(width: 375, height: 812)
}
return .init(safeArea: safeArea, size: size, traits: .iPhoneX(orientation))
}
public static let iPhoneXsMax = ViewImageConfig.iPhoneXsMax(.portrait)
public static func iPhoneXsMax(_ orientation: Orientation) -> ViewImageConfig {
let safeArea: UIEdgeInsets
let size: CGSize
switch orientation {
case .landscape:
safeArea = .init(top: 0, left: 44, bottom: 24, right: 44)
size = .init(width: 896, height: 414)
case .portrait:
safeArea = .init(top: 44, left: 0, bottom: 34, right: 0)
size = .init(width: 414, height: 896)
}
return .init(safeArea: safeArea, size: size, traits: .iPhoneXsMax(orientation))
}
@available(iOS 11.0, *)
public static let iPhoneXr = ViewImageConfig.iPhoneXr(.portrait)
@available(iOS 11.0, *)
public static func iPhoneXr(_ orientation: Orientation) -> ViewImageConfig {
let safeArea: UIEdgeInsets
let size: CGSize
switch orientation {
case .landscape:
safeArea = .init(top: 0, left: 44, bottom: 24, right: 44)
size = .init(width: 896, height: 414)
case .portrait:
safeArea = .init(top: 44, left: 0, bottom: 34, right: 0)
size = .init(width: 414, height: 896)
}
return .init(safeArea: safeArea, size: size, traits: .iPhoneXr(orientation))
}
public static let iPadMini = ViewImageConfig.iPadMini(.landscape)
public static func iPadMini(_ orientation: Orientation) -> ViewImageConfig {
switch orientation {
case .landscape:
return ViewImageConfig.iPadMini(.landscape(splitView: .full))
case .portrait:
return ViewImageConfig.iPadMini(.portrait(splitView: .full))
}
}
public static func iPadMini(_ orientation: TabletOrientation) -> ViewImageConfig {
let size: CGSize
let traits: UITraitCollection
switch orientation {
case .landscape(let splitView):
switch splitView {
case .oneThird:
size = .init(width: 320, height: 768)
traits = .iPadMini_Compact_SplitView
case .oneHalf:
size = .init(width: 507, height: 768)
traits = .iPadMini_Compact_SplitView
case .twoThirds:
size = .init(width: 694, height: 768)
traits = .iPadMini
case .full:
size = .init(width: 1024, height: 768)
traits = .iPadMini
}
case .portrait(let splitView):
switch splitView {
case .oneThird:
size = .init(width: 320, height: 1024)
traits = .iPadMini_Compact_SplitView
case .twoThirds:
size = .init(width: 438, height: 1024)
traits = .iPadMini_Compact_SplitView
case .full:
size = .init(width: 768, height: 1024)
traits = .iPadMini
}
}
return .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: size, traits: traits)
}
public static let iPadPro10_5 = ViewImageConfig.iPadPro10_5(.landscape)
public static func iPadPro10_5(_ orientation: Orientation) -> ViewImageConfig {
switch orientation {
case .landscape:
return ViewImageConfig.iPadPro10_5(.landscape(splitView: .full))
case .portrait:
return ViewImageConfig.iPadPro10_5(.portrait(splitView: .full))
}
}
public static func iPadPro10_5(_ orientation: TabletOrientation) -> ViewImageConfig {
let size: CGSize
let traits: UITraitCollection
switch orientation {
case .landscape(let splitView):
switch splitView {
case .oneThird:
size = .init(width: 320, height: 834)
traits = .iPadPro10_5_Compact_SplitView
case .oneHalf:
size = .init(width: 551, height: 834)
traits = .iPadPro10_5_Compact_SplitView
case .twoThirds:
size = .init(width: 782, height: 834)
traits = .iPadPro10_5
case .full:
size = .init(width: 1112, height: 834)
traits = .iPadPro10_5
}
case .portrait(let splitView):
switch splitView {
case .oneThird:
size = .init(width: 320, height: 1112)
traits = .iPadPro10_5_Compact_SplitView
case .twoThirds:
size = .init(width: 504, height: 1112)
traits = .iPadPro10_5_Compact_SplitView
case .full:
size = .init(width: 834, height: 1112)
traits = .iPadPro10_5
}
}
return .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: size, traits: traits)
}
public static let iPadPro11 = ViewImageConfig.iPadPro11(.landscape)
public static func iPadPro11(_ orientation: Orientation) -> ViewImageConfig {
switch orientation {
case .landscape:
return ViewImageConfig.iPadPro11(.landscape(splitView: .full))
case .portrait:
return ViewImageConfig.iPadPro11(.portrait(splitView: .full))
}
}
public static func iPadPro11(_ orientation: TabletOrientation) -> ViewImageConfig {
let size: CGSize
let traits: UITraitCollection
switch orientation {
case .landscape(let splitView):
switch splitView {
case .oneThird:
size = .init(width: 375, height: 834)
traits = .iPadPro11_Compact_SplitView
case .oneHalf:
size = .init(width: 592, height: 834)
traits = .iPadPro11_Compact_SplitView
case .twoThirds:
size = .init(width: 809, height: 834)
traits = .iPadPro11
case .full:
size = .init(width: 1194, height: 834)
traits = .iPadPro11
}
case .portrait(let splitView):
switch splitView {
case .oneThird:
size = .init(width: 320, height: 1194)
traits = .iPadPro11_Compact_SplitView
case .twoThirds:
size = .init(width: 504, height: 1194)
traits = .iPadPro11_Compact_SplitView
case .full:
size = .init(width: 834, height: 1194)
traits = .iPadPro11
}
}
return .init(safeArea: .init(top: 24, left: 0, bottom: 20, right: 0), size: size, traits: traits)
}
public static let iPadPro12_9 = ViewImageConfig.iPadPro12_9(.landscape)
public static func iPadPro12_9(_ orientation: Orientation) -> ViewImageConfig {
switch orientation {
case .landscape:
return ViewImageConfig.iPadPro12_9(.landscape(splitView: .full))
case .portrait:
return ViewImageConfig.iPadPro12_9(.portrait(splitView: .full))
}
}
public static func iPadPro12_9(_ orientation: TabletOrientation) -> ViewImageConfig {
let size: CGSize
let traits: UITraitCollection
switch orientation {
case .landscape(let splitView):
switch splitView {
case .oneThird:
size = .init(width: 375, height: 1024)
traits = .iPadPro12_9_Compact_SplitView
case .oneHalf:
size = .init(width: 678, height: 1024)
traits = .iPadPro12_9
case .twoThirds:
size = .init(width: 981, height: 1024)
traits = .iPadPro12_9
case .full:
size = .init(width: 1366, height: 1024)
traits = .iPadPro12_9
}
case .portrait(let splitView):
switch splitView {
case .oneThird:
size = .init(width: 375, height: 1366)
traits = .iPadPro12_9_Compact_SplitView
case .twoThirds:
size = .init(width: 639, height: 1366)
traits = .iPadPro12_9_Compact_SplitView
case .full:
size = .init(width: 1024, height: 1366)
traits = .iPadPro12_9
}
}
return .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: size, traits: traits)
}
#elseif os(tvOS)
public static let tv = ViewImageConfig(
safeArea: .init(top: 60, left: 90, bottom: 60, right: 90),
size: .init(width: 1920, height: 1080),
traits: .init()
)
public static let tv4K = ViewImageConfig(
safeArea: .init(top: 120, left: 180, bottom: 120, right: 180),
size: .init(width: 3840, height: 2160),
traits: .init()
)
#endif
}
extension UITraitCollection {
#if os(iOS)
public static func iPhoneSe(_ orientation: ViewImageConfig.Orientation)
-> UITraitCollection {
let base: [UITraitCollection] = [
// .init(displayGamut: .SRGB),
// .init(displayScale: 2),
.init(forceTouchCapability: .available),
.init(layoutDirection: .leftToRight),
.init(preferredContentSizeCategory: .medium),
.init(userInterfaceIdiom: .phone)
]
switch orientation {
case .landscape:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .compact)
]
)
case .portrait:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular),
]
)
}
}
public static func iPhone8(_ orientation: ViewImageConfig.Orientation)
-> UITraitCollection {
let base: [UITraitCollection] = [
// .init(displayGamut: .P3),
// .init(displayScale: 2),
.init(forceTouchCapability: .available),
.init(layoutDirection: .leftToRight),
.init(preferredContentSizeCategory: .medium),
.init(userInterfaceIdiom: .phone)
]
switch orientation {
case .landscape:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .compact)
]
)
case .portrait:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular)
]
)
}
}
public static func iPhone8Plus(_ orientation: ViewImageConfig.Orientation)
-> UITraitCollection {
let base: [UITraitCollection] = [
// .init(displayGamut: .P3),
// .init(displayScale: 3),
.init(forceTouchCapability: .available),
.init(layoutDirection: .leftToRight),
.init(preferredContentSizeCategory: .medium),
.init(userInterfaceIdiom: .phone)
]
switch orientation {
case .landscape:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .regular),
.init(verticalSizeClass: .compact)
]
)
case .portrait:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular)
]
)
}
}
public static func iPhoneX(_ orientation: ViewImageConfig.Orientation)
-> UITraitCollection {
let base: [UITraitCollection] = [
// .init(displayGamut: .P3),
// .init(displayScale: 3),
.init(forceTouchCapability: .available),
.init(layoutDirection: .leftToRight),
.init(preferredContentSizeCategory: .medium),
.init(userInterfaceIdiom: .phone)
]
switch orientation {
case .landscape:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .compact)
]
)
case .portrait:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular)
]
)
}
}
public static func iPhoneXr(_ orientation: ViewImageConfig.Orientation)
-> UITraitCollection {
let base: [UITraitCollection] = [
// .init(displayGamut: .P3),
// .init(displayScale: 2),
.init(forceTouchCapability: .unavailable),
.init(layoutDirection: .leftToRight),
.init(preferredContentSizeCategory: .medium),
.init(userInterfaceIdiom: .phone)
]
switch orientation {
case .landscape:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .regular),
.init(verticalSizeClass: .compact)
]
)
case .portrait:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular)
]
)
}
}
public static func iPhoneXsMax(_ orientation: ViewImageConfig.Orientation)
-> UITraitCollection {
let base: [UITraitCollection] = [
// .init(displayGamut: .P3),
// .init(displayScale: 3),
.init(forceTouchCapability: .available),
.init(layoutDirection: .leftToRight),
.init(preferredContentSizeCategory: .medium),
.init(userInterfaceIdiom: .phone)
]
switch orientation {
case .landscape:
return .init(
traitsFrom: base + [
.init(horizontalSizeClass: .regular),
.init(verticalSizeClass: .compact)
]
)
case .portrait:
return .init(
traitsFrom: [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular)
]
)
}
}
public static let iPadMini = iPad
public static let iPadMini_Compact_SplitView = iPadCompactSplitView
public static let iPadPro10_5 = iPad
public static let iPadPro10_5_Compact_SplitView = iPadCompactSplitView
public static let iPadPro11 = iPad
public static let iPadPro11_Compact_SplitView = iPadCompactSplitView
public static let iPadPro12_9 = iPad
public static let iPadPro12_9_Compact_SplitView = iPadCompactSplitView
private static let iPad = UITraitCollection(
traitsFrom: [
// .init(displayScale: 2),
.init(horizontalSizeClass: .regular),
.init(verticalSizeClass: .regular),
.init(userInterfaceIdiom: .pad)
]
)
private static let iPadCompactSplitView = UITraitCollection(
traitsFrom: [
.init(horizontalSizeClass: .compact),
.init(verticalSizeClass: .regular),
.init(userInterfaceIdiom: .pad)
]
)
#elseif os(tvOS)
// TODO
#endif
}
#endif
func addImagesForRenderedViews(_ view: View) -> [Async<View>] {
return view.snapshot
.map { async in
[
Async { callback in
async.run { image in
let imageView = ImageView()
imageView.image = image
imageView.frame = view.frame
#if os(macOS)
view.superview?.addSubview(imageView, positioned: .above, relativeTo: view)
#elseif os(iOS) || os(tvOS)
view.superview?.insertSubview(imageView, aboveSubview: view)
#endif
callback(imageView)
}
}
]
}
?? view.subviews.flatMap(addImagesForRenderedViews)
}
extension View {
var snapshot: Async<Image>? {
func inWindow<T>(_ perform: () -> T) -> T {
#if os(macOS)
let superview = self.superview
defer { superview?.addSubview(self) }
let window = ScaledWindow()
window.contentView = NSView()
window.contentView?.addSubview(self)
window.makeKey()
#endif
return perform()
}
#if (os(iOS) && !targetEnvironment(macCatalyst)) || os(tvOS)
if let glkView = self as? GLKView {
return Async(value: inWindow { glkView.snapshot })
}
#endif
if let scnView = self as? SCNView {
return Async(value: inWindow { scnView.snapshot() })
} else if let skView = self as? SKView {
if #available(macOS 10.11, *) {
let cgImage = inWindow { skView.texture(from: skView.scene!)!.cgImage() }
#if os(macOS)
let image = Image(cgImage: cgImage, size: skView.bounds.size)
#elseif os(iOS) || os(tvOS)
let image = Image(cgImage: cgImage)
#endif
return Async(value: image)
} else {
fatalError("Taking SKView snapshots requires macOS 10.11 or greater")
}
}
#if os(iOS) || os(macOS)
if let wkWebView = self as? WKWebView {
return Async<Image> { callback in
let delegate = NavigationDelegate()
let work = {
if #available(iOS 11.0, macOS 10.13, *) {
inWindow {
guard wkWebView.frame.width != 0, wkWebView.frame.height != 0 else {
callback(Image())
return
}
wkWebView.takeSnapshot(with: nil) { image, _ in
_ = delegate
callback(image!)
}
}
} else {
#if os(iOS)
fatalError("Taking WKWebView snapshots requires iOS 11.0 or greater")
#elseif os(macOS)
fatalError("Taking WKWebView snapshots requires macOS 10.13 or greater")
#endif
}
}
if wkWebView.isLoading {
delegate.didFinish = work
wkWebView.navigationDelegate = delegate
} else {
work()
}
}
}
#endif
return nil
}
#if os(iOS) || os(tvOS)
func asImage() -> Image {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
#endif
}
#if os(iOS) || os(macOS)
private final class NavigationDelegate: NSObject, WKNavigationDelegate {
var didFinish: () -> Void
init(didFinish: @escaping () -> Void = {}) {
self.didFinish = didFinish
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState") { _, _ in
self.didFinish()
}
}
}
#endif
#if os(iOS) || os(tvOS)
extension UIApplication {
static var sharedIfAvailable: UIApplication? {
let sharedSelector = NSSelectorFromString("sharedApplication")
guard UIApplication.responds(to: sharedSelector) else {
return nil
}
let shared = UIApplication.perform(sharedSelector)
return shared?.takeUnretainedValue() as! UIApplication?
}
}
func prepareView(
config: ViewImageConfig,
drawHierarchyInKeyWindow: Bool,
traits: UITraitCollection,
view: UIView,
viewController: UIViewController
) -> () -> Void {
let size = config.size ?? viewController.view.frame.size
view.frame.size = size
if view != viewController.view {
viewController.view.bounds = view.bounds
viewController.view.addSubview(view)
}
let traits = UITraitCollection(traitsFrom: [config.traits, traits])
let window: UIWindow
if drawHierarchyInKeyWindow {
guard let keyWindow = getKeyWindow() else {
fatalError("'drawHierarchyInKeyWindow' requires tests to be run in a host application")
}
window = keyWindow
window.frame.size = size
} else {
window = Window(
config: .init(safeArea: config.safeArea, size: config.size ?? size, traits: traits),
viewController: viewController
)
}
let dispose = add(traits: traits, viewController: viewController, to: window)
if size.width == 0 || size.height == 0 {
// Try to call sizeToFit() if the view still has invalid size
view.sizeToFit()
view.setNeedsLayout()
view.layoutIfNeeded()
}
return dispose
}
func snapshotView(
config: ViewImageConfig,
drawHierarchyInKeyWindow: Bool,
traits: UITraitCollection,
view: UIView,
viewController: UIViewController
)
-> Async<UIImage> {
let initialFrame = view.frame
let dispose = prepareView(
config: config,
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
traits: traits,
view: view,
viewController: viewController
)
// NB: Avoid safe area influence.
if config.safeArea == .zero { view.frame.origin = .init(x: offscreen, y: offscreen) }
return (view.snapshot ?? Async { callback in
addImagesForRenderedViews(view).sequence().run { views in
callback(
renderer(bounds: view.bounds, for: traits).image { ctx in
if drawHierarchyInKeyWindow {
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
} else {
view.layer.render(in: ctx.cgContext)
}
}
)
views.forEach { $0.removeFromSuperview() }
view.frame = initialFrame
}
}).map { dispose(); return $0 }
}
private let offscreen: CGFloat = 10_000
func renderer(bounds: CGRect, for traits: UITraitCollection) -> UIGraphicsImageRenderer {
let renderer: UIGraphicsImageRenderer
if #available(iOS 11.0, tvOS 11.0, *) {
renderer = UIGraphicsImageRenderer(bounds: bounds, format: .init(for: traits))
} else {
renderer = UIGraphicsImageRenderer(bounds: bounds)
}
return renderer
}
private func add(traits: UITraitCollection, viewController: UIViewController, to window: UIWindow) -> () -> Void {
let rootViewController: UIViewController
if viewController != window.rootViewController {
rootViewController = UIViewController()
rootViewController.view.backgroundColor = .clear
rootViewController.view.frame = window.frame
rootViewController.view.translatesAutoresizingMaskIntoConstraints =
viewController.view.translatesAutoresizingMaskIntoConstraints
rootViewController.preferredContentSize = rootViewController.view.frame.size
viewController.view.frame = rootViewController.view.frame
rootViewController.view.addSubview(viewController.view)
if viewController.view.translatesAutoresizingMaskIntoConstraints {
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
} else {
NSLayoutConstraint.activate([
viewController.view.topAnchor.constraint(equalTo: rootViewController.view.topAnchor),
viewController.view.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor),
viewController.view.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor),
viewController.view.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor),
])
}
rootViewController.addChild(viewController)
} else {
rootViewController = viewController
}
rootViewController.setOverrideTraitCollection(traits, forChild: viewController)
viewController.didMove(toParent: rootViewController)
window.rootViewController = rootViewController
rootViewController.beginAppearanceTransition(true, animated: false)
rootViewController.endAppearanceTransition()
rootViewController.view.setNeedsLayout()
rootViewController.view.layoutIfNeeded()
viewController.view.setNeedsLayout()
viewController.view.layoutIfNeeded()
return {
rootViewController.beginAppearanceTransition(false, animated: false)
rootViewController.endAppearanceTransition()
window.rootViewController = nil
}
}
private func getKeyWindow() -> UIWindow? {
var window: UIWindow?
if #available(iOS 13.0, *) {
window = UIApplication.sharedIfAvailable?.windows.first { $0.isKeyWindow }
} else {
window = UIApplication.sharedIfAvailable?.keyWindow
}
return window
}
private final class Window: UIWindow {
var config: ViewImageConfig
init(config: ViewImageConfig, viewController: UIViewController) {
let size = config.size ?? viewController.view.bounds.size
self.config = config
super.init(frame: .init(origin: .zero, size: size))
// NB: Safe area renders inaccurately for UI{Navigation,TabBar}Controller.
// Fixes welcome!
if viewController is UINavigationController {
self.frame.size.height -= self.config.safeArea.top
self.config.safeArea.top = 0
} else if let viewController = viewController as? UITabBarController {
self.frame.size.height -= self.config.safeArea.bottom
self.config.safeArea.bottom = 0
if viewController.selectedViewController is UINavigationController {
self.frame.size.height -= self.config.safeArea.top
self.config.safeArea.top = 0
}
}
self.isHidden = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@available(iOS 11.0, *)
override var safeAreaInsets: UIEdgeInsets {
#if os(iOS)
let removeTopInset = self.config.safeArea == .init(top: 20, left: 0, bottom: 0, right: 0)
&& self.rootViewController?.prefersStatusBarHidden ?? false
if removeTopInset { return .zero }
#endif
return self.config.safeArea
}
}
#endif
#if os(macOS)
import Cocoa
private final class ScaledWindow: NSWindow {
override var backingScaleFactor: CGFloat {
return 2
}
}
#endif
#endif
extension Array {
func sequence<A>() -> Async<[A]> where Element == Async<A> {
guard !self.isEmpty else { return Async(value: []) }
return Async<[A]> { callback in
var result = [A?](repeating: nil, count: self.count)
result.reserveCapacity(self.count)
var count = 0
zip(self.indices, self).forEach { idx, async in
async.run {
result[idx] = $0
count += 1
if count == self.count {
callback(result as! [A])
}
}
}
}
}
}
@@ -1,8 +0,0 @@
#if os(Linux)
import Foundation
public struct XCTAttachment {
public init(data: Data) {}
public init(data: Data, uniformTypeIdentifier: String) {}
}
#endif
@@ -1,125 +0,0 @@
import Foundation
struct Difference<A> {
enum Which {
case first
case second
case both
}
let elements: [A]
let which: Which
}
func diff<A: Hashable>(_ fst: [A], _ snd: [A]) -> [Difference<A>] {
var idxsOf = [A: [Int]]()
fst.enumerated().forEach { idxsOf[$1, default: []].append($0) }
let sub = snd.enumerated().reduce((overlap: [Int: Int](), fst: 0, snd: 0, len: 0)) { sub, sndPair in
(idxsOf[sndPair.element] ?? [])
.reduce((overlap: [Int: Int](), fst: sub.fst, snd: sub.snd, len: sub.len)) { innerSub, fstIdx in
var newOverlap = innerSub.overlap
newOverlap[fstIdx] = (sub.overlap[fstIdx - 1] ?? 0) + 1
if let newLen = newOverlap[fstIdx], newLen > sub.len {
return (newOverlap, fstIdx - newLen + 1, sndPair.offset - newLen + 1, newLen)
}
return (newOverlap, innerSub.fst, innerSub.snd, innerSub.len)
}
}
let (_, fstIdx, sndIdx, len) = sub
if len == 0 {
let fstDiff = fst.isEmpty ? [] : [Difference(elements: fst, which: .first)]
let sndDiff = snd.isEmpty ? [] : [Difference(elements: snd, which: .second)]
return fstDiff + sndDiff
} else {
let fstDiff = diff(Array(fst.prefix(upTo: fstIdx)), Array(snd.prefix(upTo: sndIdx)))
let midDiff = [Difference(elements: Array(fst.suffix(from: fstIdx).prefix(len)), which: .both)]
let lstDiff = diff(Array(fst.suffix(from: fstIdx + len)), Array(snd.suffix(from: sndIdx + len)))
return fstDiff + midDiff + lstDiff
}
}
let minus = ""
let plus = "+"
private let figureSpace = "\u{2007}"
struct Hunk {
let fstIdx: Int
let fstLen: Int
let sndIdx: Int
let sndLen: Int
let lines: [String]
var patchMark: String {
let fstMark = "\(minus)\(fstIdx + 1),\(fstLen)"
let sndMark = "\(plus)\(sndIdx + 1),\(sndLen)"
return "@@ \(fstMark) \(sndMark) @@"
}
// Semigroup
static func +(lhs: Hunk, rhs: Hunk) -> Hunk {
return Hunk(
fstIdx: lhs.fstIdx + rhs.fstIdx,
fstLen: lhs.fstLen + rhs.fstLen,
sndIdx: lhs.sndIdx + rhs.sndIdx,
sndLen: lhs.sndLen + rhs.sndLen,
lines: lhs.lines + rhs.lines
)
}
// Monoid
init(fstIdx: Int = 0, fstLen: Int = 0, sndIdx: Int = 0, sndLen: Int = 0, lines: [String] = []) {
self.fstIdx = fstIdx
self.fstLen = fstLen
self.sndIdx = sndIdx
self.sndLen = sndLen
self.lines = lines
}
init(idx: Int = 0, len: Int = 0, lines: [String] = []) {
self.init(fstIdx: idx, fstLen: len, sndIdx: idx, sndLen: len, lines: lines)
}
}
func chunk(diff diffs: [Difference<String>], context ctx: Int = 4) -> [Hunk] {
func prepending(_ prefix: String) -> (String) -> String {
return { prefix + $0 + ($0.hasSuffix(" ") ? "¬" : "") }
}
let changed: (Hunk) -> Bool = { $0.lines.contains(where: { $0.hasPrefix(minus) || $0.hasPrefix(plus) }) }
let (hunk, hunks) = diffs
.reduce((current: Hunk(), hunks: [Hunk]())) { cursor, diff in
let (current, hunks) = cursor
let len = diff.elements.count
switch diff.which {
case .both where len > ctx * 2:
let hunk = current + Hunk(len: ctx, lines: diff.elements.prefix(ctx).map(prepending(figureSpace)))
let next = Hunk(
fstIdx: current.fstIdx + current.fstLen + len - ctx,
fstLen: ctx,
sndIdx: current.sndIdx + current.sndLen + len - ctx,
sndLen: ctx,
lines: (diff.elements.suffix(ctx) as ArraySlice<String>).map(prepending(figureSpace))
)
return (next, changed(hunk) ? hunks + [hunk] : hunks)
case .both where current.lines.isEmpty:
let lines = (diff.elements.suffix(ctx) as ArraySlice<String>).map(prepending(figureSpace))
let count = lines.count
return (current + Hunk(idx: len - count, len: count, lines: lines), hunks)
case .both:
return (current + Hunk(len: len, lines: diff.elements.map(prepending(figureSpace))), hunks)
case .first:
return (current + Hunk(fstLen: len, lines: diff.elements.map(prepending(minus))), hunks)
case .second:
return (current + Hunk(sndLen: len, lines: diff.elements.map(prepending(plus))), hunks)
}
}
return changed(hunk) ? hunks + [hunk] : hunks
}
@@ -1,34 +0,0 @@
import Foundation
import XCTest
/// The ability to compare `Value`s and convert them to and from `Data`.
public struct Diffing<Value> {
/// Converts a value _to_ data.
public var toData: (Value) -> Data
/// Produces a value _from_ data.
public var fromData: (Data) -> Value
/// Compares two values. If the values do not match, returns a failure message and artifacts describing the failure.
public var diff: (Value, Value) -> (String, [XCTAttachment])?
/// Creates a new `Diffing` on `Value`.
///
/// - Parameters:
/// - toData: A function used to convert a value _to_ data.
/// - value: A value to convert into data.
/// - fromData: A function used to produce a value _from_ data.
/// - data: Data to convert into a value.
/// - diff: A function used to compare two values. If the values do not match, returns a failure message and artifacts describing the failure.
/// - lhs: A value to compare.
/// - rhs: Another value to compare.
public init(
toData: @escaping (_ value: Value) -> Data,
fromData: @escaping (_ data: Data) -> Value,
diff: @escaping (_ lhs: Value, _ rhs: Value) -> (String, [XCTAttachment])?
) {
self.toData = toData
self.fromData = fromData
self.diff = diff
}
}
@@ -1,27 +0,0 @@
import Foundation
import XCTest
extension Snapshotting {
/// Transforms an existing snapshot strategy into one that waits for some amount of time before taking the snapshot. This can be useful for waiting for animations to complete or for UIKit events to finish (i.e. waiting for a UINavigationController to push a child onto the stack).
/// - Parameters:
/// - duration: The amount of time to wait before taking the snapshot.
/// - strategy: The snapshot to invoke after the specified amount of time has passed.
public static func wait(
for duration: TimeInterval,
on strategy: Snapshotting
) -> Snapshotting {
return Snapshotting(
pathExtension: strategy.pathExtension,
diffing: strategy.diffing,
asyncSnapshot: { value in
Async { callback in
let expectation = XCTestExpectation(description: "Wait")
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
expectation.fulfill()
}
_ = XCTWaiter.wait(for: [expectation], timeout: duration + 1)
strategy.snapshot(value).run(callback)
}
})
}
}
@@ -1,4 +0,0 @@
import XCTest
@available(swift, obsoleted: 5.0, renamed: "XCTestCase", message: "Please use XCTestCase instead")
public typealias SnapshotTestCase = XCTestCase
@@ -1,106 +0,0 @@
import Foundation
import XCTest
/// A type representing the ability to transform a snapshottable value into a diffable format (like text or an image) for snapshot testing.
public struct Snapshotting<Value, Format> {
/// The path extension applied to references saved to disk.
public var pathExtension: String?
/// How the snapshot format is diffed and converted to and from data.
public var diffing: Diffing<Format>
/// How a value is transformed into a diffable snapshot format.
public var snapshot: (Value) -> Async<Format>
/// Creates a snapshot strategy.
///
/// - Parameters:
/// - pathExtension: The path extension applied to references saved to disk.
/// - diffing: How to diff and convert the snapshot format to and from data.
/// - snapshot: An asynchronous transform function from a value into a diffable snapshot format.
/// - value: A value to be converted.
public init(
pathExtension: String?,
diffing: Diffing<Format>,
asyncSnapshot: @escaping (_ value: Value) -> Async<Format>
) {
self.pathExtension = pathExtension
self.diffing = diffing
self.snapshot = asyncSnapshot
}
/// Creates a snapshot strategy.
///
/// - Parameters:
/// - pathExtension: The path extension applied to references saved to disk.
/// - diffing: How to diff and convert the snapshot format to and from data.
/// - snapshot: A transform function from a value into a diffable snapshot format.
/// - value: A snapshot value to be converted.
public init(
pathExtension: String?,
diffing: Diffing<Format>,
snapshot: @escaping (_ value: Value) -> Format
) {
self.init(pathExtension: pathExtension, diffing: diffing) {
Async(value: snapshot($0))
}
}
/// Transforms a strategy on `Value`s into a strategy on `NewValue`s through a function `(NewValue) -> Value`.
///
/// This is the most important operation for transforming existing strategies into new strategies. It allows you to transform a `Snapshotting<Value, Format>` into a `Snapshotting<NewValue, Format>` by pulling it back along a function `(NewValue) -> Value`. Notice that the function must go in the direction `(NewValue) -> Value` even though we are transforming in the other direction `(Snapshotting<Value, Format>) -> Snapshotting<NewValue, Format>`.
///
/// A simple example of this is to `pullback` the snapshot strategy on `UIView`s to work on `UIViewController`s:
///
/// let strategy = Snapshotting<UIView, UIImage>.image.pullback { (vc: UIViewController) in
/// return vc.view
/// }
///
/// Here we took the strategy that snapshots `UIView`s as `UIImage`s and pulled it back to work on `UIViewController`s by using the function `(UIViewController) -> UIView` that simply plucks the view out of the controller.
///
/// Nearly every snapshot strategy provided in this library is a pullback of some base strategy, which shows just how important this operation is.
///
/// - Parameters:
/// - transform: A transform function from `NewValue` into `Value`.
/// - otherValue: A value to be transformed.
public func pullback<NewValue>(_ transform: @escaping (_ otherValue: NewValue) -> Value) -> Snapshotting<NewValue, Format> {
return self.asyncPullback { newValue in Async(value: transform(newValue)) }
}
/// Transforms a strategy on `Value`s into a strategy on `NewValue`s through a function `(NewValue) -> Async<Value>`.
///
/// See the documention of `pullback` for a full description of how pullbacks works. This operation differs from `pullback` in that it allows you to use a transformation `(NewValue) -> Async<Value>`, which is necessary when your transformation needs to perform some asynchronous work.
///
/// - Parameters:
/// - transform: A transform function from `NewValue` into `Async<Value>`.
/// - otherValue: A value to be transformed.
public func asyncPullback<NewValue>(_ transform: @escaping (_ otherValue: NewValue) -> Async<Value>)
-> Snapshotting<NewValue, Format> {
return Snapshotting<NewValue, Format>(
pathExtension: self.pathExtension,
diffing: self.diffing
) { newValue in
return .init { callback in
transform(newValue).run { value in
self.snapshot(value).run { snapshot in
callback(snapshot)
}
}
}
}
}
}
/// A snapshot strategy where the type being snapshot is also a diffable type.
public typealias SimplySnapshotting<Format> = Snapshotting<Format, Format>
extension Snapshotting where Value == Format {
public init(pathExtension: String?, diffing: Diffing<Format>) {
self.init(
pathExtension: pathExtension,
diffing: diffing,
snapshot: { $0 }
)
}
}
@@ -1,142 +0,0 @@
import Foundation
extension Snapshotting where Format == String {
/// A snapshot strategy for comparing any structure based on a sanitized text dump.
public static var dump: Snapshotting {
return SimplySnapshotting.lines.pullback { snap($0) }
}
}
private func snap<T>(_ value: T, name: String? = nil, indent: Int = 0) -> String {
let indentation = String(repeating: " ", count: indent)
let mirror = Mirror(reflecting: value)
var children = mirror.children
let count = children.count
let bullet = count == 0 ? "-" : ""
let description: String
switch (value, mirror.displayStyle) {
case (_, .collection?):
description = count == 1 ? "1 element" : "\(count) elements"
case (_, .dictionary?):
description = count == 1 ? "1 key/value pair" : "\(count) key/value pairs"
children = sort(children)
case (_, .set?):
description = count == 1 ? "1 member" : "\(count) members"
children = sort(children)
case (_, .tuple?):
description = count == 1 ? "(1 element)" : "(\(count) elements)"
case (_, .optional?):
let subjectType = String(describing: mirror.subjectType)
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
description = count == 0 ? "\(subjectType).none" : "\(subjectType)"
case (let value as AnySnapshotStringConvertible, _) where type(of: value).renderChildren:
description = value.snapshotDescription
case (let value as AnySnapshotStringConvertible, _):
return "\(indentation)- \(name.map { "\($0): " } ?? "")\(value.snapshotDescription)\n"
case (let value as CustomStringConvertible, _):
description = value.description
case (_, .class?), (_, .struct?):
description = String(describing: mirror.subjectType)
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
children = sort(children)
case (_, .enum?):
let subjectType = String(describing: mirror.subjectType)
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
description = count == 0 ? "\(subjectType).\(value)" : "\(subjectType)"
case (let value, _):
description = String(describing: value)
}
let lines = ["\(indentation)\(bullet) \(name.map { "\($0): " } ?? "")\(description)\n"]
+ children.map { snap($1, name: $0, indent: indent + 2) }
return lines.joined()
}
private func sort(_ children: Mirror.Children) -> Mirror.Children {
return .init(
children
.map({ (child: $0, snap: snap($0)) })
.sorted(by: { $0.snap < $1.snap })
.map({ $0.child })
)
}
/// A type with a customized snapshot dump representation.
///
/// Types that conform to the `AnySnapshotStringConvertible` protocol can provide their own representation to be used when converting an instance to a `dump`-based snapshot.
public protocol AnySnapshotStringConvertible {
/// Whether or not to dump child nodes (defaults to `false`).
static var renderChildren: Bool { get }
/// A textual snapshot dump representation of this instance.
var snapshotDescription: String { get }
}
extension AnySnapshotStringConvertible {
public static var renderChildren: Bool {
return false
}
}
extension Character: AnySnapshotStringConvertible {
public var snapshotDescription: String {
return self.debugDescription
}
}
extension Data: AnySnapshotStringConvertible {
public var snapshotDescription: String {
return self.debugDescription
}
}
extension Date: AnySnapshotStringConvertible {
public var snapshotDescription: String {
return snapshotDateFormatter.string(from: self)
}
}
extension NSObject: AnySnapshotStringConvertible {
#if canImport(ObjectiveC)
@objc open var snapshotDescription: String {
return purgePointers(self.debugDescription)
}
#else
open var snapshotDescription: String {
return purgePointers(self.debugDescription)
}
#endif
}
extension String: AnySnapshotStringConvertible {
public var snapshotDescription: String {
return self.debugDescription
}
}
extension Substring: AnySnapshotStringConvertible {
public var snapshotDescription: String {
return self.debugDescription
}
}
extension URL: AnySnapshotStringConvertible {
public var snapshotDescription: String {
return self.debugDescription
}
}
private let snapshotDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(abbreviation: "UTC")
return formatter
}()
func purgePointers(_ string: String) -> String {
return string.replacingOccurrences(of: ":?\\s*0x[\\da-f]+(\\s*)", with: "$1", options: .regularExpression)
}
@@ -1,49 +0,0 @@
#if os(macOS)
import Cocoa
extension Snapshotting where Value == CALayer, Format == NSImage {
/// A snapshot strategy for comparing layers based on pixel equality.
public static var image: Snapshotting {
return .image(precision: 1)
}
/// A snapshot strategy for comparing layers based on pixel equality.
///
/// - Parameter precision: The percentage of pixels that must match.
public static func image(precision: Float) -> Snapshotting {
return SimplySnapshotting.image(precision: precision).pullback { layer in
let image = NSImage(size: layer.bounds.size)
image.lockFocus()
let context = NSGraphicsContext.current!.cgContext
layer.setNeedsLayout()
layer.layoutIfNeeded()
layer.render(in: context)
image.unlockFocus()
return image
}
}
}
#elseif os(iOS) || os(tvOS)
import UIKit
extension Snapshotting where Value == CALayer, Format == UIImage {
/// A snapshot strategy for comparing layers based on pixel equality.
public static var image: Snapshotting {
return .image()
}
/// A snapshot strategy for comparing layers based on pixel equality.
///
/// - Parameter precision: The percentage of pixels that must match.
public static func image(precision: Float = 1, traits: UITraitCollection = .init())
-> Snapshotting {
return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).pullback { layer in
renderer(bounds: layer.bounds, for: traits).image { ctx in
layer.setNeedsLayout()
layer.layoutIfNeeded()
layer.render(in: ctx.cgContext)
}
}
}
}
#endif
@@ -1,126 +0,0 @@
#if os(macOS)
import Cocoa
extension Snapshotting where Value == CGPath, Format == NSImage {
/// A snapshot strategy for comparing bezier paths based on pixel equality.
public static var image: Snapshotting {
return .image()
}
/// A snapshot strategy for comparing bezier paths based on pixel equality.
///
/// - Parameter precision: The percentage of pixels that must match.
public static func image(precision: Float = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting {
return SimplySnapshotting.image(precision: precision).pullback { path in
let bounds = path.boundingBoxOfPath
var transform = CGAffineTransform(translationX: -bounds.origin.x, y: -bounds.origin.y)
let path = path.copy(using: &transform)!
let image = NSImage(size: bounds.size)
image.lockFocus()
let context = NSGraphicsContext.current!.cgContext
context.addPath(path)
context.drawPath(using: drawingMode)
image.unlockFocus()
return image
}
}
}
#elseif os(iOS) || os(tvOS)
import UIKit
extension Snapshotting where Value == CGPath, Format == UIImage {
/// A snapshot strategy for comparing bezier paths based on pixel equality.
public static var image: Snapshotting {
return .image()
}
/// A snapshot strategy for comparing bezier paths based on pixel equality.
///
/// - Parameter precision: The percentage of pixels that must match.
public static func image(precision: Float = 1, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting {
return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in
let bounds = path.boundingBoxOfPath
let format: UIGraphicsImageRendererFormat
if #available(iOS 11.0, tvOS 11.0, *) {
format = UIGraphicsImageRendererFormat.preferred()
} else {
format = UIGraphicsImageRendererFormat.default()
}
format.scale = scale
return UIGraphicsImageRenderer(bounds: bounds, format: format).image { ctx in
let cgContext = ctx.cgContext
cgContext.addPath(path)
cgContext.drawPath(using: drawingMode)
}
}
}
}
#endif
#if os(macOS) || os(iOS) || os(tvOS)
@available(iOS 11.0, OSX 10.13, tvOS 11.0, *)
extension Snapshotting where Value == CGPath, Format == String {
/// A snapshot strategy for comparing bezier paths based on element descriptions.
public static var elementsDescription: Snapshotting {
return .elementsDescription(numberFormatter: defaultNumberFormatter)
}
/// A snapshot strategy for comparing bezier paths based on element descriptions.
///
/// - Parameter numberFormatter: The number formatter used for formatting points.
public static func elementsDescription(numberFormatter: NumberFormatter) -> Snapshotting {
let namesByType: [CGPathElementType: String] = [
.moveToPoint: "MoveTo",
.addLineToPoint: "LineTo",
.addQuadCurveToPoint: "QuadCurveTo",
.addCurveToPoint: "CurveTo",
.closeSubpath: "Close",
]
let numberOfPointsByType: [CGPathElementType: Int] = [
.moveToPoint: 1,
.addLineToPoint: 1,
.addQuadCurveToPoint: 2,
.addCurveToPoint: 3,
.closeSubpath: 0,
]
return SimplySnapshotting.lines.pullback { path in
var string: String = ""
path.applyWithBlock { elementPointer in
let element = elementPointer.pointee
let name = namesByType[element.type] ?? "Unknown"
if element.type == .moveToPoint && !string.isEmpty {
string += "\n"
}
string += name
if let numberOfPoints = numberOfPointsByType[element.type] {
let points = UnsafeBufferPointer(start: element.points, count: numberOfPoints)
string += " " + points.map { point in
let x = numberFormatter.string(from: point.x as NSNumber)!
let y = numberFormatter.string(from: point.y as NSNumber)!
return "(\(x), \(y))"
}.joined(separator: " ")
}
string += "\n"
}
return string
}
}
}
private let defaultNumberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.minimumFractionDigits = 1
numberFormatter.maximumFractionDigits = 3
return numberFormatter
}()
#endif
@@ -1,25 +0,0 @@
extension Snapshotting where Value: CaseIterable, Format == String {
/// A strategy for snapshotting the output for every input of a function. The format of the snapshot
/// is a comma-separated value (CSV) file that shows the mapping of inputs to outputs.
///
/// Parameter witness: A snapshotting value on the output of the function to be snapshot.
/// Returns: A snapshot strategy on functions (Value) -> A that feeds every possible input into the
/// function and records the output into a CSV file.
public static func `func`<A>(into witness: Snapshotting<A, Format>) -> Snapshotting<(Value) -> A, Format> {
var snapshotting = Snapshotting<String, String>.lines.asyncPullback { (f: (Value) -> A) in
Value.allCases.map { input in
witness.snapshot(f(input))
.map { (input, $0) }
}
.sequence()
.map { rows in
rows.map { "\"\($0)\",\"\($1)\"" }
.joined(separator: "\n")
}
}
snapshotting.pathExtension = "csv"
return snapshotting
}
}

Some files were not shown because too many files have changed in this diff Show More