Merge pull request #35 from fxm90/feature/version-4

Version 4.0
This commit is contained in:
Felix M.
2026-01-31 10:45:51 +01:00
committed by GitHub
245 changed files with 4243 additions and 14719 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
+41 -55
View File
@@ -2,75 +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:
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 * * *'
# Use latest Xcode version.
#
# Source:
# - https://www.jessesquires.com/blog/2020/01/06/selecting-an-xcode-version-on-github-ci/
# - https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#xcode
env:
DEVELOPER_DIR: /Applications/Xcode_14.0.app/Contents/Developer
# 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-12
# 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 lint
- 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 Carthage support.
working-directory: ./Example
run: bundle exec fastlane verify_carthage
- 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@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.5
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

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

+154 -56
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,201 +8,298 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [Unreleased]
## [3.0.0] - 2022-07-10
## [4.0.0] - 2026-01-31
### Changed
- Use `Combine` instead of dependency `LightweightObservable`. Therefore the minimum required iOS version increased to 13. Otherwise there are no breaking changes.
- 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 wrong minimum iOS deployment target when using Carthage.
- 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.
- 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"
- 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.
- 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"
- 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])
- 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
- Add `GradientLoadingBarView` to support SwiftUI
## [2.2.5] - 2022-27-03
### Fixed
- Fixed incorrect layout on `NotchGradientLoadingBar` when using "iPhone 11"
- 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"
- 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))
- 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])
- 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
- 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
- Fixed 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
- Fixed optical animation issue
- Fixed 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/3.0.0...main
[2.3.5]: https://github.com/fxm90/GradientLoadingBar/compare/2.3.5...3.0.0
[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
View File
-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
-26
View File
@@ -1,26 +0,0 @@
#
# .swiftlint.yml
# Created by Felix Mau (https://felix.hamburg)
#
opt_in_rules:
- file_header
file_header:
required_pattern: |
\/\/
\/\/ SWIFTLINT_CURRENT_FILENAME
\/\/ (GradientLoadingBar|Example|ExampleTests|ExampleSnapshotTests)
\/\/
\/\/ 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
-586
View File
@@ -1,586 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
DC4E50A16A3B84900FBD1981 /* Pods_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12DDBD589B02F5D3C9C3AF69 /* Pods_Example.framework */; };
DCA9833A27D68AD400D6EA30 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9833927D68AD400D6EA30 /* ExampleApp.swift */; };
DCA9833E27D68AD600D6EA30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCA9833D27D68AD600D6EA30 /* Assets.xcassets */; };
DCA9834127D68AD600D6EA30 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCA9834027D68AD600D6EA30 /* Preview Assets.xcassets */; };
DCA9836927D68E3500D6EA30 /* StoryboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9836827D68E3500D6EA30 /* StoryboardView.swift */; };
DCA9836C27D6905000D6EA30 /* EntryPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9836B27D6905000D6EA30 /* EntryPointView.swift */; };
DCA9837127D6912300D6EA30 /* BasicExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9837027D6912300D6EA30 /* BasicExampleViewController.swift */; };
DCA9837627D693BB00D6EA30 /* BasicExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCA9837527D693BB00D6EA30 /* BasicExample.storyboard */; };
DCA9837827D6951C00D6EA30 /* SafeAreaExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9837727D6951C00D6EA30 /* SafeAreaExampleViewController.swift */; };
DCA9837A27D6958500D6EA30 /* SafeAreaExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCA9837927D6958500D6EA30 /* SafeAreaExample.storyboard */; };
DCA9838727D6A44700D6EA30 /* NavigationBarExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9838627D6A44700D6EA30 /* NavigationBarExampleViewController.swift */; };
DCA9838927D6A55A00D6EA30 /* NavigationBarExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCA9838827D6A55A00D6EA30 /* NavigationBarExample.storyboard */; };
DCA9838B27D6A74800D6EA30 /* SwiftUIExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9838A27D6A74800D6EA30 /* SwiftUIExampleView.swift */; };
F50E4C7E28061655001232D3 /* AdvancedExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F50E4C7D28061655001232D3 /* AdvancedExample.storyboard */; };
F50E4C8028061715001232D3 /* AdvancedExampleTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F50E4C7F28061715001232D3 /* AdvancedExampleTableViewController.swift */; };
F5C7E8882809CDEE00036B8E /* CustomColorsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C7E8872809CDEE00036B8E /* CustomColorsTableViewCell.swift */; };
F5C7E88A2809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C7E8892809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift */; };
F5C7E88C2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C7E88B2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift */; };
F5C7E88F2809CF1200036B8E /* BorderedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C7E88E2809CF1200036B8E /* BorderedButton.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
12DDBD589B02F5D3C9C3AF69 /* Pods_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3299012A62A2E4310D02CD78 /* Pods-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.release.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.release.xcconfig"; sourceTree = "<group>"; };
DCA9833627D68AD400D6EA30 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
DCA9833927D68AD400D6EA30 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = "<group>"; };
DCA9833D27D68AD600D6EA30 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
DCA9834027D68AD600D6EA30 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
DCA9836827D68E3500D6EA30 /* StoryboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardView.swift; sourceTree = "<group>"; };
DCA9836B27D6905000D6EA30 /* EntryPointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryPointView.swift; sourceTree = "<group>"; };
DCA9837027D6912300D6EA30 /* BasicExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicExampleViewController.swift; sourceTree = "<group>"; };
DCA9837527D693BB00D6EA30 /* BasicExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BasicExample.storyboard; sourceTree = "<group>"; };
DCA9837727D6951C00D6EA30 /* SafeAreaExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeAreaExampleViewController.swift; sourceTree = "<group>"; };
DCA9837927D6958500D6EA30 /* SafeAreaExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SafeAreaExample.storyboard; sourceTree = "<group>"; };
DCA9838627D6A44700D6EA30 /* NavigationBarExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarExampleViewController.swift; sourceTree = "<group>"; };
DCA9838827D6A55A00D6EA30 /* NavigationBarExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NavigationBarExample.storyboard; sourceTree = "<group>"; };
DCA9838A27D6A74800D6EA30 /* SwiftUIExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExampleView.swift; sourceTree = "<group>"; };
F50E4C7D28061655001232D3 /* AdvancedExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = AdvancedExample.storyboard; sourceTree = "<group>"; };
F50E4C7F28061715001232D3 /* AdvancedExampleTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedExampleTableViewController.swift; sourceTree = "<group>"; };
F5C7E8872809CDEE00036B8E /* CustomColorsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomColorsTableViewCell.swift; sourceTree = "<group>"; };
F5C7E8892809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceBuilderSetupTableViewCell.swift; sourceTree = "<group>"; };
F5C7E88B2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSuperviewTableViewCell.swift; sourceTree = "<group>"; };
F5C7E88E2809CF1200036B8E /* BorderedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedButton.swift; sourceTree = "<group>"; };
FAA1A12E84EBC1F48401EB10 /* Pods-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.debug.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
DCA9833327D68AD400D6EA30 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DC4E50A16A3B84900FBD1981 /* Pods_Example.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
33D2CE217238EAE7A5515634 /* Frameworks */ = {
isa = PBXGroup;
children = (
12DDBD589B02F5D3C9C3AF69 /* Pods_Example.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
A6AC3866473F1A669D221CA4 /* Pods */ = {
isa = PBXGroup;
children = (
FAA1A12E84EBC1F48401EB10 /* Pods-Example.debug.xcconfig */,
3299012A62A2E4310D02CD78 /* Pods-Example.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
DCA9832D27D68AD400D6EA30 = {
isa = PBXGroup;
children = (
DCA9833827D68AD400D6EA30 /* Example */,
DCA9833727D68AD400D6EA30 /* Products */,
A6AC3866473F1A669D221CA4 /* Pods */,
33D2CE217238EAE7A5515634 /* Frameworks */,
);
sourceTree = "<group>";
};
DCA9833727D68AD400D6EA30 /* Products */ = {
isa = PBXGroup;
children = (
DCA9833627D68AD400D6EA30 /* Example.app */,
);
name = Products;
sourceTree = "<group>";
};
DCA9833827D68AD400D6EA30 /* Example */ = {
isa = PBXGroup;
children = (
DCA9836727D68E3500D6EA30 /* Helper */,
DCA9836A27D68E3C00D6EA30 /* Scenes */,
DCA9833927D68AD400D6EA30 /* ExampleApp.swift */,
DCA9833D27D68AD600D6EA30 /* Assets.xcassets */,
DCA9833F27D68AD600D6EA30 /* Preview Content */,
);
path = Example;
sourceTree = "<group>";
};
DCA9833F27D68AD600D6EA30 /* Preview Content */ = {
isa = PBXGroup;
children = (
DCA9834027D68AD600D6EA30 /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
DCA9836727D68E3500D6EA30 /* Helper */ = {
isa = PBXGroup;
children = (
DCA9836827D68E3500D6EA30 /* StoryboardView.swift */,
);
path = Helper;
sourceTree = "<group>";
};
DCA9836A27D68E3C00D6EA30 /* Scenes */ = {
isa = PBXGroup;
children = (
DCA9836E27D6909A00D6EA30 /* AdvancedExample */,
DCA9836D27D6908F00D6EA30 /* BasicExample */,
DCA9837427D691C600D6EA30 /* NavigationBarExample */,
DCA9837227D691A200D6EA30 /* SafeAreaExample */,
DCA9836F27D690A400D6EA30 /* SwiftUIExample */,
DCA9836B27D6905000D6EA30 /* EntryPointView.swift */,
);
path = Scenes;
sourceTree = "<group>";
};
DCA9836D27D6908F00D6EA30 /* BasicExample */ = {
isa = PBXGroup;
children = (
DCA9837527D693BB00D6EA30 /* BasicExample.storyboard */,
DCA9837027D6912300D6EA30 /* BasicExampleViewController.swift */,
);
path = BasicExample;
sourceTree = "<group>";
};
DCA9836E27D6909A00D6EA30 /* AdvancedExample */ = {
isa = PBXGroup;
children = (
F5C7E8862809CDE200036B8E /* Cells */,
F5C7E88D2809CF0700036B8E /* Views */,
F50E4C7D28061655001232D3 /* AdvancedExample.storyboard */,
F50E4C7F28061715001232D3 /* AdvancedExampleTableViewController.swift */,
);
path = AdvancedExample;
sourceTree = "<group>";
};
DCA9836F27D690A400D6EA30 /* SwiftUIExample */ = {
isa = PBXGroup;
children = (
DCA9838A27D6A74800D6EA30 /* SwiftUIExampleView.swift */,
);
path = SwiftUIExample;
sourceTree = "<group>";
};
DCA9837227D691A200D6EA30 /* SafeAreaExample */ = {
isa = PBXGroup;
children = (
DCA9837927D6958500D6EA30 /* SafeAreaExample.storyboard */,
DCA9837727D6951C00D6EA30 /* SafeAreaExampleViewController.swift */,
);
path = SafeAreaExample;
sourceTree = "<group>";
};
DCA9837427D691C600D6EA30 /* NavigationBarExample */ = {
isa = PBXGroup;
children = (
DCA9838827D6A55A00D6EA30 /* NavigationBarExample.storyboard */,
DCA9838627D6A44700D6EA30 /* NavigationBarExampleViewController.swift */,
);
path = NavigationBarExample;
sourceTree = "<group>";
};
F5C7E8862809CDE200036B8E /* Cells */ = {
isa = PBXGroup;
children = (
F5C7E8872809CDEE00036B8E /* CustomColorsTableViewCell.swift */,
F5C7E8892809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift */,
F5C7E88B2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift */,
);
path = Cells;
sourceTree = "<group>";
};
F5C7E88D2809CF0700036B8E /* Views */ = {
isa = PBXGroup;
children = (
F5C7E88E2809CF1200036B8E /* BorderedButton.swift */,
);
path = Views;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
DCA9833527D68AD400D6EA30 /* Example */ = {
isa = PBXNativeTarget;
buildConfigurationList = DCA9835A27D68AD600D6EA30 /* Build configuration list for PBXNativeTarget "Example" */;
buildPhases = (
52082F0E9B4B579867A82146 /* [CP] Check Pods Manifest.lock */,
DCA9833227D68AD400D6EA30 /* Sources */,
DCA9833327D68AD400D6EA30 /* Frameworks */,
DCA9833427D68AD400D6EA30 /* Resources */,
C56CF067D28EB40785524B49 /* [CP] Embed Pods Frameworks */,
DCA9836527D68D6400D6EA30 /* SwiftFormat */,
DCA9836627D68D7000D6EA30 /* SwiftLint */,
);
buildRules = (
);
dependencies = (
);
name = Example;
productName = Example;
productReference = DCA9833627D68AD400D6EA30 /* Example.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
DCA9832E27D68AD400D6EA30 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1320;
TargetAttributes = {
DCA9833527D68AD400D6EA30 = {
CreatedOnToolsVersion = 13.2.1;
};
};
};
buildConfigurationList = DCA9833127D68AD400D6EA30 /* Build configuration list for PBXProject "Example" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = DCA9832D27D68AD400D6EA30;
productRefGroup = DCA9833727D68AD400D6EA30 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
DCA9833527D68AD400D6EA30 /* Example */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
DCA9833427D68AD400D6EA30 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DCA9834127D68AD600D6EA30 /* Preview Assets.xcassets in Resources */,
F50E4C7E28061655001232D3 /* AdvancedExample.storyboard in Resources */,
DCA9837A27D6958500D6EA30 /* SafeAreaExample.storyboard in Resources */,
DCA9837627D693BB00D6EA30 /* BasicExample.storyboard in Resources */,
DCA9833E27D68AD600D6EA30 /* Assets.xcassets in Resources */,
DCA9838927D6A55A00D6EA30 /* NavigationBarExample.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
52082F0E9B4B579867A82146 /* [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-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;
};
C56CF067D28EB40785524B49 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
DCA9836527D68D6400D6EA30 /* SwiftFormat */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = SwiftFormat;
outputFileListPaths = (
);
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";
};
DCA9836627D68D7000D6EA30 /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = SwiftLint;
outputFileListPaths = (
);
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";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
DCA9833227D68AD400D6EA30 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DCA9837127D6912300D6EA30 /* BasicExampleViewController.swift in Sources */,
DCA9833A27D68AD400D6EA30 /* ExampleApp.swift in Sources */,
F5C7E88F2809CF1200036B8E /* BorderedButton.swift in Sources */,
F5C7E8882809CDEE00036B8E /* CustomColorsTableViewCell.swift in Sources */,
DCA9836927D68E3500D6EA30 /* StoryboardView.swift in Sources */,
F5C7E88C2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift in Sources */,
F5C7E88A2809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift in Sources */,
F50E4C8028061715001232D3 /* AdvancedExampleTableViewController.swift in Sources */,
DCA9836C27D6905000D6EA30 /* EntryPointView.swift in Sources */,
DCA9837827D6951C00D6EA30 /* SafeAreaExampleViewController.swift in Sources */,
DCA9838727D6A44700D6EA30 /* NavigationBarExampleViewController.swift in Sources */,
DCA9838B27D6A74800D6EA30 /* SwiftUIExampleView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
DCA9835827D68AD600D6EA30 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
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;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
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;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
DCA9835927D68AD600D6EA30 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_CXX_LIBRARY = "libc++";
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";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
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 = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
DCA9835B27D68AD600D6EA30 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FAA1A12E84EBC1F48401EB10 /* Pods-Example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
DEVELOPMENT_TEAM = F63GJC2AN7;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.gradientLoadingBar.example.Example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
DCA9835C27D68AD600D6EA30 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3299012A62A2E4310D02CD78 /* Pods-Example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
DEVELOPMENT_TEAM = F63GJC2AN7;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.gradientLoadingBar.example.Example;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
DCA9833127D68AD400D6EA30 /* Build configuration list for PBXProject "Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DCA9835827D68AD600D6EA30 /* Debug */,
DCA9835927D68AD600D6EA30 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
DCA9835A27D68AD600D6EA30 /* Build configuration list for PBXNativeTarget "Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DCA9835B27D68AD600D6EA30 /* Debug */,
DCA9835C27D68AD600D6EA30 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = DCA9832E27D68AD400D6EA30 /* Project object */;
}
@@ -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>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Example.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,98 +0,0 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
-31
View File
@@ -1,31 +0,0 @@
//
// ExampleApp.swift
// Example
//
// Created by Felix Mau on 07.03.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import SwiftUI
@main
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
EntryPointView()
.onAppear {
// Use iOS 13 navigation bar appearance.
//
// While the entry point scene looks better with an opaque navigation background, the child scenes look
// better with the default navigation background. As we can't update the appearance per scene we fallback
// to an appearance with the default background here.
//
// Based on: https://sarunw.com/posts/uinavigationbar-changes-in-ios13/
let appearance = UINavigationBarAppearance()
appearance.configureWithDefaultBackground()
UINavigationBar.appearance().scrollEdgeAppearance = appearance
}
}
}
}
@@ -1,29 +0,0 @@
//
// StoryboardView.swift
// Example
//
// Created by Felix Mau on 02.02.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import SwiftUI
struct StoryboardView: UIViewControllerRepresentable {
// MARK: - Public properties
let name: String
// MARK: - Public methods
func makeUIViewController(context _: Context) -> UIViewController {
let storyboard = UIStoryboard(name: name, bundle: nil)
guard let viewController = storyboard.instantiateInitialViewController() else {
fatalError("⚠️ – Could not instantiate initial view controller for storyboard with name `\(name)`.")
}
return viewController
}
func updateUIViewController(_: UIViewController, context _: Context) {}
}
@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,169 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ZAD-6Y-gxa">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Advanced Example Table View Controller-->
<scene sceneID="iwG-zT-VrO">
<objects>
<tableViewController id="ZAD-6Y-gxa" customClass="AdvancedExampleTableViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" id="2vt-bX-4b7">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="CustomColorsTableViewCell" id="qq9-jr-nNk" customClass="CustomColorsTableViewCell" customModule="Example" customModuleProvider="target">
<rect key="frame" x="20" y="49.5" width="374" height="120.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qq9-jr-nNk" id="60G-w2-6BV">
<rect key="frame" x="0.0" y="0.0" width="374" height="120.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="zN2-pr-LT6">
<rect key="frame" x="16" y="16" width="342" height="88.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Example showing how to provide custom gradient colors." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lbJ-pG-fGJ">
<rect key="frame" x="0.0" y="0.0" width="342" height="36"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uL7-lu-aRP" userLabel="Toggle Custom Colors Button">
<rect key="frame" x="0.0" y="48" width="342" height="40.5"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="jZd-Vw-MAb"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Toggle Custom Colors ↑">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="customColorsButtonTouchUpInside:" destination="qq9-jr-nNk" eventType="touchUpInside" id="XyI-4Q-sja"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="zN2-pr-LT6" firstAttribute="leading" secondItem="60G-w2-6BV" secondAttribute="leading" constant="16" id="18p-RW-IFT"/>
<constraint firstAttribute="bottom" secondItem="zN2-pr-LT6" secondAttribute="bottom" constant="16" id="6LP-eX-o55"/>
<constraint firstItem="zN2-pr-LT6" firstAttribute="top" secondItem="60G-w2-6BV" secondAttribute="top" constant="16" id="RzJ-mr-IHP"/>
<constraint firstAttribute="trailing" secondItem="zN2-pr-LT6" secondAttribute="trailing" constant="16" id="h7A-pm-6hb"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="InterfaceBuilderSetupTableViewCell" id="yBS-fc-uJy" customClass="InterfaceBuilderSetupTableViewCell" customModule="Example" customModuleProvider="target">
<rect key="frame" x="20" y="170" width="374" height="83.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="yBS-fc-uJy" id="Tid-am-qRM">
<rect key="frame" x="0.0" y="0.0" width="374" height="83.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="KTF-6b-NmT">
<rect key="frame" x="16" y="16" width="342" height="51.5"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bCd-m6-FJm" customClass="GradientActivityIndicatorView" customModule="GradientLoadingBar">
<rect key="frame" x="0.0" y="0.0" width="342" height="3"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="3" id="FQe-RY-KH7"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TpO-6A-9Bl" userLabel="Toggle IB Setup Button">
<rect key="frame" x="0.0" y="11" width="342" height="40.5"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="QLO-3y-Y29"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Toggle IB Setup 🎨">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="toggleIBSetupButtonTouchUpInside:" destination="yBS-fc-uJy" eventType="touchUpInside" id="WLP-Dw-VmD"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="KTF-6b-NmT" firstAttribute="top" secondItem="Tid-am-qRM" secondAttribute="top" constant="16" id="2Bc-ex-Ggk"/>
<constraint firstItem="KTF-6b-NmT" firstAttribute="leading" secondItem="Tid-am-qRM" secondAttribute="leading" constant="16" id="Aka-HV-cVb"/>
<constraint firstAttribute="bottom" secondItem="KTF-6b-NmT" secondAttribute="bottom" constant="16" id="MH0-A6-FGH"/>
<constraint firstAttribute="trailing" secondItem="KTF-6b-NmT" secondAttribute="trailing" constant="16" id="pLD-Mq-O5j"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="gradientActivityIndicator" destination="bCd-m6-FJm" id="z4t-IP-gqw"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="CustomSuperviewViewCell" id="ah2-WM-rFd" customClass="CustomSuperviewTableViewCell" customModule="Example" customModuleProvider="target">
<rect key="frame" x="20" y="253.5" width="374" height="120.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ah2-WM-rFd" id="2Sj-GS-zEL">
<rect key="frame" x="0.0" y="0.0" width="374" height="120.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="ja4-s5-ZP7">
<rect key="frame" x="20" y="16" width="334" height="88.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Example showing how to add the loading bar as a subview." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c7X-oS-Zcv">
<rect key="frame" x="0.0" y="0.0" width="334" height="36"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZxZ-rY-wUa" userLabel="Toggle Custom Superview Button" customClass="BorderedButton" customModule="Example" customModuleProvider="target">
<rect key="frame" x="0.0" y="48" width="334" height="40.5"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="YS7-tA-cMK"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Toggle Custom Superview 🧪">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="toggleButtonTouchUpInside:" destination="ah2-WM-rFd" eventType="touchUpInside" id="W51-If-b5H"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="ja4-s5-ZP7" firstAttribute="top" secondItem="2Sj-GS-zEL" secondAttribute="top" constant="16" id="ERr-8u-XhG"/>
<constraint firstAttribute="trailing" secondItem="ja4-s5-ZP7" secondAttribute="trailing" constant="20" id="SsK-Vx-1BL"/>
<constraint firstAttribute="bottom" secondItem="ja4-s5-ZP7" secondAttribute="bottom" constant="16" id="UBf-R5-ixV"/>
<constraint firstItem="ja4-s5-ZP7" firstAttribute="leading" secondItem="2Sj-GS-zEL" secondAttribute="leading" constant="20" id="xgE-OJ-VIu"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="toggleButton" destination="ZxZ-rY-wUa" id="TwF-62-tM1"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="ZAD-6Y-gxa" id="3QD-qs-jpF"/>
<outlet property="delegate" destination="ZAD-6Y-gxa" id="yIF-PG-bRO"/>
</connections>
</tableView>
<extendedEdge key="edgesForExtendedLayout" bottom="YES"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="R94-H9-mvS" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-165" y="27"/>
</scene>
</scenes>
<designables>
<designable name="ZxZ-rY-wUa">
<size key="intrinsicContentSize" width="216.5" height="31"/>
</designable>
</designables>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
@@ -1,136 +0,0 @@
//
// AdvancedExampleTableViewController.swift
// Example
//
// Created by Felix Mau on 12.04.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
import UIKit
final class AdvancedExampleTableViewController: UITableViewController {
// MARK: - Config
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),
]
static let firstSectionHeaderHeight: CGFloat = 48
static let defaultSectionHeaderHeight: CGFloat = 32
}
// MARK: - Types
private enum Section: CaseIterable {
case customColors
case interfaceBuilderSetup
case customSuperview
var cellIdentifier: String {
switch self {
case .customColors:
return CustomColorsTableViewCell.reuseIdentifier
case .interfaceBuilderSetup:
return InterfaceBuilderSetupTableViewCell.reuseIdentifier
case .customSuperview:
return CustomSuperviewTableViewCell.reuseIdentifier
}
}
var sectionTitle: String {
switch self {
case .customColors:
return "Custom Colors"
case .interfaceBuilderSetup:
return "Interface Builder Setup"
case .customSuperview:
return "Custom Superview"
}
}
var sectionHeaderHeight: CGFloat {
switch self {
case .customColors:
return Config.firstSectionHeaderHeight
case .interfaceBuilderSetup, .customSuperview:
return Config.defaultSectionHeaderHeight
}
}
}
// MARK: - Private properties
private let gradientLoadingBar = GradientLoadingBar()
// MARK: - Public methods
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGroupedBackground
gradientLoadingBar.gradientColors = Config.gradientColors
}
// MARK: - Table view data source
override func numberOfSections(in _: UITableView) -> Int {
Section.allCases.count
}
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section = Section.allCases[indexPath.section]
switch section {
case .customColors:
guard let cell = tableView.dequeueReusableCell(withIdentifier: section.cellIdentifier, for: indexPath) as? CustomColorsTableViewCell else {
fatalError("⚠️ – Invalid table view setup. Expected to have `CustomColorsTableViewCell` at this point!")
}
cell.tapHandler = { [weak self] in
if self?.gradientLoadingBar.isHidden == true {
self?.gradientLoadingBar.fadeIn()
} else {
self?.gradientLoadingBar.fadeOut()
}
}
return cell
case .interfaceBuilderSetup, .customSuperview:
return tableView.dequeueReusableCell(withIdentifier: section.cellIdentifier, for: indexPath)
}
}
override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? {
Section.allCases[section].sectionTitle
}
override func tableView(_: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
Section.allCases[section].sectionHeaderHeight
}
}
// MARK: - Helper
struct AdvancedExampleView: View {
var body: some View {
StoryboardView(name: "AdvancedExample")
.navigationTitle("🚀 Advanced Example")
.edgesIgnoringSafeArea(.bottom)
}
}
@@ -1,24 +0,0 @@
//
// CustomColorsTableViewCell.swift
// Example
//
// Created by Felix Mau on 15.04.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import UIKit
final class CustomColorsTableViewCell: UITableViewCell {
// MARK: - Public properties
static let reuseIdentifier = "CustomColorsTableViewCell"
var tapHandler: (() -> Void)?
// MARK: - Private methods
@IBAction private func customColorsButtonTouchUpInside(_: Any) {
tapHandler?()
}
}
@@ -1,62 +0,0 @@
//
// CustomSuperviewTableViewCell.swift
// Example
//
// Created by Felix Mau on 15.04.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import UIKit
final class CustomSuperviewTableViewCell: UITableViewCell {
// MARK: - Config
private enum Config {
static let height: CGFloat = 3
}
// MARK: - Outlets
@IBOutlet private var toggleButton: UIButton!
// MARK: - Public properties
static let reuseIdentifier = "CustomSuperviewViewCell"
// MARK: - Private properties
private let gradientActivityIndicatorView = GradientActivityIndicatorView()
// MARK: - Public methods
override func awakeFromNib() {
super.awakeFromNib()
setupGradientActivityIndicatorView()
}
// MARK: - Private methods
private func setupGradientActivityIndicatorView() {
gradientActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
toggleButton.addSubview(gradientActivityIndicatorView)
NSLayoutConstraint.activate([
gradientActivityIndicatorView.leadingAnchor.constraint(equalTo: toggleButton.leadingAnchor),
gradientActivityIndicatorView.trailingAnchor.constraint(equalTo: toggleButton.trailingAnchor),
gradientActivityIndicatorView.bottomAnchor.constraint(equalTo: toggleButton.bottomAnchor),
gradientActivityIndicatorView.heightAnchor.constraint(equalToConstant: Config.height),
])
}
@IBAction private func toggleButtonTouchUpInside(_: Any) {
if gradientActivityIndicatorView.isHidden {
gradientActivityIndicatorView.fadeIn()
} else {
gradientActivityIndicatorView.fadeOut()
}
}
}
@@ -1,41 +0,0 @@
//
// InterfaceBuilderSetupTableViewCell.swift
// Example
//
// Created by Felix Mau on 15.04.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import UIKit
final class InterfaceBuilderSetupTableViewCell: UITableViewCell {
// MARK: - Config
private enum Config {
static let animationDuration: TimeInterval = 0.5
}
// MARK: - Outlets
@IBOutlet private var gradientActivityIndicator: GradientActivityIndicatorView!
// MARK: - Public properties
static let reuseIdentifier = "InterfaceBuilderSetupTableViewCell"
// MARK: - Private methods
@IBAction private func toggleIBSetupButtonTouchUpInside(_: Any) {
// We explicitly "only" reduce the alpha here, as calling `fadeIn()` / `fadeOut()` would update the `isHidden`
// flag accordingly. This would then lead to a height-update of the parent stack view.
UIView.animate(withDuration: Config.animationDuration) {
if self.gradientActivityIndicator.alpha > 0 {
self.gradientActivityIndicator.alpha = 0
} else {
self.gradientActivityIndicator.alpha = 1
}
}
}
}
@@ -1,47 +0,0 @@
//
// BorderedButton.swift
// Example
//
// Created by Felix Mau on 15.04.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import UIKit
@IBDesignable
final class BorderedButton: UIButton {
// MARK: - Config
private enum Config {
static let borderColor = #colorLiteral(red: 0.2862745098, green: 0.5647058824, blue: 0.8862745098, alpha: 1)
static let borderWidth: CGFloat = 1
static let cornerRadius: CGFloat = 4
}
// MARK: - Instance Lifecycle
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 = Config.borderColor
layer.borderColor = Config.borderColor.cgColor
layer.borderWidth = Config.borderWidth
layer.cornerRadius = Config.cornerRadius
layer.masksToBounds = true
}
}
@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Basic Example View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController id="Y6W-OH-hqX" customClass="BasicExampleViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="i3m-u5-CBZ" userLabel="Content Stack View">
<rect key="frame" x="16" y="428" width="382" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iqR-B3-IAJ" userLabel="ShowButton">
<rect key="frame" x="0.0" y="0.0" width="183" height="40"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="ygd-aW-5tx"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Show ↑">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="showButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="syE-bX-c9J"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0mi-83-2Nx" userLabel="HideButton">
<rect key="frame" x="199" y="0.0" width="183" height="40"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="bWt-eh-ohc"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Hide ">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="hideButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="xWJ-X0-hCH"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="i3m-u5-CBZ" firstAttribute="centerY" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" id="J8f-pR-dOH"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="i3m-u5-CBZ" secondAttribute="trailing" constant="16" id="RpI-X5-afT"/>
<constraint firstItem="i3m-u5-CBZ" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="qcn-hk-qmQ"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="132" y="115"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
@@ -1,42 +0,0 @@
//
// BasicExampleViewController.swift
// Example
//
// Created by Felix Mau on 07.03.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
import UIKit
final class BasicExampleViewController: UIViewController {
// MARK: - Public methods
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Reset any possible visible loading bar.
GradientLoadingBar.shared.fadeOut()
}
// MARK: - Private methods
@IBAction private func showButtonTouchUpInside(_: Any) {
GradientLoadingBar.shared.fadeIn()
}
@IBAction private func hideButtonTouchUpInside(_: Any) {
GradientLoadingBar.shared.fadeOut()
}
}
// MARK: - Helper
struct BasicExampleView: View {
var body: some View {
StoryboardView(name: "BasicExample")
.navigationTitle("🏡 Basic Example")
}
}
@@ -1,95 +0,0 @@
//
// EntryPointView.swift
// Example
//
// Created by Felix Mau on 07.03.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import SwiftUI
struct EntryPointView: View {
// MARK: - Private properties
@State
private var isNavigationBarExampleVisible = false
// MARK: - Render
var body: some View {
NavigationView {
List {
Section(header: Text("UIKit Examples")) {
NavigationLink(destination: BasicExampleView()) {
TitleSubtitleView(title: "🏡 Basic Example",
subtitle: "Basic usage and setup.")
}
NavigationLink(destination: SafeAreaExampleView()) {
TitleSubtitleView(title: "📲 Safe Area Example",
subtitle: "Loading bar ignoring the safe area.")
}
NavigationLink(destination: AdvancedExampleView()) {
TitleSubtitleView(title: "🚀 Advanced Example",
subtitle: "How to apply e.g. custom colors.")
}
Button {
isNavigationBarExampleVisible = true
} label: {
TitleSubtitleView(title: "🧭 Navigation Bar Example",
subtitle: "Attach the loading bar to a navigation bar.")
// Make entire row tappable and not just the text.
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
}
Section(header: Text("SwiftUI Examples")) {
NavigationLink(destination: SwiftUIExampleView()) {
TitleSubtitleView(title: "🎨 GradientLoadingBarView Example",
subtitle: "How to use the SwiftUI view.")
}
}
}
// We present the `NavigationBarExampleView` as a sheet using it's own navigation controller.
// The `NavigationView` passed from SwiftUI isn't accessible from a `UIViewController` in a way, that we can add subviews to it.
.sheet(isPresented: $isNavigationBarExampleVisible) {
NavigationBarExampleView()
}
// Unfortunately setting the title here results in constraint warnings.
// I couldn't find a possible fix yet, even `.navigationViewStyle(.stack)` doesn't seem to work.
// https://stackoverflow.com/q/65316497
.navigationTitle("GradientLoadingBar")
.navigationBarTitleDisplayMode(.large)
}
}
}
// MARK: - Subviews
private struct TitleSubtitleView: View {
let title: String
let subtitle: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.subheadline)
}
.padding(.vertical, 8)
}
}
// MARK: - Preview
struct EntryPointView_Previews: PreviewProvider {
static var previews: some View {
EntryPointView()
}
}
@@ -1,89 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="QqU-dK-9N3">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--🧭 Navigation Bar Example-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController id="Y6W-OH-hqX" customClass="NavigationBarExampleViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="NLc-ox-i4v">
<rect key="frame" x="16" y="428" width="382" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="m50-Bi-edx" userLabel="ShowButton">
<rect key="frame" x="0.0" y="0.0" width="183" height="40"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="3OT-yv-cyC"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Show">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="showButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="Wp7-QY-9dg"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aXh-Qp-yVs" userLabel="HideButton">
<rect key="frame" x="199" y="0.0" width="183" height="40"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="0sc-bb-ZEH"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Hide">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="hideButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="vxz-29-LMX"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="NLc-ox-i4v" secondAttribute="trailing" constant="16" id="2Ig-gz-o5i"/>
<constraint firstItem="NLc-ox-i4v" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="5DU-9Z-LFC"/>
<constraint firstItem="NLc-ox-i4v" firstAttribute="centerY" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" id="uG6-3e-Y6q"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="🧭 Navigation Bar Example" id="cJ0-SC-qx4"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1042.0289855072465" y="61.607142857142854"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="Fzj-P1-1gQ">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="QqU-dK-9N3" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="Gew-of-pOh">
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="Y6W-OH-hqX" kind="relationship" relationship="rootViewController" id="YmV-f4-HGX"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="brr-zi-WNx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="131.8840579710145" y="61.607142857142854"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
@@ -1,80 +0,0 @@
//
// NavigationBarExampleViewController.swift
// Example
//
// Created by Felix Mau on 07.03.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
import UIKit
final 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()
let barButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
navigationItem.rightBarButtonItem = barButtonItem
setupGradientProgressIndicatorView()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Reset any possible visible loading bar.
gradientProgressIndicatorView.fadeOut()
}
// MARK: - Private methods
private func setupGradientProgressIndicatorView() {
guard let navigationBar = navigationController?.navigationBar else { return }
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),
])
}
@IBAction private func doneButtonTapped(_: Any) {
dismiss(animated: true)
}
@IBAction private func showButtonTouchUpInside(_: Any) {
gradientProgressIndicatorView.fadeIn()
}
@IBAction private func hideButtonTouchUpInside(_: Any) {
gradientProgressIndicatorView.fadeOut()
}
}
// MARK: - Helper
struct NavigationBarExampleView: View {
var body: some View {
StoryboardView(name: "NavigationBarExample")
}
}
@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Safe Area Example View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController id="Y6W-OH-hqX" customClass="SafeAreaExampleViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="vQo-n2-n4V" userLabel="Top Content Stack View">
<rect key="frame" x="16" y="400" width="382" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xx7-io-IHi" userLabel="ShowBasicBarButton">
<rect key="frame" x="0.0" y="0.0" width="244" height="40"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="wri-XG-tfO"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Show Basic Bar">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="showBasicBarButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="o8j-Z6-6kZ"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Xxs-Xn-l65" userLabel="HideBasicBarButton">
<rect key="frame" x="260" y="0.0" width="122" height="40"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="zfx-4A-wZB"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Hide">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="hideBasicBarButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="XXK-yZ-LR4"/>
</connections>
</button>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="J6X-XZ-YZb" userLabel="Bottom Content Stack View">
<rect key="frame" x="16" y="456" width="382" height="40"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DS9-JT-RIl" userLabel="HideNotchBarButton">
<rect key="frame" x="0.0" y="0.0" width="104" height="40"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="64O-UM-b99"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Hide">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="hideNotchBarButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="INf-lf-Egt"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8wt-vv-Sj1" userLabel="ShowNotchBarButton">
<rect key="frame" x="120" y="0.0" width="262" height="40"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="nUb-HA-Cts"/>
</constraints>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Show Notch Bar">
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
</buttonConfiguration>
<connections>
<action selector="showNotchBarButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="aHn-BO-ovO"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="J6X-XZ-YZb" secondAttribute="trailing" constant="16" id="FLY-Rz-mWy"/>
<constraint firstItem="vQo-n2-n4V" firstAttribute="leading" secondItem="5EZ-qb-Rvc" secondAttribute="leading" constant="16" id="Vh8-QH-gYf"/>
<constraint firstItem="J6X-XZ-YZb" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" constant="8" id="XPa-0S-CCv"/>
<constraint firstItem="J6X-XZ-YZb" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="XpT-4k-VAf"/>
<constraint firstItem="vQo-n2-n4V" firstAttribute="bottom" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" constant="-8" id="czm-UN-XEM"/>
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="vQo-n2-n4V" secondAttribute="trailing" constant="16" id="umO-We-0lr"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="133" y="115"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
@@ -1,56 +0,0 @@
//
// SafeAreaExampleViewController.swift
// Example
//
// Created by Felix Mau on 07.03.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
import UIKit
final 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)
// Reset any possible visible loading bar.
gradientLoadingBar.fadeOut()
notchGradientLoadingBar.fadeOut()
}
// MARK: - Private methods
@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()
}
}
// MARK: - Helper
struct SafeAreaExampleView: View {
var body: some View {
StoryboardView(name: "SafeAreaExample")
.navigationTitle("📲 Safe Area Example")
}
}
@@ -1,177 +0,0 @@
//
// SwiftUIExampleView.swift
// Example
//
// Created by Felix Mau on 07.03.22.
// Copyright © 2022 Felix Mau. All rights reserved.
//
import GradientLoadingBar
import SwiftUI
struct SwiftUIExampleView: View {
// MARK: - Render
var body: some View {
List {
Section(header: Text("Default example")) {
DefaultExampleView()
}
Section(header: Text("Custom colors")) {
CustomColorsExampleView()
}
Section(header: Text("Custom animation duration")) {
CustomProgressDurationExampleView()
}
}
.navigationTitle("🎨 SwiftUI Example")
}
}
// MARK: - Subviews
private struct DefaultExampleView: View {
@State
private var isVisible = true
var body: some View {
VStack(spacing: 16) {
Text("Example using the default configuration.")
.frame(maxWidth: .infinity, alignment: .leading)
GradientLoadingBarView()
.frame(maxWidth: .infinity, maxHeight: 3)
.cornerRadius(1.5)
.opacity(isVisible ? 1 : 0)
HStack(spacing: 16) {
Button("Show") {
withAnimation(.easeInOut) {
isVisible = true
}
}
.buttonStyle(RoundedRectangleButtonStyle())
Button("Hide") {
withAnimation(.easeInOut) {
isVisible = false
}
}
.buttonStyle(RoundedRectangleButtonStyle())
}
}
.padding(.horizontal, 8)
.padding(.vertical, 16)
}
}
private struct CustomColorsExampleView: View {
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)
}
@State
private var isVisible = true
var body: some View {
VStack(spacing: 16) {
Text("Example showing how to provide custom gradient colors.")
.frame(maxWidth: .infinity, alignment: .leading)
GradientLoadingBarView(gradientColors: Config.gradientColors)
.frame(maxWidth: .infinity, maxHeight: 3)
.cornerRadius(1.5)
.opacity(isVisible ? 1 : 0)
HStack(spacing: 16) {
Button("Show") {
withAnimation(.easeInOut) {
isVisible = true
}
}
.buttonStyle(RoundedRectangleButtonStyle())
Button("Hide") {
withAnimation(.easeInOut) {
isVisible = false
}
}
.buttonStyle(RoundedRectangleButtonStyle())
}
}
.padding(.horizontal, 8)
.padding(.vertical, 16)
}
}
private struct CustomProgressDurationExampleView: View {
private enum Config {
/// The custom progress duration to use.
static let progressDuration: TimeInterval = 10
}
@State
private var isVisible = true
var body: some View {
VStack(spacing: 16) {
Text("Example showing how to provide a custom progress animation duration.")
.frame(maxWidth: .infinity, alignment: .leading)
GradientLoadingBarView(progressDuration: Config.progressDuration)
.frame(maxWidth: .infinity, maxHeight: 3)
.cornerRadius(1.5)
.opacity(isVisible ? 1 : 0)
HStack(spacing: 16) {
Button("Show") {
withAnimation(.easeInOut) {
isVisible = true
}
}
.buttonStyle(RoundedRectangleButtonStyle())
Button("Hide") {
withAnimation(.easeInOut) {
isVisible = false
}
}
.buttonStyle(RoundedRectangleButtonStyle())
}
}
.padding(.horizontal, 8)
.padding(.vertical, 16)
}
}
// MARK: - Helper
private struct RoundedRectangleButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(12)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Color.blue.cornerRadius(8))
.scaleEffect(configuration.isPressed ? 0.95 : 1)
}
}
// MARK: - Preview
struct SwiftUIExampleView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIExampleView()
}
}
-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)
-286
View File
@@ -1,286 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
activesupport (6.1.6.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.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.2.0)
aws-partitions (1.626.0)
aws-sdk-core (3.141.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
cocoapods (1.11.3)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.11.3)
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.8.0)
nap (~> 1.0)
ruby-macho (>= 1.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.11.3)
activesupport (>= 5.0, < 7)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.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.10)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.92.4)
faraday (1.10.2)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
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-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.209.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 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.3)
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)
optparse (~> 0.1.1)
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.3.0)
ffi (1.15.5)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.25.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-core (0.7.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.13.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-playcustomapp_v1 (0.10.0)
google-apis-core (>= 0.7, < 2.a)
google-apis-storage_v1 (0.17.0)
google-apis-core (>= 0.7, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.39.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.17.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jmespath (1.6.1)
json (2.6.2)
jwt (2.5.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
minitest (5.16.3)
molinillo (0.8.0)
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)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
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 (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
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)
trailblazer-option (0.1.2)
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 (2.0.5)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.22.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)
zeitwerk (2.6.0)
PLATFORMS
ruby
DEPENDENCIES
cocoapods
fastlane
fastlane-plugin-swiftformat
BUNDLED WITH
2.3.18
@@ -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,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,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 Symbol’s 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)
}
}
-45
View File
@@ -1,45 +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 'Example' do
pod 'GradientLoadingBar', :path => '../', :testspecs => ['Tests']
# Development pods.
pod 'SwiftFormat/CLI', '~> 0.41'
pod 'SwiftLint', '~> 0.42'
pod 'SwiftConfigurationFiles', :git => 'https://github.com/fxm90/SwiftConfigurationFiles.git'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if target.name == 'GradientLoadingBar'
# Explicitly set the iOS deployment target for `GradientLoadingBar` in our pods project, as this will be read by Carthage.
#
# - Note: This value has to match `s.ios.deployment_target` from the `.podspec` file!!
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
else
# Remove the deployment target from all other pods in our project and let them inherit
# the project/workspace deployment target that has been specified at the top of the Podfile.
# Source: https://stackoverflow.com/a/63489366
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
end
end
end
#
# 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
end
end
-43
View File
@@ -1,43 +0,0 @@
PODS:
- GradientLoadingBar (3.0.0)
- GradientLoadingBar/Tests (3.0.0):
- SnapshotTesting (~> 1.9)
- SnapshotTesting (1.9.0)
- SwiftConfigurationFiles (0.0.1)
- SwiftFormat/CLI (0.50.1)
- SwiftLint (0.49.1)
DEPENDENCIES:
- GradientLoadingBar (from `../`)
- GradientLoadingBar/Tests (from `../`)
- SwiftConfigurationFiles (from `https://github.com/fxm90/SwiftConfigurationFiles.git`)
- SwiftFormat/CLI (~> 0.41)
- SwiftLint (~> 0.42)
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- SnapshotTesting
- SwiftFormat
- SwiftLint
EXTERNAL SOURCES:
GradientLoadingBar:
:path: "../"
SwiftConfigurationFiles:
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
CHECKOUT OPTIONS:
SwiftConfigurationFiles:
:commit: efe01ba39069e8957fb89217b8c7fed298254691
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
SPEC CHECKSUMS:
GradientLoadingBar: 3c4246535efdedaf9b471c05caabfdb4b87b40a2
SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b
SwiftConfigurationFiles: 1cf2228a911ebed9f42f8dec077bb634f04ca6c8
SwiftFormat: e73212c71908404e333da34e303772b9e516ac9b
SwiftLint: 32ee33ded0636d0905ef6911b2b67bbaeeedafa5
PODFILE CHECKSUM: ea7965c23e5364bcd5cd7d3693d685af73a3e31d
COCOAPODS: 1.11.3
@@ -1,46 +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.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${CURRENT_PROJECT_VERSION}</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSPrincipalClass</key>
<string></string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" systemVersion="17A277" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
@@ -1,30 +0,0 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface CPTestAppHostAppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) UIWindow *window;
@end
@implementation CPTestAppHostAppDelegate
- (BOOL)application:(UIApplication *)__unused application didFinishLaunchingWithOptions:(NSDictionary *)__unused launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.rootViewController = [UIViewController new];
[self.window makeKeyAndVisible];
return YES;
}
@end
int main(int argc, char *argv[])
{
@autoreleasepool
{
return UIApplicationMain(argc, argv, nil, NSStringFromClass([CPTestAppHostAppDelegate class]));
}
}
@@ -1,38 +0,0 @@
{
"name": "GradientLoadingBar",
"version": "3.0.0",
"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": "3.0.0"
},
"swift_versions": "5.5",
"platforms": {
"ios": "13.0"
},
"source_files": "GradientLoadingBar/Sources/**/*.swift",
"testspecs": [
{
"name": "Tests",
"test_type": "unit",
"requires_app_host": true,
"source_files": "GradientLoadingBar/Tests/**/*.{swift,md}",
"dependencies": {
"SnapshotTesting": [
"~> 1.9"
]
}
}
],
"swift_version": "5.5"
}
@@ -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
}
}
-43
View File
@@ -1,43 +0,0 @@
PODS:
- GradientLoadingBar (3.0.0)
- GradientLoadingBar/Tests (3.0.0):
- SnapshotTesting (~> 1.9)
- SnapshotTesting (1.9.0)
- SwiftConfigurationFiles (0.0.1)
- SwiftFormat/CLI (0.50.1)
- SwiftLint (0.49.1)
DEPENDENCIES:
- GradientLoadingBar (from `../`)
- GradientLoadingBar/Tests (from `../`)
- SwiftConfigurationFiles (from `https://github.com/fxm90/SwiftConfigurationFiles.git`)
- SwiftFormat/CLI (~> 0.41)
- SwiftLint (~> 0.42)
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- SnapshotTesting
- SwiftFormat
- SwiftLint
EXTERNAL SOURCES:
GradientLoadingBar:
:path: "../"
SwiftConfigurationFiles:
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
CHECKOUT OPTIONS:
SwiftConfigurationFiles:
:commit: efe01ba39069e8957fb89217b8c7fed298254691
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
SPEC CHECKSUMS:
GradientLoadingBar: 3c4246535efdedaf9b471c05caabfdb4b87b40a2
SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b
SwiftConfigurationFiles: 1cf2228a911ebed9f42f8dec077bb634f04ca6c8
SwiftFormat: e73212c71908404e333da34e303772b9e516ac9b
SwiftLint: 32ee33ded0636d0905ef6911b2b67bbaeeedafa5
PODFILE CHECKSUM: ea7965c23e5364bcd5cd7d3693d685af73a3e31d
COCOAPODS: 1.11.3
File diff suppressed because it is too large Load Diff
@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1A92A7C75080512A0168578180C192A2"
BuildableName = "GradientLoadingBar-Unit-Tests.xctest"
BlueprintName = "GradientLoadingBar-Unit-Tests"
ReferencedContainer = "container:Pods.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1A92A7C75080512A0168578180C192A2"
BuildableName = "GradientLoadingBar-Unit-Tests.xctest"
BlueprintName = "GradientLoadingBar-Unit-Tests"
ReferencedContainer = "container:Pods.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">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AD9297F9E6CC3E50EDCFECA0A77800B7"
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>
</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">
</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 1–8 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 1–8 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