├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Package.swift ├── README.md ├── ShimmerView.podspec ├── ShimmerView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── ShimmerView.xcscheme ├── ShimmerView.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── ShimmerViewSamples ├── ShimmerViewSamples.xcodeproj │ └── project.pbxproj └── ShimmerViewSamples │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Basic │ ├── BasicViewController.swift │ ├── BasicViewModel.swift │ ├── ColorElementInputView.swift │ ├── ColorInputView.swift │ ├── EffectAngleInputView.swift │ ├── EffectSpanInputView.swift │ └── NumberInputView.swift │ ├── CommonUI │ ├── OutlinedButton.swift │ ├── SpacerView.swift │ └── TextInputView.swift │ ├── Extensions │ ├── CurrentValueSubject+Extensions.swift │ ├── DisposeBag.swift │ └── UIColor+Extensions.swift │ ├── Info.plist │ ├── List │ ├── ListViewCell.swift │ ├── ListViewCell.xib │ └── ListViewController.swift │ ├── MainViewController.swift │ ├── SceneDelegate.swift │ └── SwiftUI │ ├── Placeholder.swift │ └── SwiftUIViewController.swift ├── Sources ├── Common │ ├── ShimmerCoreView.Animator.swift │ ├── ShimmerCoreView.swift │ └── ShimmerViewStyle.swift ├── Extensions │ ├── CGPoint+Extensions.swift │ ├── CGRect+Extensions.swift │ ├── UIColor+Extensions.swift │ └── UIResponder+Extensions.swift ├── Info.plist ├── SwiftUI │ ├── ShimmerElement.swift │ └── ShimmerScope.swift └── UIKit │ ├── ShimmerReplicatorView.swift │ ├── ShimmerReplicatorViewCell.swift │ ├── ShimmerSyncTarget.swift │ └── ShimmerView.swift ├── Tests ├── Info.plist ├── ShimmerCoreViewAnimatorTests.swift ├── ShimmerCoreViewTests.swift ├── ShimmerSyncTargetTests.swift └── ShimmerViewTests.swift └── images ├── shimmer_view_example_0.gif ├── shimmer_view_list.gif ├── shimmer_view_not_synced.gif ├── shimmer_view_swift_ui.gif └── shimmer_view_synced.gif /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please read the CLA carefully before submitting your contribution to Mercari. 2 | Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. 3 | 4 | https://www.mercari.com/cla/ 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please read the CLA below carefully before submitting your contribution. 4 | 5 | https://www.mercari.com/cla/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mercari, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | 3 | import Foundation 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ShimmerView", 8 | platforms: [ 9 | .iOS(.v14), 10 | ], 11 | products: [ 12 | .library(name: "ShimmerView", targets: ["ShimmerView"]) 13 | ], 14 | targets: [ 15 | .target( 16 | name: "ShimmerView", 17 | path: "Sources" 18 | ), 19 | .testTarget( 20 | name: "ShimmerViewTests", 21 | dependencies: ["ShimmerView"], 22 | path: "Tests" 23 | ), 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShimmerView ![Platform iOS11+](https://img.shields.io/badge/platform-ios11%2B-red) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 2 | 3 | ShimmerView is a collection of APIs to construct Skelton View + Shimmering Effect type loading indicator on UIKit and SwiftUI. This framework is inspired by Facebook's [Shimmer](https://github.com/facebook/Shimmer). 4 | 5 | ![ShimmerViewExample0](images/shimmer_view_example_0.gif) 6 | 7 | ## Installation 8 | 9 | ### Carthage 10 | ``` 11 | github "mercari/ShimmerView" 12 | ``` 13 | 14 | ### Cocoapods 15 | ``` 16 | pod 'ShimmerView' 17 | ``` 18 | 19 | ### SwiftPM 20 | In your `Package.swift`, add the following line to the `dependencies`: 21 | ```swift 22 | .package(url: "https://github.com/mercari/ShimmerView.git", from: #version#) 23 | ``` 24 | 25 | ## Feature 26 | ### Synchronized Effect 27 | The shimmering effect would be effectively displayed when all the subviews’s effect in the screen is synced and animated together. 28 | 29 | | Not Synced | Synced | 30 | |---|---| 31 | |![NotSynced](images/shimmer_view_not_synced.gif)|![Synced](images/shimmer_view_synced.gif)| 32 | 33 | ShimmerView calculates its relative coordinate against the sync target and synchronize the shimmering effect with other ShimmerViews. 34 | 35 | ### Customizable 36 | You can customize the appearance of the shimmer effect by using `ShimmerViewStyle`. 37 | ```swift 38 | let style = ShimmerViewStyle( 39 | baseColor: UIColor(red: 239/255, green: 239/255, blue: 239/255, alpha: 1.0), 40 | highlightColor: UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0), 41 | duration: 1.2, 42 | interval: 0.4, 43 | effectSpan: .points(120), 44 | effectAngle: 0 * CGFloat.pi) 45 | ``` 46 | 47 | ## Usage on SwiftUI (iOS 14.0+) 48 | `ShimmerView` has two related APIs on SwiftUI as below: 49 | - `ShimmerScope` 50 | - `ShimmerElement` 51 | 52 | You can create a custome loading indicator by combining these APIs and other `SwiftUI` APIs. 53 | ```swift 54 | struct Placeholder: View { 55 | 56 | @State 57 | private var isAnimating: Bool = true 58 | 59 | var body: some View { 60 | ShimmerScope(isAnimating: $isAnimating) { 61 | HStack(alignment: .top) { 62 | ShimmerElement(width: 100, height: 100) 63 | .cornerRadius(4) 64 | VStack(alignment: .leading, spacing: 8) { 65 | ShimmerElement(height: 12) 66 | .cornerRadius(4) 67 | ShimmerElement(height: 12) 68 | .cornerRadius(4) 69 | ShimmerElement(width: 100, height: 12) 70 | .cornerRadius(4) 71 | } 72 | } 73 | .padding(.horizontal, 16) 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | ![ShimmerViewSwiftUI](images/shimmer_view_swift_ui.gif) 80 | 81 | ### ShimmerScope 82 | `ShimmerScope` is a container view to sync the animation timing and style of all the `ShimmerElement`s in its content. 83 | 84 | Please note that `ShimmerScope` uses `GeometryReader` in its body function and it doesn't fit to the content but expands to the parent view bounds. 85 | 86 | ### ShimmerElement 87 | `ShimmerElement` is a view to show the shimmering effect animation. 88 | 89 | `ShimmerElement` looks for `EnvironmentObject` injected by `ShimmerScope` so please make sure to use it as a sub view of `ShimmerScope`'s `content` function. 90 | 91 | ## Usage on UIKit 92 | `ShimmerView` has four related APIs on UIKit as blow: 93 | - `ShimmerView` 94 | - `ShimmerSyncTarget` 95 | - `ShimmerReplicatorView` 96 | - `ShimmerReplicatorViewCell` 97 | 98 | ### ShimmerView 99 | `ShimmerView` is a subclass of `UIView` that has `CAGradientLayer` as a sublayer and encapsulates the logic for the shimmering effect animation. 100 | ```swift 101 | let shimmerView = ShimmerView() 102 | view.addSubview(shimmerView) 103 | shimmerView.startAnimating() 104 | ``` 105 | 106 | The style of `ShimmerView` can be customized with `ShimmerViewStyle`. 107 | ```swift 108 | let style = ShimmerViewStyle.default 109 | shimmerView.apply(style: style) 110 | ``` 111 | 112 | ### ShimmerSyncTarget 113 | `ShimmerView` can be used as it is, but to make the best effect, create a view or view controller that contains multiple ShimmerViews and specify the container as `ShimmerSyncTarget`. ShimmerView will calculate its relative origin against the target and adjust the effect automatically. 114 | 115 | ### ShimmerReplicatorView & ShimmerReplicatorViewCell 116 | `ShimmerReplicatorView` will let you create a list type loading screen with a few lines of code. It replicates the `ShimmerReplicatorViewCell` provided by the `cellProvider` closure to fill the bounds of the view. 117 | 118 | ![ShimmerViewList](images/shimmer_view_list.gif) 119 | 120 | ```swift 121 | let replicatorView = ShimmerReplicatorView( 122 | itemSize: .fixedSize(CGSize(width: 80, height: 80)), 123 | interitemSpacing: 16, 124 | lineSpacing: 0, 125 | horizontalEdgeMode: .within, 126 | cellProvider: { () -> ShimmerReplicatorViewCell in 127 | let cell = ShimmerView() 128 | cell.layer.cornerRadius = 8.0 129 | cell.layer.masksToBounds = true 130 | return cell 131 | }) 132 | replicatorView.startAnimating() 133 | ``` 134 | 135 | ## Contribution 136 | 137 | Please read the CLA carefully before submitting your contribution to Mercari. 138 | Under any circumstances, by submitting your contribution, you are deemed to accept and agree to be bound by the terms and conditions of the CLA. 139 | 140 | [https://www.mercari.com/cla/](https://www.mercari.com/cla/) 141 | 142 | 143 | ## License 144 | 145 | Copyright 2020 Mercari, Inc. 146 | 147 | Licensed under the MIT License. 148 | -------------------------------------------------------------------------------- /ShimmerView.podspec: -------------------------------------------------------------------------------- 1 | # ShimmerView.podspec 2 | 3 | Pod::Spec.new do |s| 4 | s.name = "ShimmerView" 5 | s.version = "0.5.0" 6 | s.summary = "A framework to create Skelton View + Shimmering Effect type loading indicator on UIKit and SwiftUI." 7 | s.homepage = "https://github.com/mercari/ShimmerView" 8 | s.license = "MIT" 9 | s.author = ['Mercari, Inc.'] 10 | s.swift_version = "5.0" 11 | s.ios.deployment_target = "11.0" 12 | s.source = { :git => "https://github.com/mercari/ShimmerView.git", :tag => s.version } 13 | s.source_files = "Sources/**/*.swift" 14 | end 15 | -------------------------------------------------------------------------------- /ShimmerView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3DBD29E728EAD656006ED7AF /* UIResponder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29E328EAD656006ED7AF /* UIResponder+Extensions.swift */; }; 11 | 3DBD29E828EAD656006ED7AF /* CGRect+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29E428EAD656006ED7AF /* CGRect+Extensions.swift */; }; 12 | 3DBD29E928EAD656006ED7AF /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29E528EAD656006ED7AF /* UIColor+Extensions.swift */; }; 13 | 3DBD29EA28EAD656006ED7AF /* CGPoint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29E628EAD656006ED7AF /* CGPoint+Extensions.swift */; }; 14 | 3DBD29EE28EAD662006ED7AF /* ShimmerCoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29EB28EAD662006ED7AF /* ShimmerCoreView.swift */; }; 15 | 3DBD29EF28EAD662006ED7AF /* ShimmerCoreView.Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29EC28EAD662006ED7AF /* ShimmerCoreView.Animator.swift */; }; 16 | 3DBD29F028EAD662006ED7AF /* ShimmerViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29ED28EAD662006ED7AF /* ShimmerViewStyle.swift */; }; 17 | 3DBD2A0328EAD685006ED7AF /* ShimmerScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD2A0128EAD684006ED7AF /* ShimmerScope.swift */; }; 18 | 3DBD2A0428EAD685006ED7AF /* ShimmerElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD2A0228EAD684006ED7AF /* ShimmerElement.swift */; }; 19 | 3DBD2A0E28EAD6F7006ED7AF /* ShimmerSyncTargetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD2A0928EAD6F7006ED7AF /* ShimmerSyncTargetTests.swift */; }; 20 | 3DBD2A0F28EAD6F7006ED7AF /* ShimmerCoreViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD2A0A28EAD6F7006ED7AF /* ShimmerCoreViewTests.swift */; }; 21 | 3DBD2A1028EAD6F7006ED7AF /* ShimmerViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD2A0B28EAD6F7006ED7AF /* ShimmerViewTests.swift */; }; 22 | 3DBD2A3428EADC05006ED7AF /* ShimmerReplicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29FB28EAD67A006ED7AF /* ShimmerReplicatorView.swift */; }; 23 | 3DBD2A3528EADC05006ED7AF /* ShimmerReplicatorViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29FA28EAD67A006ED7AF /* ShimmerReplicatorViewCell.swift */; }; 24 | 3DBD2A3628EADC05006ED7AF /* ShimmerSyncTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29FC28EAD67A006ED7AF /* ShimmerSyncTarget.swift */; }; 25 | 3DBD2A3728EADC05006ED7AF /* ShimmerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD29F928EAD67A006ED7AF /* ShimmerView.swift */; }; 26 | 3DBD2A3828EADCFF006ED7AF /* ShimmerCoreViewAnimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD2A0C28EAD6F7006ED7AF /* ShimmerCoreViewAnimatorTests.swift */; }; 27 | 3DF13EF7249792D8005675B8 /* ShimmerView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DF13EED249792D8005675B8 /* ShimmerView.framework */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXContainerItemProxy section */ 31 | 3DF13EF8249792D8005675B8 /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 3DF13EE4249792D8005675B8 /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = 3DF13EEC249792D8005675B8; 36 | remoteInfo = ShimmerView; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 3DBD29E328EAD656006ED7AF /* UIResponder+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIResponder+Extensions.swift"; sourceTree = ""; }; 42 | 3DBD29E428EAD656006ED7AF /* CGRect+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+Extensions.swift"; sourceTree = ""; }; 43 | 3DBD29E528EAD656006ED7AF /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; 44 | 3DBD29E628EAD656006ED7AF /* CGPoint+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGPoint+Extensions.swift"; sourceTree = ""; }; 45 | 3DBD29EB28EAD662006ED7AF /* ShimmerCoreView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerCoreView.swift; sourceTree = ""; }; 46 | 3DBD29EC28EAD662006ED7AF /* ShimmerCoreView.Animator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerCoreView.Animator.swift; sourceTree = ""; }; 47 | 3DBD29ED28EAD662006ED7AF /* ShimmerViewStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerViewStyle.swift; sourceTree = ""; }; 48 | 3DBD29F928EAD67A006ED7AF /* ShimmerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerView.swift; sourceTree = ""; }; 49 | 3DBD29FA28EAD67A006ED7AF /* ShimmerReplicatorViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerReplicatorViewCell.swift; sourceTree = ""; }; 50 | 3DBD29FB28EAD67A006ED7AF /* ShimmerReplicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerReplicatorView.swift; sourceTree = ""; }; 51 | 3DBD29FC28EAD67A006ED7AF /* ShimmerSyncTarget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerSyncTarget.swift; sourceTree = ""; }; 52 | 3DBD2A0128EAD684006ED7AF /* ShimmerScope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerScope.swift; sourceTree = ""; }; 53 | 3DBD2A0228EAD684006ED7AF /* ShimmerElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerElement.swift; sourceTree = ""; }; 54 | 3DBD2A0528EAD696006ED7AF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | 3DBD2A0828EAD6F7006ED7AF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 3DBD2A0928EAD6F7006ED7AF /* ShimmerSyncTargetTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerSyncTargetTests.swift; sourceTree = ""; }; 57 | 3DBD2A0A28EAD6F7006ED7AF /* ShimmerCoreViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerCoreViewTests.swift; sourceTree = ""; }; 58 | 3DBD2A0B28EAD6F7006ED7AF /* ShimmerViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerViewTests.swift; sourceTree = ""; }; 59 | 3DBD2A0C28EAD6F7006ED7AF /* ShimmerCoreViewAnimatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShimmerCoreViewAnimatorTests.swift; sourceTree = ""; }; 60 | 3DF13EED249792D8005675B8 /* ShimmerView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ShimmerView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 3DF13EF6249792D8005675B8 /* ShimmerViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShimmerViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | /* End PBXFileReference section */ 63 | 64 | /* Begin PBXFrameworksBuildPhase section */ 65 | 3DF13EEA249792D8005675B8 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | 3DF13EF3249792D8005675B8 /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | 3DF13EF7249792D8005675B8 /* ShimmerView.framework in Frameworks */, 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | /* End PBXFrameworksBuildPhase section */ 81 | 82 | /* Begin PBXGroup section */ 83 | 3DBD29DE28EAD5D9006ED7AF /* Sources */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 3DBD29E128EAD61A006ED7AF /* Common */, 87 | 3DBD29E228EAD620006ED7AF /* Extensions */, 88 | 3DBD2A0528EAD696006ED7AF /* Info.plist */, 89 | 3DBD29DF28EAD606006ED7AF /* SwiftUI */, 90 | 3DBD29E028EAD613006ED7AF /* UIKit */, 91 | ); 92 | path = Sources; 93 | sourceTree = ""; 94 | }; 95 | 3DBD29DF28EAD606006ED7AF /* SwiftUI */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 3DBD2A0228EAD684006ED7AF /* ShimmerElement.swift */, 99 | 3DBD2A0128EAD684006ED7AF /* ShimmerScope.swift */, 100 | ); 101 | path = SwiftUI; 102 | sourceTree = ""; 103 | }; 104 | 3DBD29E028EAD613006ED7AF /* UIKit */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 3DBD29FB28EAD67A006ED7AF /* ShimmerReplicatorView.swift */, 108 | 3DBD29FA28EAD67A006ED7AF /* ShimmerReplicatorViewCell.swift */, 109 | 3DBD29FC28EAD67A006ED7AF /* ShimmerSyncTarget.swift */, 110 | 3DBD29F928EAD67A006ED7AF /* ShimmerView.swift */, 111 | ); 112 | path = UIKit; 113 | sourceTree = ""; 114 | }; 115 | 3DBD29E128EAD61A006ED7AF /* Common */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 3DBD29EC28EAD662006ED7AF /* ShimmerCoreView.Animator.swift */, 119 | 3DBD29EB28EAD662006ED7AF /* ShimmerCoreView.swift */, 120 | 3DBD29ED28EAD662006ED7AF /* ShimmerViewStyle.swift */, 121 | ); 122 | path = Common; 123 | sourceTree = ""; 124 | }; 125 | 3DBD29E228EAD620006ED7AF /* Extensions */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 3DBD29E628EAD656006ED7AF /* CGPoint+Extensions.swift */, 129 | 3DBD29E428EAD656006ED7AF /* CGRect+Extensions.swift */, 130 | 3DBD29E528EAD656006ED7AF /* UIColor+Extensions.swift */, 131 | 3DBD29E328EAD656006ED7AF /* UIResponder+Extensions.swift */, 132 | ); 133 | path = Extensions; 134 | sourceTree = ""; 135 | }; 136 | 3DBD2A0728EAD6D3006ED7AF /* Tests */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 3DBD2A0828EAD6F7006ED7AF /* Info.plist */, 140 | 3DBD2A0C28EAD6F7006ED7AF /* ShimmerCoreViewAnimatorTests.swift */, 141 | 3DBD2A0A28EAD6F7006ED7AF /* ShimmerCoreViewTests.swift */, 142 | 3DBD2A0928EAD6F7006ED7AF /* ShimmerSyncTargetTests.swift */, 143 | 3DBD2A0B28EAD6F7006ED7AF /* ShimmerViewTests.swift */, 144 | ); 145 | path = Tests; 146 | sourceTree = ""; 147 | }; 148 | 3DF13EE3249792D8005675B8 = { 149 | isa = PBXGroup; 150 | children = ( 151 | 3DBD29DE28EAD5D9006ED7AF /* Sources */, 152 | 3DBD2A0728EAD6D3006ED7AF /* Tests */, 153 | 3DF13EEE249792D8005675B8 /* Products */, 154 | ); 155 | sourceTree = ""; 156 | }; 157 | 3DF13EEE249792D8005675B8 /* Products */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 3DF13EED249792D8005675B8 /* ShimmerView.framework */, 161 | 3DF13EF6249792D8005675B8 /* ShimmerViewTests.xctest */, 162 | ); 163 | name = Products; 164 | sourceTree = ""; 165 | }; 166 | /* End PBXGroup section */ 167 | 168 | /* Begin PBXHeadersBuildPhase section */ 169 | 3DF13EE8249792D8005675B8 /* Headers */ = { 170 | isa = PBXHeadersBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXHeadersBuildPhase section */ 177 | 178 | /* Begin PBXNativeTarget section */ 179 | 3DF13EEC249792D8005675B8 /* ShimmerView */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = 3DF13F01249792D9005675B8 /* Build configuration list for PBXNativeTarget "ShimmerView" */; 182 | buildPhases = ( 183 | 3DF13EE8249792D8005675B8 /* Headers */, 184 | 3DF13EE9249792D8005675B8 /* Sources */, 185 | 3DF13EEA249792D8005675B8 /* Frameworks */, 186 | 3DF13EEB249792D8005675B8 /* Resources */, 187 | ); 188 | buildRules = ( 189 | ); 190 | dependencies = ( 191 | ); 192 | name = ShimmerView; 193 | productName = ShimmerView; 194 | productReference = 3DF13EED249792D8005675B8 /* ShimmerView.framework */; 195 | productType = "com.apple.product-type.framework"; 196 | }; 197 | 3DF13EF5249792D8005675B8 /* ShimmerViewTests */ = { 198 | isa = PBXNativeTarget; 199 | buildConfigurationList = 3DF13F04249792D9005675B8 /* Build configuration list for PBXNativeTarget "ShimmerViewTests" */; 200 | buildPhases = ( 201 | 3DF13EF2249792D8005675B8 /* Sources */, 202 | 3DF13EF3249792D8005675B8 /* Frameworks */, 203 | 3DF13EF4249792D8005675B8 /* Resources */, 204 | ); 205 | buildRules = ( 206 | ); 207 | dependencies = ( 208 | 3DF13EF9249792D8005675B8 /* PBXTargetDependency */, 209 | ); 210 | name = ShimmerViewTests; 211 | productName = ShimmerViewTests; 212 | productReference = 3DF13EF6249792D8005675B8 /* ShimmerViewTests.xctest */; 213 | productType = "com.apple.product-type.bundle.unit-test"; 214 | }; 215 | /* End PBXNativeTarget section */ 216 | 217 | /* Begin PBXProject section */ 218 | 3DF13EE4249792D8005675B8 /* Project object */ = { 219 | isa = PBXProject; 220 | attributes = { 221 | LastSwiftUpdateCheck = 1150; 222 | LastUpgradeCheck = 1150; 223 | ORGANIZATIONNAME = Mercari; 224 | TargetAttributes = { 225 | 3DF13EEC249792D8005675B8 = { 226 | CreatedOnToolsVersion = 11.5; 227 | LastSwiftMigration = 1150; 228 | }; 229 | 3DF13EF5249792D8005675B8 = { 230 | CreatedOnToolsVersion = 11.5; 231 | }; 232 | }; 233 | }; 234 | buildConfigurationList = 3DF13EE7249792D8005675B8 /* Build configuration list for PBXProject "ShimmerView" */; 235 | compatibilityVersion = "Xcode 9.3"; 236 | developmentRegion = en; 237 | hasScannedForEncodings = 0; 238 | knownRegions = ( 239 | en, 240 | Base, 241 | ); 242 | mainGroup = 3DF13EE3249792D8005675B8; 243 | productRefGroup = 3DF13EEE249792D8005675B8 /* Products */; 244 | projectDirPath = ""; 245 | projectRoot = ""; 246 | targets = ( 247 | 3DF13EEC249792D8005675B8 /* ShimmerView */, 248 | 3DF13EF5249792D8005675B8 /* ShimmerViewTests */, 249 | ); 250 | }; 251 | /* End PBXProject section */ 252 | 253 | /* Begin PBXResourcesBuildPhase section */ 254 | 3DF13EEB249792D8005675B8 /* Resources */ = { 255 | isa = PBXResourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | 3DF13EF4249792D8005675B8 /* Resources */ = { 262 | isa = PBXResourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXResourcesBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 3DF13EE9249792D8005675B8 /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 3DBD2A3428EADC05006ED7AF /* ShimmerReplicatorView.swift in Sources */, 276 | 3DBD2A3528EADC05006ED7AF /* ShimmerReplicatorViewCell.swift in Sources */, 277 | 3DBD2A3628EADC05006ED7AF /* ShimmerSyncTarget.swift in Sources */, 278 | 3DBD2A3728EADC05006ED7AF /* ShimmerView.swift in Sources */, 279 | 3DBD2A0328EAD685006ED7AF /* ShimmerScope.swift in Sources */, 280 | 3DBD2A0428EAD685006ED7AF /* ShimmerElement.swift in Sources */, 281 | 3DBD29F028EAD662006ED7AF /* ShimmerViewStyle.swift in Sources */, 282 | 3DBD29E728EAD656006ED7AF /* UIResponder+Extensions.swift in Sources */, 283 | 3DBD29E828EAD656006ED7AF /* CGRect+Extensions.swift in Sources */, 284 | 3DBD29EF28EAD662006ED7AF /* ShimmerCoreView.Animator.swift in Sources */, 285 | 3DBD29EA28EAD656006ED7AF /* CGPoint+Extensions.swift in Sources */, 286 | 3DBD29E928EAD656006ED7AF /* UIColor+Extensions.swift in Sources */, 287 | 3DBD29EE28EAD662006ED7AF /* ShimmerCoreView.swift in Sources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | 3DF13EF2249792D8005675B8 /* Sources */ = { 292 | isa = PBXSourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | 3DBD2A3828EADCFF006ED7AF /* ShimmerCoreViewAnimatorTests.swift in Sources */, 296 | 3DBD2A0E28EAD6F7006ED7AF /* ShimmerSyncTargetTests.swift in Sources */, 297 | 3DBD2A0F28EAD6F7006ED7AF /* ShimmerCoreViewTests.swift in Sources */, 298 | 3DBD2A1028EAD6F7006ED7AF /* ShimmerViewTests.swift in Sources */, 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | /* End PBXSourcesBuildPhase section */ 303 | 304 | /* Begin PBXTargetDependency section */ 305 | 3DF13EF9249792D8005675B8 /* PBXTargetDependency */ = { 306 | isa = PBXTargetDependency; 307 | target = 3DF13EEC249792D8005675B8 /* ShimmerView */; 308 | targetProxy = 3DF13EF8249792D8005675B8 /* PBXContainerItemProxy */; 309 | }; 310 | /* End PBXTargetDependency section */ 311 | 312 | /* Begin XCBuildConfiguration section */ 313 | 3DF13EFF249792D9005675B8 /* Debug */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | ALWAYS_SEARCH_USER_PATHS = NO; 317 | CLANG_ANALYZER_NONNULL = YES; 318 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 319 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 320 | CLANG_CXX_LIBRARY = "libc++"; 321 | CLANG_ENABLE_MODULES = YES; 322 | CLANG_ENABLE_OBJC_ARC = YES; 323 | CLANG_ENABLE_OBJC_WEAK = YES; 324 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 325 | CLANG_WARN_BOOL_CONVERSION = YES; 326 | CLANG_WARN_COMMA = YES; 327 | CLANG_WARN_CONSTANT_CONVERSION = YES; 328 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 329 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 330 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 331 | CLANG_WARN_EMPTY_BODY = YES; 332 | CLANG_WARN_ENUM_CONVERSION = YES; 333 | CLANG_WARN_INFINITE_RECURSION = YES; 334 | CLANG_WARN_INT_CONVERSION = YES; 335 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 336 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 337 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 338 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 339 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 340 | CLANG_WARN_STRICT_PROTOTYPES = YES; 341 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 342 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 343 | CLANG_WARN_UNREACHABLE_CODE = YES; 344 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 345 | COPY_PHASE_STRIP = NO; 346 | CURRENT_PROJECT_VERSION = 1; 347 | DEBUG_INFORMATION_FORMAT = dwarf; 348 | ENABLE_STRICT_OBJC_MSGSEND = YES; 349 | ENABLE_TESTABILITY = YES; 350 | GCC_C_LANGUAGE_STANDARD = gnu11; 351 | GCC_DYNAMIC_NO_PIC = NO; 352 | GCC_NO_COMMON_BLOCKS = YES; 353 | GCC_OPTIMIZATION_LEVEL = 0; 354 | GCC_PREPROCESSOR_DEFINITIONS = ( 355 | "DEBUG=1", 356 | "$(inherited)", 357 | ); 358 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 359 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 360 | GCC_WARN_UNDECLARED_SELECTOR = YES; 361 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 362 | GCC_WARN_UNUSED_FUNCTION = YES; 363 | GCC_WARN_UNUSED_VARIABLE = YES; 364 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 365 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 366 | MTL_FAST_MATH = YES; 367 | ONLY_ACTIVE_ARCH = YES; 368 | SDKROOT = iphoneos; 369 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 370 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 371 | VERSIONING_SYSTEM = "apple-generic"; 372 | VERSION_INFO_PREFIX = ""; 373 | }; 374 | name = Debug; 375 | }; 376 | 3DF13F00249792D9005675B8 /* Release */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_ANALYZER_NONNULL = YES; 381 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 382 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 383 | CLANG_CXX_LIBRARY = "libc++"; 384 | CLANG_ENABLE_MODULES = YES; 385 | CLANG_ENABLE_OBJC_ARC = YES; 386 | CLANG_ENABLE_OBJC_WEAK = YES; 387 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 388 | CLANG_WARN_BOOL_CONVERSION = YES; 389 | CLANG_WARN_COMMA = YES; 390 | CLANG_WARN_CONSTANT_CONVERSION = YES; 391 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 392 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 393 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 394 | CLANG_WARN_EMPTY_BODY = YES; 395 | CLANG_WARN_ENUM_CONVERSION = YES; 396 | CLANG_WARN_INFINITE_RECURSION = YES; 397 | CLANG_WARN_INT_CONVERSION = YES; 398 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 399 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 400 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 401 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 402 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 403 | CLANG_WARN_STRICT_PROTOTYPES = YES; 404 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 405 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 406 | CLANG_WARN_UNREACHABLE_CODE = YES; 407 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 408 | COPY_PHASE_STRIP = NO; 409 | CURRENT_PROJECT_VERSION = 1; 410 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 411 | ENABLE_NS_ASSERTIONS = NO; 412 | ENABLE_STRICT_OBJC_MSGSEND = YES; 413 | GCC_C_LANGUAGE_STANDARD = gnu11; 414 | GCC_NO_COMMON_BLOCKS = YES; 415 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 416 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 417 | GCC_WARN_UNDECLARED_SELECTOR = YES; 418 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 419 | GCC_WARN_UNUSED_FUNCTION = YES; 420 | GCC_WARN_UNUSED_VARIABLE = YES; 421 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 422 | MTL_ENABLE_DEBUG_INFO = NO; 423 | MTL_FAST_MATH = YES; 424 | SDKROOT = iphoneos; 425 | SWIFT_COMPILATION_MODE = wholemodule; 426 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 427 | VALIDATE_PRODUCT = YES; 428 | VERSIONING_SYSTEM = "apple-generic"; 429 | VERSION_INFO_PREFIX = ""; 430 | }; 431 | name = Release; 432 | }; 433 | 3DF13F02249792D9005675B8 /* Debug */ = { 434 | isa = XCBuildConfiguration; 435 | buildSettings = { 436 | CLANG_ENABLE_MODULES = YES; 437 | CODE_SIGN_STYLE = Manual; 438 | DEFINES_MODULE = YES; 439 | DEVELOPMENT_TEAM = ""; 440 | DYLIB_COMPATIBILITY_VERSION = 1; 441 | DYLIB_CURRENT_VERSION = 1; 442 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 443 | INFOPLIST_FILE = Sources/Info.plist; 444 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 445 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 446 | LD_RUNPATH_SEARCH_PATHS = ( 447 | "$(inherited)", 448 | "@executable_path/Frameworks", 449 | "@loader_path/Frameworks", 450 | ); 451 | PRODUCT_BUNDLE_IDENTIFIER = mercari.ShimmerView; 452 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 453 | PROVISIONING_PROFILE_SPECIFIER = ""; 454 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; 455 | SKIP_INSTALL = YES; 456 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 457 | SWIFT_VERSION = 5.0; 458 | TARGETED_DEVICE_FAMILY = "1,2"; 459 | }; 460 | name = Debug; 461 | }; 462 | 3DF13F03249792D9005675B8 /* Release */ = { 463 | isa = XCBuildConfiguration; 464 | buildSettings = { 465 | CLANG_ENABLE_MODULES = YES; 466 | CODE_SIGN_STYLE = Manual; 467 | DEFINES_MODULE = YES; 468 | DEVELOPMENT_TEAM = ""; 469 | DYLIB_COMPATIBILITY_VERSION = 1; 470 | DYLIB_CURRENT_VERSION = 1; 471 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 472 | INFOPLIST_FILE = Sources/Info.plist; 473 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 474 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 475 | LD_RUNPATH_SEARCH_PATHS = ( 476 | "$(inherited)", 477 | "@executable_path/Frameworks", 478 | "@loader_path/Frameworks", 479 | ); 480 | PRODUCT_BUNDLE_IDENTIFIER = mercari.ShimmerView; 481 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 482 | PROVISIONING_PROFILE_SPECIFIER = ""; 483 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; 484 | SKIP_INSTALL = YES; 485 | SWIFT_VERSION = 5.0; 486 | TARGETED_DEVICE_FAMILY = "1,2"; 487 | }; 488 | name = Release; 489 | }; 490 | 3DF13F05249792D9005675B8 /* Debug */ = { 491 | isa = XCBuildConfiguration; 492 | buildSettings = { 493 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 494 | CODE_SIGN_STYLE = Manual; 495 | DEVELOPMENT_TEAM = ""; 496 | INFOPLIST_FILE = Tests/Info.plist; 497 | LD_RUNPATH_SEARCH_PATHS = ( 498 | "$(inherited)", 499 | "@executable_path/Frameworks", 500 | "@loader_path/Frameworks", 501 | ); 502 | PRODUCT_BUNDLE_IDENTIFIER = mercari.ShimmerViewTests; 503 | PRODUCT_NAME = "$(TARGET_NAME)"; 504 | PROVISIONING_PROFILE_SPECIFIER = ""; 505 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; 506 | SWIFT_VERSION = 5.0; 507 | TARGETED_DEVICE_FAMILY = "1,2"; 508 | }; 509 | name = Debug; 510 | }; 511 | 3DF13F06249792D9005675B8 /* Release */ = { 512 | isa = XCBuildConfiguration; 513 | buildSettings = { 514 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 515 | CODE_SIGN_STYLE = Manual; 516 | DEVELOPMENT_TEAM = ""; 517 | INFOPLIST_FILE = Tests/Info.plist; 518 | LD_RUNPATH_SEARCH_PATHS = ( 519 | "$(inherited)", 520 | "@executable_path/Frameworks", 521 | "@loader_path/Frameworks", 522 | ); 523 | PRODUCT_BUNDLE_IDENTIFIER = mercari.ShimmerViewTests; 524 | PRODUCT_NAME = "$(TARGET_NAME)"; 525 | PROVISIONING_PROFILE_SPECIFIER = ""; 526 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; 527 | SWIFT_VERSION = 5.0; 528 | TARGETED_DEVICE_FAMILY = "1,2"; 529 | }; 530 | name = Release; 531 | }; 532 | /* End XCBuildConfiguration section */ 533 | 534 | /* Begin XCConfigurationList section */ 535 | 3DF13EE7249792D8005675B8 /* Build configuration list for PBXProject "ShimmerView" */ = { 536 | isa = XCConfigurationList; 537 | buildConfigurations = ( 538 | 3DF13EFF249792D9005675B8 /* Debug */, 539 | 3DF13F00249792D9005675B8 /* Release */, 540 | ); 541 | defaultConfigurationIsVisible = 0; 542 | defaultConfigurationName = Release; 543 | }; 544 | 3DF13F01249792D9005675B8 /* Build configuration list for PBXNativeTarget "ShimmerView" */ = { 545 | isa = XCConfigurationList; 546 | buildConfigurations = ( 547 | 3DF13F02249792D9005675B8 /* Debug */, 548 | 3DF13F03249792D9005675B8 /* Release */, 549 | ); 550 | defaultConfigurationIsVisible = 0; 551 | defaultConfigurationName = Release; 552 | }; 553 | 3DF13F04249792D9005675B8 /* Build configuration list for PBXNativeTarget "ShimmerViewTests" */ = { 554 | isa = XCConfigurationList; 555 | buildConfigurations = ( 556 | 3DF13F05249792D9005675B8 /* Debug */, 557 | 3DF13F06249792D9005675B8 /* Release */, 558 | ); 559 | defaultConfigurationIsVisible = 0; 560 | defaultConfigurationName = Release; 561 | }; 562 | /* End XCConfigurationList section */ 563 | }; 564 | rootObject = 3DF13EE4249792D8005675B8 /* Project object */; 565 | } 566 | -------------------------------------------------------------------------------- /ShimmerView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ShimmerView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ShimmerView.xcodeproj/xcshareddata/xcschemes/ShimmerView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /ShimmerView.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ShimmerView.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3D1774EB249B77C400A4C7B5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1774EA249B77C400A4C7B5 /* AppDelegate.swift */; }; 11 | 3D1774ED249B77C400A4C7B5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1774EC249B77C400A4C7B5 /* SceneDelegate.swift */; }; 12 | 3D1774EF249B77C400A4C7B5 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1774EE249B77C400A4C7B5 /* MainViewController.swift */; }; 13 | 3D1774F4249B77C500A4C7B5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D1774F3249B77C500A4C7B5 /* Assets.xcassets */; }; 14 | 3D1774F7249B77C500A4C7B5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3D1774F5249B77C500A4C7B5 /* LaunchScreen.storyboard */; }; 15 | 3D177500249B789D00A4C7B5 /* BasicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1774FF249B789D00A4C7B5 /* BasicViewController.swift */; }; 16 | 3D17755724A0C0F700A4C7B5 /* SpacerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D17755624A0C0F700A4C7B5 /* SpacerView.swift */; }; 17 | 3D17757024A0DF7800A4C7B5 /* ColorInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D17756F24A0DF7800A4C7B5 /* ColorInputView.swift */; }; 18 | 3D17757224A19D9B00A4C7B5 /* NumberInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D17757124A19D9B00A4C7B5 /* NumberInputView.swift */; }; 19 | 3D17757424A1F6EE00A4C7B5 /* ColorElementInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D17757324A1F6EE00A4C7B5 /* ColorElementInputView.swift */; }; 20 | 3D17757624A2041400A4C7B5 /* EffectSpanInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D17757524A2041400A4C7B5 /* EffectSpanInputView.swift */; }; 21 | 3D17757824A2F5C900A4C7B5 /* OutlinedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D17757724A2F5C900A4C7B5 /* OutlinedButton.swift */; }; 22 | 3D17757A24A30FED00A4C7B5 /* TextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D17757924A30FED00A4C7B5 /* TextInputView.swift */; }; 23 | 3D49EE9924A4436B007F0C60 /* BasicViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D49EE9824A4436B007F0C60 /* BasicViewModel.swift */; }; 24 | 3D49EE9E24A5F28D007F0C60 /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D49EE9D24A5F28D007F0C60 /* ListViewController.swift */; }; 25 | 3D49EEA224A5F483007F0C60 /* ListViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3D49EEA124A5F483007F0C60 /* ListViewCell.xib */; }; 26 | 3D49EEA424A5FB56007F0C60 /* ListViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D49EEA324A5FB56007F0C60 /* ListViewCell.swift */; }; 27 | 3D53F44024A33CEE00AECB38 /* EffectAngleInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D53F43F24A33CEE00AECB38 /* EffectAngleInputView.swift */; }; 28 | 3D53F44224A3435400AECB38 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D53F44124A3435400AECB38 /* UIColor+Extensions.swift */; }; 29 | 3DAC488B24A3705D00D0F581 /* CurrentValueSubject+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAC488A24A3705D00D0F581 /* CurrentValueSubject+Extensions.swift */; }; 30 | 3DAC48A424A3736F00D0F581 /* DisposeBag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAC48A324A3736F00D0F581 /* DisposeBag.swift */; }; 31 | 3DDFB84627C4991D002EF594 /* SwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDFB84527C4991D002EF594 /* SwiftUIViewController.swift */; }; 32 | 3DDFB84827C4996D002EF594 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DDFB84727C4996D002EF594 /* Placeholder.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 3D1774E7249B77C400A4C7B5 /* ShimmerViewSamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ShimmerViewSamples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 3D1774EA249B77C400A4C7B5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 38 | 3D1774EC249B77C400A4C7B5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 39 | 3D1774EE249B77C400A4C7B5 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 40 | 3D1774F3249B77C500A4C7B5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | 3D1774F6249B77C500A4C7B5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 42 | 3D1774F8249B77C500A4C7B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 3D1774FF249B789D00A4C7B5 /* BasicViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicViewController.swift; sourceTree = ""; }; 44 | 3D17755624A0C0F700A4C7B5 /* SpacerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacerView.swift; sourceTree = ""; }; 45 | 3D17756F24A0DF7800A4C7B5 /* ColorInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorInputView.swift; sourceTree = ""; }; 46 | 3D17757124A19D9B00A4C7B5 /* NumberInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberInputView.swift; sourceTree = ""; }; 47 | 3D17757324A1F6EE00A4C7B5 /* ColorElementInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorElementInputView.swift; sourceTree = ""; }; 48 | 3D17757524A2041400A4C7B5 /* EffectSpanInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectSpanInputView.swift; sourceTree = ""; }; 49 | 3D17757724A2F5C900A4C7B5 /* OutlinedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlinedButton.swift; sourceTree = ""; }; 50 | 3D17757924A30FED00A4C7B5 /* TextInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputView.swift; sourceTree = ""; }; 51 | 3D49EE9824A4436B007F0C60 /* BasicViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicViewModel.swift; sourceTree = ""; }; 52 | 3D49EE9D24A5F28D007F0C60 /* ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; }; 53 | 3D49EEA124A5F483007F0C60 /* ListViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ListViewCell.xib; sourceTree = ""; }; 54 | 3D49EEA324A5FB56007F0C60 /* ListViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewCell.swift; sourceTree = ""; }; 55 | 3D53F43F24A33CEE00AECB38 /* EffectAngleInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectAngleInputView.swift; sourceTree = ""; }; 56 | 3D53F44124A3435400AECB38 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; 57 | 3DAC488A24A3705D00D0F581 /* CurrentValueSubject+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentValueSubject+Extensions.swift"; sourceTree = ""; }; 58 | 3DAC48A324A3736F00D0F581 /* DisposeBag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBag.swift; sourceTree = ""; }; 59 | 3DDFB84527C4991D002EF594 /* SwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewController.swift; sourceTree = ""; }; 60 | 3DDFB84727C4996D002EF594 /* Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Placeholder.swift; sourceTree = ""; }; 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | 3D1774E4249B77C400A4C7B5 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 3D1774DE249B77C400A4C7B5 = { 75 | isa = PBXGroup; 76 | children = ( 77 | 3D1774E9249B77C400A4C7B5 /* ShimmerViewSamples */, 78 | 3D1774E8249B77C400A4C7B5 /* Products */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | 3D1774E8249B77C400A4C7B5 /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 3D1774E7249B77C400A4C7B5 /* ShimmerViewSamples.app */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 3D1774E9249B77C400A4C7B5 /* ShimmerViewSamples */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 3DDFB84427C498FB002EF594 /* SwiftUI */, 94 | 3D49EE9C24A5F273007F0C60 /* List */, 95 | 3DAC48A724A3799B00D0F581 /* CommonUI */, 96 | 3D53F44324A3442C00AECB38 /* Extensions */, 97 | 3D1774FE249B787D00A4C7B5 /* Basic */, 98 | 3D1774EA249B77C400A4C7B5 /* AppDelegate.swift */, 99 | 3D1774EC249B77C400A4C7B5 /* SceneDelegate.swift */, 100 | 3D1774EE249B77C400A4C7B5 /* MainViewController.swift */, 101 | 3D1774F3249B77C500A4C7B5 /* Assets.xcassets */, 102 | 3D1774F5249B77C500A4C7B5 /* LaunchScreen.storyboard */, 103 | 3D1774F8249B77C500A4C7B5 /* Info.plist */, 104 | ); 105 | path = ShimmerViewSamples; 106 | sourceTree = ""; 107 | }; 108 | 3D1774FE249B787D00A4C7B5 /* Basic */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 3D1774FF249B789D00A4C7B5 /* BasicViewController.swift */, 112 | 3D49EE9824A4436B007F0C60 /* BasicViewModel.swift */, 113 | 3D17756F24A0DF7800A4C7B5 /* ColorInputView.swift */, 114 | 3D17757124A19D9B00A4C7B5 /* NumberInputView.swift */, 115 | 3D17757324A1F6EE00A4C7B5 /* ColorElementInputView.swift */, 116 | 3D17757524A2041400A4C7B5 /* EffectSpanInputView.swift */, 117 | 3D53F43F24A33CEE00AECB38 /* EffectAngleInputView.swift */, 118 | ); 119 | path = Basic; 120 | sourceTree = ""; 121 | }; 122 | 3D49EE9C24A5F273007F0C60 /* List */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 3D49EE9D24A5F28D007F0C60 /* ListViewController.swift */, 126 | 3D49EEA324A5FB56007F0C60 /* ListViewCell.swift */, 127 | 3D49EEA124A5F483007F0C60 /* ListViewCell.xib */, 128 | ); 129 | path = List; 130 | sourceTree = ""; 131 | }; 132 | 3D53F44324A3442C00AECB38 /* Extensions */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 3D53F44124A3435400AECB38 /* UIColor+Extensions.swift */, 136 | 3DAC488A24A3705D00D0F581 /* CurrentValueSubject+Extensions.swift */, 137 | 3DAC48A324A3736F00D0F581 /* DisposeBag.swift */, 138 | ); 139 | path = Extensions; 140 | sourceTree = ""; 141 | }; 142 | 3DAC48A724A3799B00D0F581 /* CommonUI */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 3D17757924A30FED00A4C7B5 /* TextInputView.swift */, 146 | 3D17757724A2F5C900A4C7B5 /* OutlinedButton.swift */, 147 | 3D17755624A0C0F700A4C7B5 /* SpacerView.swift */, 148 | ); 149 | path = CommonUI; 150 | sourceTree = ""; 151 | }; 152 | 3DDFB84427C498FB002EF594 /* SwiftUI */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | 3DDFB84527C4991D002EF594 /* SwiftUIViewController.swift */, 156 | 3DDFB84727C4996D002EF594 /* Placeholder.swift */, 157 | ); 158 | path = SwiftUI; 159 | sourceTree = ""; 160 | }; 161 | /* End PBXGroup section */ 162 | 163 | /* Begin PBXNativeTarget section */ 164 | 3D1774E6249B77C400A4C7B5 /* ShimmerViewSamples */ = { 165 | isa = PBXNativeTarget; 166 | buildConfigurationList = 3D1774FB249B77C500A4C7B5 /* Build configuration list for PBXNativeTarget "ShimmerViewSamples" */; 167 | buildPhases = ( 168 | 3D1774E3249B77C400A4C7B5 /* Sources */, 169 | 3D1774E4249B77C400A4C7B5 /* Frameworks */, 170 | 3D1774E5249B77C400A4C7B5 /* Resources */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | ); 176 | name = ShimmerViewSamples; 177 | productName = ShimmerViewSamples; 178 | productReference = 3D1774E7249B77C400A4C7B5 /* ShimmerViewSamples.app */; 179 | productType = "com.apple.product-type.application"; 180 | }; 181 | /* End PBXNativeTarget section */ 182 | 183 | /* Begin PBXProject section */ 184 | 3D1774DF249B77C400A4C7B5 /* Project object */ = { 185 | isa = PBXProject; 186 | attributes = { 187 | LastSwiftUpdateCheck = 1150; 188 | LastUpgradeCheck = 1150; 189 | ORGANIZATIONNAME = Mercari; 190 | TargetAttributes = { 191 | 3D1774E6249B77C400A4C7B5 = { 192 | CreatedOnToolsVersion = 11.5; 193 | }; 194 | }; 195 | }; 196 | buildConfigurationList = 3D1774E2249B77C400A4C7B5 /* Build configuration list for PBXProject "ShimmerViewSamples" */; 197 | compatibilityVersion = "Xcode 9.3"; 198 | developmentRegion = en; 199 | hasScannedForEncodings = 0; 200 | knownRegions = ( 201 | en, 202 | Base, 203 | ); 204 | mainGroup = 3D1774DE249B77C400A4C7B5; 205 | productRefGroup = 3D1774E8249B77C400A4C7B5 /* Products */; 206 | projectDirPath = ""; 207 | projectRoot = ""; 208 | targets = ( 209 | 3D1774E6249B77C400A4C7B5 /* ShimmerViewSamples */, 210 | ); 211 | }; 212 | /* End PBXProject section */ 213 | 214 | /* Begin PBXResourcesBuildPhase section */ 215 | 3D1774E5249B77C400A4C7B5 /* Resources */ = { 216 | isa = PBXResourcesBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | 3D1774F7249B77C500A4C7B5 /* LaunchScreen.storyboard in Resources */, 220 | 3D1774F4249B77C500A4C7B5 /* Assets.xcassets in Resources */, 221 | 3D49EEA224A5F483007F0C60 /* ListViewCell.xib in Resources */, 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | }; 225 | /* End PBXResourcesBuildPhase section */ 226 | 227 | /* Begin PBXSourcesBuildPhase section */ 228 | 3D1774E3249B77C400A4C7B5 /* Sources */ = { 229 | isa = PBXSourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | 3D49EE9E24A5F28D007F0C60 /* ListViewController.swift in Sources */, 233 | 3D1774EF249B77C400A4C7B5 /* MainViewController.swift in Sources */, 234 | 3D17757824A2F5C900A4C7B5 /* OutlinedButton.swift in Sources */, 235 | 3D177500249B789D00A4C7B5 /* BasicViewController.swift in Sources */, 236 | 3D53F44224A3435400AECB38 /* UIColor+Extensions.swift in Sources */, 237 | 3D17757424A1F6EE00A4C7B5 /* ColorElementInputView.swift in Sources */, 238 | 3D53F44024A33CEE00AECB38 /* EffectAngleInputView.swift in Sources */, 239 | 3D1774EB249B77C400A4C7B5 /* AppDelegate.swift in Sources */, 240 | 3D17755724A0C0F700A4C7B5 /* SpacerView.swift in Sources */, 241 | 3DAC48A424A3736F00D0F581 /* DisposeBag.swift in Sources */, 242 | 3DAC488B24A3705D00D0F581 /* CurrentValueSubject+Extensions.swift in Sources */, 243 | 3DDFB84827C4996D002EF594 /* Placeholder.swift in Sources */, 244 | 3DDFB84627C4991D002EF594 /* SwiftUIViewController.swift in Sources */, 245 | 3D17757624A2041400A4C7B5 /* EffectSpanInputView.swift in Sources */, 246 | 3D1774ED249B77C400A4C7B5 /* SceneDelegate.swift in Sources */, 247 | 3D17757024A0DF7800A4C7B5 /* ColorInputView.swift in Sources */, 248 | 3D49EE9924A4436B007F0C60 /* BasicViewModel.swift in Sources */, 249 | 3D17757224A19D9B00A4C7B5 /* NumberInputView.swift in Sources */, 250 | 3D49EEA424A5FB56007F0C60 /* ListViewCell.swift in Sources */, 251 | 3D17757A24A30FED00A4C7B5 /* TextInputView.swift in Sources */, 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | /* End PBXSourcesBuildPhase section */ 256 | 257 | /* Begin PBXVariantGroup section */ 258 | 3D1774F5249B77C500A4C7B5 /* LaunchScreen.storyboard */ = { 259 | isa = PBXVariantGroup; 260 | children = ( 261 | 3D1774F6249B77C500A4C7B5 /* Base */, 262 | ); 263 | name = LaunchScreen.storyboard; 264 | sourceTree = ""; 265 | }; 266 | /* End PBXVariantGroup section */ 267 | 268 | /* Begin XCBuildConfiguration section */ 269 | 3D1774F9249B77C500A4C7B5 /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ALWAYS_SEARCH_USER_PATHS = NO; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 276 | CLANG_CXX_LIBRARY = "libc++"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_ENABLE_OBJC_WEAK = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 296 | CLANG_WARN_STRICT_PROTOTYPES = YES; 297 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 298 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 299 | CLANG_WARN_UNREACHABLE_CODE = YES; 300 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 301 | COPY_PHASE_STRIP = NO; 302 | DEBUG_INFORMATION_FORMAT = dwarf; 303 | ENABLE_STRICT_OBJC_MSGSEND = YES; 304 | ENABLE_TESTABILITY = YES; 305 | GCC_C_LANGUAGE_STANDARD = gnu11; 306 | GCC_DYNAMIC_NO_PIC = NO; 307 | GCC_NO_COMMON_BLOCKS = YES; 308 | GCC_OPTIMIZATION_LEVEL = 0; 309 | GCC_PREPROCESSOR_DEFINITIONS = ( 310 | "DEBUG=1", 311 | "$(inherited)", 312 | ); 313 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 314 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 315 | GCC_WARN_UNDECLARED_SELECTOR = YES; 316 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 317 | GCC_WARN_UNUSED_FUNCTION = YES; 318 | GCC_WARN_UNUSED_VARIABLE = YES; 319 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 320 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 321 | MTL_FAST_MATH = YES; 322 | ONLY_ACTIVE_ARCH = YES; 323 | SDKROOT = iphoneos; 324 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 325 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 326 | }; 327 | name = Debug; 328 | }; 329 | 3D1774FA249B77C500A4C7B5 /* Release */ = { 330 | isa = XCBuildConfiguration; 331 | buildSettings = { 332 | ALWAYS_SEARCH_USER_PATHS = NO; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_ENABLE_OBJC_WEAK = YES; 340 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 341 | CLANG_WARN_BOOL_CONVERSION = YES; 342 | CLANG_WARN_COMMA = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 345 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 346 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 363 | ENABLE_NS_ASSERTIONS = NO; 364 | ENABLE_STRICT_OBJC_MSGSEND = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu11; 366 | GCC_NO_COMMON_BLOCKS = YES; 367 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 368 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 369 | GCC_WARN_UNDECLARED_SELECTOR = YES; 370 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 371 | GCC_WARN_UNUSED_FUNCTION = YES; 372 | GCC_WARN_UNUSED_VARIABLE = YES; 373 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 374 | MTL_ENABLE_DEBUG_INFO = NO; 375 | MTL_FAST_MATH = YES; 376 | SDKROOT = iphoneos; 377 | SWIFT_COMPILATION_MODE = wholemodule; 378 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 379 | VALIDATE_PRODUCT = YES; 380 | }; 381 | name = Release; 382 | }; 383 | 3D1774FC249B77C500A4C7B5 /* Debug */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 387 | CODE_SIGN_STYLE = Manual; 388 | DEVELOPMENT_TEAM = ""; 389 | INFOPLIST_FILE = ShimmerViewSamples/Info.plist; 390 | LD_RUNPATH_SEARCH_PATHS = ( 391 | "$(inherited)", 392 | "@executable_path/Frameworks", 393 | ); 394 | PRODUCT_BUNDLE_IDENTIFIER = mercari.ShimmerViewSamples; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | PROVISIONING_PROFILE_SPECIFIER = ""; 397 | SWIFT_VERSION = 5.0; 398 | TARGETED_DEVICE_FAMILY = "1,2"; 399 | }; 400 | name = Debug; 401 | }; 402 | 3D1774FD249B77C500A4C7B5 /* Release */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 406 | CODE_SIGN_STYLE = Manual; 407 | DEVELOPMENT_TEAM = ""; 408 | INFOPLIST_FILE = ShimmerViewSamples/Info.plist; 409 | LD_RUNPATH_SEARCH_PATHS = ( 410 | "$(inherited)", 411 | "@executable_path/Frameworks", 412 | ); 413 | PRODUCT_BUNDLE_IDENTIFIER = mercari.ShimmerViewSamples; 414 | PRODUCT_NAME = "$(TARGET_NAME)"; 415 | PROVISIONING_PROFILE_SPECIFIER = ""; 416 | SWIFT_VERSION = 5.0; 417 | TARGETED_DEVICE_FAMILY = "1,2"; 418 | }; 419 | name = Release; 420 | }; 421 | /* End XCBuildConfiguration section */ 422 | 423 | /* Begin XCConfigurationList section */ 424 | 3D1774E2249B77C400A4C7B5 /* Build configuration list for PBXProject "ShimmerViewSamples" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | 3D1774F9249B77C500A4C7B5 /* Debug */, 428 | 3D1774FA249B77C500A4C7B5 /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | 3D1774FB249B77C500A4C7B5 /* Build configuration list for PBXNativeTarget "ShimmerViewSamples" */ = { 434 | isa = XCConfigurationList; 435 | buildConfigurations = ( 436 | 3D1774FC249B77C500A4C7B5 /* Debug */, 437 | 3D1774FD249B77C500A4C7B5 /* Release */, 438 | ); 439 | defaultConfigurationIsVisible = 0; 440 | defaultConfigurationName = Release; 441 | }; 442 | /* End XCConfigurationList section */ 443 | }; 444 | rootObject = 3D1774DF249B77C400A4C7B5 /* Project object */; 445 | } 446 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | // Override point for customization after application launch. 10 | return true 11 | } 12 | 13 | // MARK: UISceneSession Lifecycle 14 | 15 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 16 | // Called when a new scene session is being created. 17 | // Use this method to select a configuration to create the new scene with. 18 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 19 | } 20 | 21 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 22 | // Called when the user discards a scene session. 23 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 24 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 25 | } 26 | 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Basic/BasicViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ShimmerView 3 | 4 | class BasicViewController: UIViewController { 5 | 6 | private var constrationForScrollViewBottom: NSLayoutConstraint? 7 | 8 | private let scrollView: UIScrollView = { 9 | let scrollView = UIScrollView() 10 | scrollView.translatesAutoresizingMaskIntoConstraints = false 11 | scrollView.keyboardDismissMode = .onDrag 12 | scrollView.backgroundColor = .systemBackground 13 | return scrollView 14 | }() 15 | 16 | private let stackView: UIStackView = { 17 | let stackView = UIStackView() 18 | stackView.translatesAutoresizingMaskIntoConstraints = false 19 | stackView.axis = .vertical 20 | stackView.alignment = .fill 21 | stackView.spacing = 0 22 | return stackView 23 | }() 24 | 25 | private lazy var centeringView: UIView = { 26 | let view = UIView() 27 | view.translatesAutoresizingMaskIntoConstraints = false 28 | view.addSubview(shimmerView) 29 | NSLayoutConstraint.activate([ 30 | shimmerView.topAnchor.constraint(equalTo: view.topAnchor), 31 | shimmerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 32 | shimmerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 33 | shimmerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8) 34 | ]) 35 | return view 36 | }() 37 | 38 | private lazy var shimmerView: ShimmerView = { 39 | let view = ShimmerView() 40 | view.translatesAutoresizingMaskIntoConstraints = false 41 | NSLayoutConstraint.activate([ 42 | view.widthAnchor.constraint(equalTo: view.heightAnchor) 43 | ]) 44 | view.layer.cornerRadius = 16 45 | view.layer.masksToBounds = true 46 | return view 47 | }() 48 | 49 | private lazy var baseColorInputView: ColorInputView = { 50 | let view = ColorInputView(kind: .base, defaultValue: viewModel.state.value.baseColor) 51 | return view 52 | }() 53 | 54 | private lazy var highlightColorInputView: ColorInputView = { 55 | let view = ColorInputView(kind: .highlight, defaultValue: viewModel.state.value.highlightColor) 56 | return view 57 | }() 58 | 59 | private lazy var durationInputView: NumberInputView = { 60 | let view = NumberInputView(kind: .duration, defaultValue: CGFloat(viewModel.state.value.duration)) 61 | return view 62 | }() 63 | 64 | private lazy var intervalInputView: NumberInputView = { 65 | let view = NumberInputView(kind: .interval, defaultValue: CGFloat(viewModel.state.value.interval)) 66 | return view 67 | }() 68 | 69 | private lazy var effectSpanInputView: EffectSpanInputView = { 70 | let view = EffectSpanInputView(effectSpan: viewModel.state.value.effectSpan) 71 | return view 72 | }() 73 | 74 | private lazy var effectAngleInputView: EffectAngleInputView = { 75 | let view = EffectAngleInputView(defaultValue: viewModel.state.value.effectAngle) 76 | return view 77 | }() 78 | 79 | private var viewModel: ViewModel 80 | 81 | init() { 82 | self.viewModel = ViewModel() 83 | super.init(nibName: nil, bundle: nil) 84 | } 85 | 86 | required init?(coder: NSCoder) { 87 | fatalError("init(coder:) has not been implemented") 88 | } 89 | 90 | override func viewDidLoad() { 91 | super.viewDidLoad() 92 | 93 | view.backgroundColor = .secondarySystemBackground 94 | 95 | // Notification 96 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) 97 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) 98 | 99 | view.addSubview(scrollView) 100 | NSLayoutConstraint.activate([ 101 | scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 102 | scrollView.widthAnchor.constraint(lessThanOrEqualToConstant: 600), 103 | scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 104 | ]) 105 | let secondry = scrollView.widthAnchor.constraint(equalTo: view.widthAnchor) 106 | secondry.priority = .defaultHigh 107 | secondry.isActive = true 108 | 109 | constrationForScrollViewBottom = scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor) 110 | constrationForScrollViewBottom?.isActive = true 111 | 112 | scrollView.addSubview(stackView) 113 | NSLayoutConstraint.activate([ 114 | stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), 115 | stackView.leftAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leftAnchor), 116 | stackView.rightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.rightAnchor), 117 | stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), 118 | stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) 119 | ]) 120 | 121 | stackView.addArrangedSubview(SpacerView(height: 36)) 122 | 123 | stackView.addArrangedSubview(centeringView) 124 | stackView.setCustomSpacing(36, after: centeringView) 125 | 126 | stackView.addArrangedSubview(baseColorInputView) 127 | stackView.addArrangedSubview(SpacerView(height: 1.0, leftPadding: 16, backgroundColor: .separator)) 128 | stackView.addArrangedSubview(highlightColorInputView) 129 | stackView.addArrangedSubview(SpacerView(height: 1.0, leftPadding: 16, backgroundColor: .separator)) 130 | stackView.addArrangedSubview(durationInputView) 131 | stackView.addArrangedSubview(SpacerView(height: 1.0, leftPadding: 16, backgroundColor: .separator)) 132 | stackView.addArrangedSubview(intervalInputView) 133 | stackView.addArrangedSubview(SpacerView(height: 1.0, leftPadding: 16, backgroundColor: .separator)) 134 | stackView.addArrangedSubview(effectSpanInputView) 135 | stackView.addArrangedSubview(SpacerView(height: 1.0, leftPadding: 16, backgroundColor: .separator)) 136 | stackView.addArrangedSubview(effectAngleInputView) 137 | 138 | stackView.addArrangedSubview(SpacerView(height: 36)) 139 | 140 | shimmerView.startAnimating() 141 | 142 | bind() 143 | } 144 | 145 | private func bind() { 146 | viewModel.bindStyleDidUpdate { [weak self] style in 147 | self?.shimmerView.apply(style: style) 148 | } 149 | 150 | baseColorInputView.bindValueDidUpdate { [weak self] value in 151 | self?.viewModel.baseColorDidUpdate(color: value) 152 | } 153 | 154 | highlightColorInputView.bindValueDidUpdate { [weak self] value in 155 | self?.viewModel.highlightColorDidUpdate(color: value) 156 | } 157 | 158 | durationInputView.bindValueDidUpdate { [weak self] value in 159 | self?.viewModel.durationDidUpdate(duration: value) 160 | } 161 | 162 | intervalInputView.bindValueDidUpdate { [weak self] value in 163 | self?.viewModel.intervalDidUpdate(interval: value) 164 | } 165 | 166 | effectSpanInputView.bindValueDidUpdate { [weak self] value in 167 | self?.viewModel.effectSpanDidUpdate(effectSpan: value) 168 | } 169 | 170 | effectAngleInputView.bindValueDidUpdate { [weak self] value in 171 | self?.viewModel.effectAngleDidUpdate(effectAngle: value) 172 | } 173 | } 174 | 175 | @objc func keyboardWillShow(notification: Notification) { 176 | 177 | guard let userInfo = notification.userInfo else { 178 | return 179 | } 180 | 181 | // animation key 182 | guard let animationKey = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { 183 | return 184 | } 185 | let animationOption = UIView.AnimationOptions(rawValue: animationKey) 186 | 187 | // duration 188 | guard let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval else { 189 | return 190 | } 191 | 192 | guard let endFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { 193 | return 194 | } 195 | 196 | constrationForScrollViewBottom?.constant = -endFrame.height 197 | 198 | UIView.animate(withDuration: animationDuration, 199 | delay: 0.0, 200 | options: animationOption, 201 | animations: { 202 | self.view.layoutIfNeeded() 203 | }) 204 | } 205 | 206 | @objc func keyboardWillHide(notification: Notification) { 207 | guard let userInfo = notification.userInfo else { 208 | return 209 | } 210 | 211 | // animation key 212 | guard let animationKey = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { 213 | return 214 | } 215 | let animationOption = UIView.AnimationOptions(rawValue: animationKey) 216 | 217 | // duration 218 | guard let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval else { 219 | return 220 | } 221 | 222 | constrationForScrollViewBottom?.constant = 0 223 | 224 | UIView.animate(withDuration: animationDuration, 225 | delay: 0.0, 226 | options: animationOption, 227 | animations: { 228 | self.view.layoutIfNeeded() 229 | }) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Basic/BasicViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import ShimmerView 4 | import Combine 5 | 6 | extension BasicViewController { 7 | class ViewModel { 8 | 9 | private var styleDidUpdateClosure: ((ShimmerViewStyle) -> ())? 10 | let state: CurrentValueSubject 11 | private let disposeBag = DisposeBag() 12 | 13 | init() { 14 | state = CurrentValueSubject(.default) 15 | state.removeDuplicates().sink { [weak self] style in 16 | self?.styleDidUpdateClosure?(style) 17 | }.add(to: disposeBag) 18 | } 19 | 20 | func bindStyleDidUpdate(closure: @escaping (ShimmerViewStyle) -> ()) { 21 | styleDidUpdateClosure = closure 22 | } 23 | 24 | func baseColorDidUpdate(color: UIColor) { 25 | state.mutate(\.baseColor, color) 26 | } 27 | 28 | func highlightColorDidUpdate(color: UIColor) { 29 | state.mutate(\.highlightColor, color) 30 | } 31 | 32 | func durationDidUpdate(duration: CGFloat) { 33 | state.mutate(\.duration, CFTimeInterval(duration)) 34 | } 35 | 36 | func intervalDidUpdate(interval: CGFloat) { 37 | state.mutate(\.interval, CFTimeInterval(interval)) 38 | } 39 | 40 | func effectSpanDidUpdate(effectSpan: ShimmerView.EffectSpan) { 41 | state.mutate(\.effectSpan, effectSpan) 42 | } 43 | 44 | func effectAngleDidUpdate(effectAngle: CGFloat) { 45 | state.mutate(\.effectAngle, effectAngle * CGFloat.pi) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Basic/ColorElementInputView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class ColorElementInputView: UIView, UITextFieldDelegate { 4 | private static let maxCharacterCount: Int = 3 5 | 6 | enum Kind { 7 | case r, g, b 8 | var titleString: String { 9 | switch self { 10 | case .r: 11 | return "R" 12 | case .g: 13 | return "G" 14 | case .b: 15 | return "B" 16 | } 17 | } 18 | } 19 | 20 | private var stackView: UIStackView = { 21 | let view = UIStackView() 22 | view.translatesAutoresizingMaskIntoConstraints = false 23 | view.axis = .horizontal 24 | view.spacing = 12.0 25 | view.alignment = .fill 26 | return view 27 | }() 28 | 29 | private var titleLabel: UILabel = { 30 | let label = UILabel() 31 | label.translatesAutoresizingMaskIntoConstraints = false 32 | label.font = .preferredFont(forTextStyle: .body) 33 | return label 34 | }() 35 | 36 | private(set) var slider: UISlider = { 37 | let view = UISlider() 38 | view.translatesAutoresizingMaskIntoConstraints = false 39 | view.minimumValue = 0 40 | view.maximumValue = 255 41 | view.isContinuous = true 42 | return view 43 | }() 44 | 45 | private(set) lazy var textInputView: TextInputView = { 46 | let view = TextInputView() 47 | NSLayoutConstraint.activate([ 48 | view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1.5) 49 | ]) 50 | view.textFiled.delegate = self 51 | return view 52 | }() 53 | 54 | private var valueDidChangeClosure: ((Int) -> ())? 55 | 56 | init(kind: Kind, defaultValue: Int) { 57 | super.init(frame: .zero) 58 | 59 | translatesAutoresizingMaskIntoConstraints = false 60 | 61 | heightAnchor.constraint(equalToConstant: 32).isActive = true 62 | 63 | addSubview(stackView) 64 | NSLayoutConstraint.activate([ 65 | stackView.topAnchor.constraint(equalTo: topAnchor), 66 | stackView.leftAnchor.constraint(equalTo: leftAnchor), 67 | stackView.rightAnchor.constraint(equalTo: rightAnchor), 68 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor) 69 | ]) 70 | 71 | titleLabel.text = kind.titleString 72 | 73 | stackView.addArrangedSubview(titleLabel) 74 | stackView.addArrangedSubview(slider) 75 | stackView.addArrangedSubview(textInputView) 76 | 77 | slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged) 78 | slider.addTarget(self, action: #selector(sliderDidEndEditing), for: .touchUpInside) 79 | slider.addTarget(self, action: #selector(sliderDidEndEditing), for: .touchUpOutside) 80 | 81 | slider.value = Float(max(min(defaultValue, 255), 0)) 82 | textInputView.textFiled.text = "\(max(min(defaultValue, 255), 0))" 83 | } 84 | 85 | required init?(coder: NSCoder) { 86 | fatalError("init(coder:) has not been implemented") 87 | } 88 | 89 | func bindValuDidChange(closure: @escaping (Int) -> ()) { 90 | valueDidChangeClosure = closure 91 | } 92 | 93 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 94 | // if string count == 0, return true 95 | guard string.count != 0 else { 96 | return true 97 | } 98 | 99 | // string should be consisted only from numbers 100 | guard CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string)) else { 101 | return false 102 | } 103 | 104 | // max length of the letters should be 3 105 | guard ((textField.text ?? "") + string).count <= ColorElementInputView.maxCharacterCount else { 106 | return false 107 | } 108 | 109 | return true 110 | } 111 | 112 | func textFieldDidEndEditing(_ textField: UITextField) { 113 | guard let text = textField.text, 114 | let convertedNumber = Int(text) else { 115 | return 116 | } 117 | let newValue = max(min(convertedNumber, 255), 0) 118 | textField.text = "\(newValue)" 119 | slider.value = Float(newValue) 120 | valueDidChangeClosure?(newValue) 121 | } 122 | 123 | @objc func sliderValueChanged() { 124 | textInputView.textFiled.text = "\(Int(slider.value))" 125 | } 126 | 127 | @objc func sliderDidEndEditing() { 128 | valueDidChangeClosure?(Int(slider.value)) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Basic/ColorInputView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | 4 | class ColorInputView: UIView { 5 | struct State: Equatable { 6 | var r: Int 7 | var g: Int 8 | var b: Int 9 | 10 | init(color: UIColor) { 11 | let components = color.components 12 | r = Int(components.red*255) 13 | g = Int(components.green*255) 14 | b = Int(components.blue*255) 15 | } 16 | 17 | var uiColor: UIColor { 18 | UIColor(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: 1.0) 19 | } 20 | } 21 | 22 | enum Kind { 23 | case base, highlight 24 | var titleString: String { 25 | switch self { 26 | case .base: 27 | return "Base Color" 28 | case .highlight: 29 | return "Highlight Color" 30 | } 31 | } 32 | } 33 | 34 | private lazy var titleLabel: UILabel = { 35 | let label = UILabel() 36 | label.translatesAutoresizingMaskIntoConstraints = false 37 | label.font = UIFont.preferredFont(forTextStyle: .headline) 38 | label.textAlignment = .left 39 | return label 40 | }() 41 | 42 | private lazy var colorCircleView: UIView = { 43 | let view = UIView() 44 | view.translatesAutoresizingMaskIntoConstraints = false 45 | view.layer.cornerRadius = 4 46 | view.layer.masksToBounds = true 47 | NSLayoutConstraint.activate([ 48 | view.widthAnchor.constraint(equalToConstant: 20), 49 | view.heightAnchor.constraint(equalToConstant: 20) 50 | ]) 51 | return view 52 | }() 53 | 54 | private lazy var titleStackView: UIStackView = { 55 | let view = UIStackView() 56 | view.translatesAutoresizingMaskIntoConstraints = false 57 | view.axis = .horizontal 58 | view.spacing = 8 59 | view.alignment = .center 60 | view.addArrangedSubview(titleLabel) 61 | view.addArrangedSubview(colorCircleView) 62 | view.addArrangedSubview(SpacerView(height: 0)) 63 | view.heightAnchor.constraint(equalToConstant: 56).isActive = true 64 | return view 65 | }() 66 | 67 | private lazy var rInputView: ColorElementInputView = { 68 | let view = ColorElementInputView(kind: .r, defaultValue: state.value.r) 69 | return view 70 | }() 71 | 72 | private lazy var gInputView: ColorElementInputView = { 73 | let view = ColorElementInputView(kind: .g, defaultValue: state.value.g) 74 | return view 75 | }() 76 | 77 | private lazy var bInputView: ColorElementInputView = { 78 | let view = ColorElementInputView(kind: .b, defaultValue: state.value.b) 79 | return view 80 | }() 81 | 82 | private var stackView: UIStackView = { 83 | let view = UIStackView() 84 | view.translatesAutoresizingMaskIntoConstraints = false 85 | view.axis = .vertical 86 | view.alignment = .fill 87 | view.spacing = 0 88 | return view 89 | }() 90 | 91 | private var valueDidUpdateClosure: ((UIColor) -> Void)? 92 | private var state: CurrentValueSubject 93 | private var disposeBag = DisposeBag() 94 | 95 | init(kind: Kind, defaultValue: UIColor) { 96 | state = CurrentValueSubject(State(color: defaultValue)) 97 | 98 | super.init(frame: .zero) 99 | 100 | self.translatesAutoresizingMaskIntoConstraints = false 101 | 102 | titleLabel.text = kind.titleString 103 | 104 | addSubview(stackView) 105 | NSLayoutConstraint.activate([ 106 | stackView.topAnchor.constraint(equalTo: topAnchor), 107 | stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: 16), 108 | stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -16), 109 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor) 110 | ]) 111 | stackView.addArrangedSubview(titleStackView) 112 | stackView.addArrangedSubview(rInputView) 113 | stackView.addArrangedSubview(SpacerView(height: 12)) 114 | stackView.addArrangedSubview(gInputView) 115 | stackView.addArrangedSubview(SpacerView(height: 12)) 116 | stackView.addArrangedSubview(bInputView) 117 | stackView.addArrangedSubview(SpacerView(height: 16)) 118 | 119 | rInputView.bindValuDidChange { [weak self] newValue in 120 | self?.state.mutate(\.r, newValue) 121 | } 122 | 123 | gInputView.bindValuDidChange { [weak self] newValue in 124 | self?.state.mutate(\.g, newValue) 125 | } 126 | 127 | bInputView.bindValuDidChange { [weak self] newValue in 128 | self?.state.mutate(\.b, newValue) 129 | } 130 | 131 | state.removeDuplicates().sink { [weak self] output in 132 | self?.colorCircleView.backgroundColor = output.uiColor 133 | }.add(to: disposeBag) 134 | 135 | state.dropFirst().removeDuplicates().sink { [weak self] output in 136 | self?.valueDidUpdateClosure?(output.uiColor) 137 | }.add(to: disposeBag) 138 | } 139 | 140 | required init?(coder: NSCoder) { 141 | fatalError("init(coder:) has not been implemented") 142 | } 143 | 144 | func bindValueDidUpdate(closure: @escaping (UIColor) -> Void) { 145 | valueDidUpdateClosure = closure 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Basic/EffectAngleInputView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | 4 | class EffectAngleInputView: UIView { 5 | private static let maxCharacterCount: Int = 4 6 | private var allowedCharSet: CharacterSet = { 7 | var baseCharSet = CharacterSet.decimalDigits 8 | baseCharSet.formUnion(CharacterSet(charactersIn: ".")) 9 | return baseCharSet 10 | }() 11 | private var numberFormatter: NumberFormatter = { 12 | var formatter = NumberFormatter() 13 | formatter.minimumFractionDigits = 0 14 | formatter.maximumFractionDigits = 2 15 | return formatter 16 | }() 17 | 18 | private var titleLabel: UILabel = { 19 | let label = UILabel() 20 | label.translatesAutoresizingMaskIntoConstraints = false 21 | label.font = .preferredFont(forTextStyle: .headline) 22 | label.heightAnchor.constraint(equalToConstant: 56).isActive = true 23 | label.text = "Effect Angle" 24 | return label 25 | }() 26 | 27 | private var stackView: UIStackView = { 28 | let view = UIStackView() 29 | view.axis = .vertical 30 | view.translatesAutoresizingMaskIntoConstraints = false 31 | return view 32 | }() 33 | 34 | private lazy var inputBaseView: UIStackView = { 35 | let view = UIStackView() 36 | view.axis = .horizontal 37 | view.spacing = 8.0 38 | view.translatesAutoresizingMaskIntoConstraints = false 39 | view.heightAnchor.constraint(equalToConstant: 32).isActive = true 40 | view.addArrangedSubview(slider) 41 | view.addArrangedSubview(textInputView) 42 | view.addArrangedSubview(piLabel) 43 | return view 44 | }() 45 | 46 | private(set) lazy var slider: UISlider = { 47 | let view = UISlider() 48 | view.translatesAutoresizingMaskIntoConstraints = false 49 | view.minimumValue = 0 50 | view.maximumValue = 2 51 | view.isContinuous = true 52 | return view 53 | }() 54 | 55 | private(set) lazy var textInputView: TextInputView = { 56 | let view = TextInputView() 57 | NSLayoutConstraint.activate([ 58 | view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: 2.0) 59 | ]) 60 | view.textFiled.delegate = self 61 | return view 62 | }() 63 | 64 | private lazy var piLabel: UILabel = { 65 | let view = UILabel() 66 | view.translatesAutoresizingMaskIntoConstraints = false 67 | view.font = .preferredFont(forTextStyle: .body) 68 | view.text = "π" 69 | return view 70 | }() 71 | 72 | private var state: CurrentValueSubject 73 | private var valueDidUpdateClosure: ((CGFloat) -> ())? 74 | private var disposeBag = DisposeBag() 75 | 76 | init(defaultValue: CGFloat) { 77 | state = CurrentValueSubject(max(min(CGFloat(defaultValue), 2), 0)) 78 | super.init(frame: .zero) 79 | 80 | translatesAutoresizingMaskIntoConstraints = false 81 | 82 | addSubview(stackView) 83 | NSLayoutConstraint.activate([ 84 | stackView.topAnchor.constraint(equalTo: topAnchor), 85 | stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: 16), 86 | stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -16), 87 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor) 88 | ]) 89 | 90 | stackView.addArrangedSubview(titleLabel) 91 | stackView.addArrangedSubview(inputBaseView) 92 | stackView.addArrangedSubview(SpacerView(height: 16)) 93 | 94 | slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged) 95 | slider.addTarget(self, action: #selector(sliderDidEndEditing), for: .touchUpInside) 96 | slider.addTarget(self, action: #selector(sliderDidEndEditing), for: .touchUpOutside) 97 | 98 | bind() 99 | } 100 | 101 | required init?(coder: NSCoder) { 102 | fatalError("init(coder:) has not been implemented") 103 | } 104 | 105 | private func bind() { 106 | state.sink { [weak self] value in 107 | self?.textInputView.textFiled.text = self?.numberFormatter.string(for: value) 108 | }.add(to: disposeBag) 109 | } 110 | 111 | func bindValueDidUpdate(closure: @escaping (CGFloat) -> ()) { 112 | self.valueDidUpdateClosure = closure 113 | } 114 | 115 | @objc func sliderValueChanged() { 116 | state.send(CGFloat(slider.value)) 117 | } 118 | 119 | @objc func sliderDidEndEditing() { 120 | valueDidUpdateClosure?(state.value) 121 | } 122 | } 123 | 124 | extension EffectAngleInputView: UITextFieldDelegate { 125 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 126 | // if string count == 0, return true 127 | guard string.count != 0 else { 128 | return true 129 | } 130 | 131 | // string should be consisted only from numbers 132 | guard allowedCharSet.isSuperset(of: CharacterSet(charactersIn: string)) else { 133 | return false 134 | } 135 | 136 | // max length of the letters 137 | guard ((textField.text ?? "") + string).count <= EffectAngleInputView.maxCharacterCount else { 138 | return false 139 | } 140 | 141 | return true 142 | } 143 | 144 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 145 | textField.resignFirstResponder() 146 | return true 147 | } 148 | 149 | func textFieldDidEndEditing(_ textField: UITextField) { 150 | guard let text = textField.text else { 151 | return 152 | } 153 | 154 | guard let convertedValue = Float(text) else { 155 | textField.text = numberFormatter.string(for: state.value) 156 | return 157 | } 158 | 159 | state.send(max(min(CGFloat(convertedValue), 2), 0)) 160 | 161 | valueDidUpdateClosure?(state.value) 162 | return 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Basic/EffectSpanInputView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ShimmerView 3 | import Combine 4 | 5 | private extension ShimmerView.EffectSpan { 6 | var number: CGFloat { 7 | switch self { 8 | case .points(let points): 9 | return points 10 | case .ratio(let ratio): 11 | return ratio*100 12 | } 13 | } 14 | 15 | var effectSpanInputViewButtonKind: EffectSpanInputView.ButtonKind { 16 | switch self { 17 | case .points: 18 | return .points 19 | case .ratio: 20 | return .ratio 21 | } 22 | } 23 | 24 | init(buttonKind: EffectSpanInputView.ButtonKind, number: CGFloat) { 25 | switch buttonKind { 26 | case .points: 27 | self = .points(number) 28 | case .ratio: 29 | self = .ratio(number*0.01) 30 | } 31 | } 32 | 33 | var effectSpanInputViewState: EffectSpanInputView.State { 34 | return .init(selectedButton: effectSpanInputViewButtonKind, number: number) 35 | } 36 | } 37 | 38 | class EffectSpanInputView: UIView { 39 | 40 | private static let maxCharacterCount: Int = 4 41 | private var allowedCharSet: CharacterSet = { 42 | var baseCharSet = CharacterSet.decimalDigits 43 | baseCharSet.formUnion(CharacterSet(charactersIn: ".")) 44 | return baseCharSet 45 | }() 46 | private var numberFormatter: NumberFormatter = { 47 | var formatter = NumberFormatter() 48 | formatter.minimumFractionDigits = 0 49 | formatter.maximumFractionDigits = 1 50 | return formatter 51 | }() 52 | 53 | fileprivate enum ButtonKind: Int, CaseIterable { 54 | case ratio, points 55 | 56 | var titleString: String { 57 | switch self { 58 | case .ratio: 59 | return "Ratio" 60 | case .points: 61 | return "Points" 62 | } 63 | } 64 | 65 | var unitString: String { 66 | switch self { 67 | case .ratio: 68 | return "%" 69 | case .points: 70 | return "Points" 71 | } 72 | } 73 | } 74 | 75 | fileprivate struct State: Equatable { 76 | var selectedButton: ButtonKind 77 | var number: CGFloat 78 | var effectSpan: ShimmerView.EffectSpan { 79 | return .init(buttonKind: selectedButton, number: number) 80 | } 81 | } 82 | 83 | private var titleLabel: UILabel = { 84 | let label = UILabel() 85 | label.translatesAutoresizingMaskIntoConstraints = false 86 | label.font = .preferredFont(forTextStyle: .headline) 87 | label.heightAnchor.constraint(equalToConstant: 56).isActive = true 88 | label.text = "Effect Span" 89 | return label 90 | }() 91 | 92 | private var stackView: UIStackView = { 93 | let view = UIStackView() 94 | view.axis = .vertical 95 | view.translatesAutoresizingMaskIntoConstraints = false 96 | return view 97 | }() 98 | 99 | private lazy var inputBaseView: UIView = { 100 | let view = UIView() 101 | view.translatesAutoresizingMaskIntoConstraints = false 102 | view.heightAnchor.constraint(equalToConstant: 32).isActive = true 103 | view.addSubview(buttonsStackView) 104 | NSLayoutConstraint.activate([ 105 | buttonsStackView.leftAnchor.constraint(equalTo: view.leftAnchor), 106 | buttonsStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor) 107 | ]) 108 | 109 | view.addSubview(numberInputStackView) 110 | NSLayoutConstraint.activate([ 111 | numberInputStackView.rightAnchor.constraint(equalTo: view.rightAnchor), 112 | numberInputStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor) 113 | ]) 114 | return view 115 | }() 116 | 117 | private lazy var buttons: [OutlinedButton] = { 118 | var buttons = [OutlinedButton]() 119 | for buttonKind in ButtonKind.allCases { 120 | let button = OutlinedButton() 121 | button.setTitle(buttonKind.titleString, for: .normal) 122 | button.tag = buttonKind.rawValue 123 | button.addTarget(self, action: #selector(buttonDidTap(button:)), for: .touchUpInside) 124 | buttons.append(button) 125 | } 126 | return buttons 127 | }() 128 | 129 | private lazy var buttonsStackView: UIStackView = { 130 | let view = UIStackView() 131 | view.translatesAutoresizingMaskIntoConstraints = false 132 | view.axis = .horizontal 133 | view.spacing = 8 134 | for button in buttons { 135 | view.addArrangedSubview(button) 136 | } 137 | return view 138 | }() 139 | 140 | private lazy var numberInputView: TextInputView = { 141 | let view = TextInputView() 142 | view.translatesAutoresizingMaskIntoConstraints = false 143 | view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: 2.0).isActive = true 144 | view.textFiled.textAlignment = .right 145 | view.textFiled.delegate = self 146 | return view 147 | }() 148 | 149 | private lazy var unitLabel: UILabel = { 150 | let label = UILabel() 151 | label.translatesAutoresizingMaskIntoConstraints = false 152 | label.font = .preferredFont(forTextStyle: .body) 153 | label.text = "%" 154 | return label 155 | }() 156 | 157 | private lazy var numberInputStackView: UIStackView = { 158 | let view = UIStackView() 159 | view.translatesAutoresizingMaskIntoConstraints = false 160 | view.alignment = .center 161 | view.axis = .horizontal 162 | view.spacing = 8.0 163 | view.addArrangedSubview(numberInputView) 164 | view.addArrangedSubview(unitLabel) 165 | return view 166 | }() 167 | 168 | private var valueDidUpdateClosure: ((ShimmerView.EffectSpan) -> ())? 169 | 170 | private let state: CurrentValueSubject 171 | private var disposeBag = DisposeBag() 172 | 173 | init(effectSpan: ShimmerView.EffectSpan) { 174 | state = CurrentValueSubject(effectSpan.effectSpanInputViewState) 175 | 176 | super.init(frame: .zero) 177 | 178 | translatesAutoresizingMaskIntoConstraints = false 179 | 180 | addSubview(stackView) 181 | NSLayoutConstraint.activate([ 182 | stackView.topAnchor.constraint(equalTo: topAnchor), 183 | stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: 16), 184 | stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -16), 185 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor) 186 | ]) 187 | 188 | stackView.addArrangedSubview(titleLabel) 189 | stackView.addArrangedSubview(inputBaseView) 190 | stackView.addArrangedSubview(SpacerView(height: 16)) 191 | 192 | bind() 193 | } 194 | 195 | private func bind() { 196 | state.removeDuplicates().sink { [weak self] state in 197 | guard let self = self else { return } 198 | 199 | self.numberInputView.textFiled.text = self.numberFormatter.string(for: state.number) 200 | for _button in self.buttons { 201 | _button.isSelected = state.selectedButton.rawValue == _button.tag 202 | } 203 | self.unitLabel.text = state.selectedButton.unitString 204 | }.add(to: disposeBag) 205 | 206 | state.map(\.effectSpan).dropFirst().removeDuplicates().sink { [weak self] effectSpan in 207 | self?.valueDidUpdateClosure?(effectSpan) 208 | }.add(to: disposeBag) 209 | } 210 | 211 | required init?(coder: NSCoder) { 212 | fatalError("init(coder:) has not been implemented") 213 | } 214 | 215 | @objc func buttonDidTap(button: UIButton) { 216 | guard let selectedButton = ButtonKind(rawValue: button.tag) else { 217 | return 218 | } 219 | 220 | guard selectedButton != state.value.selectedButton else { 221 | return 222 | } 223 | 224 | state.mutate { state in 225 | state.selectedButton = selectedButton 226 | state.number = 0 227 | } 228 | } 229 | 230 | func bindValueDidUpdate(closure: @escaping (ShimmerView.EffectSpan) -> ()) { 231 | self.valueDidUpdateClosure = closure 232 | } 233 | } 234 | 235 | extension EffectSpanInputView: UITextFieldDelegate { 236 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 237 | if string.count == 0 { 238 | return true 239 | } 240 | 241 | if !allowedCharSet.isSuperset(of: CharacterSet(charactersIn: string)) { 242 | return false 243 | } 244 | 245 | let newString = (textField.text ?? "") + string 246 | return newString.count <= EffectSpanInputView.maxCharacterCount 247 | } 248 | 249 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 250 | textField.resignFirstResponder() 251 | return true 252 | } 253 | 254 | func textFieldDidEndEditing(_ textField: UITextField) { 255 | guard let text = textField.text else { 256 | return 257 | } 258 | 259 | guard let convertedValue = Float(text) else { 260 | textField.text = numberFormatter.string(for: state.value.number) 261 | return 262 | } 263 | 264 | state.mutate(\.number, CGFloat(convertedValue)) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Basic/NumberInputView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Combine 3 | 4 | class NumberInputView: UIView, UITextFieldDelegate { 5 | private static let maxCharacterCount: Int = 4 6 | private var allowedCharSet: CharacterSet = { 7 | var baseCharSet = CharacterSet.decimalDigits 8 | baseCharSet.formUnion(CharacterSet(charactersIn: ".")) 9 | return baseCharSet 10 | }() 11 | private var numberFormatter: NumberFormatter = { 12 | var formatter = NumberFormatter() 13 | formatter.minimumFractionDigits = 0 14 | formatter.maximumFractionDigits = 2 15 | return formatter 16 | }() 17 | 18 | enum Kind { 19 | case duration, interval 20 | var titleString: String { 21 | switch self { 22 | case .duration: 23 | return "Duration" 24 | case .interval: 25 | return "Interval" 26 | } 27 | } 28 | } 29 | 30 | private var titleLabel: UILabel = { 31 | let label = UILabel() 32 | label.translatesAutoresizingMaskIntoConstraints = false 33 | label.font = .preferredFont(forTextStyle: .headline) 34 | return label 35 | }() 36 | 37 | private(set) lazy var textInputView: TextInputView = { 38 | let view = TextInputView() 39 | NSLayoutConstraint.activate([ 40 | view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: 2.0) 41 | ]) 42 | view.textFiled.delegate = self 43 | return view 44 | }() 45 | 46 | private var state: CurrentValueSubject 47 | private var valueDidUpdateClosure: ((CGFloat) -> ())? 48 | private var disposeBag = DisposeBag() 49 | 50 | init(kind: Kind, defaultValue: CGFloat) { 51 | state = CurrentValueSubject(defaultValue) 52 | super.init(frame: .zero) 53 | translatesAutoresizingMaskIntoConstraints = false 54 | 55 | heightAnchor.constraint(equalToConstant: 56).isActive = true 56 | 57 | addSubview(titleLabel) 58 | titleLabel.text = kind.titleString 59 | 60 | NSLayoutConstraint.activate([ 61 | titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), 62 | titleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 16) 63 | ]) 64 | 65 | addSubview(textInputView) 66 | NSLayoutConstraint.activate([ 67 | textInputView.centerYAnchor.constraint(equalTo: centerYAnchor), 68 | textInputView.rightAnchor.constraint(equalTo: rightAnchor, constant: -16) 69 | ]) 70 | 71 | bind() 72 | } 73 | 74 | private func bind() { 75 | state.sink { [weak self] value in 76 | self?.textInputView.textFiled.text = self?.numberFormatter.string(for: value) 77 | }.add(to: disposeBag) 78 | 79 | state.dropFirst().removeDuplicates().sink { [weak self] value in 80 | self?.valueDidUpdateClosure?(value) 81 | }.add(to: disposeBag) 82 | } 83 | 84 | required init?(coder: NSCoder) { 85 | fatalError("init(coder:) has not been implemented") 86 | } 87 | 88 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 89 | // if string count == 0, return true 90 | guard string.count != 0 else { 91 | return true 92 | } 93 | 94 | // string should be consisted only from numbers 95 | guard allowedCharSet.isSuperset(of: CharacterSet(charactersIn: string)) else { 96 | return false 97 | } 98 | 99 | // max length of the letters 100 | guard ((textField.text ?? "") + string).count <= NumberInputView.maxCharacterCount else { 101 | return false 102 | } 103 | 104 | return true 105 | } 106 | 107 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 108 | textField.resignFirstResponder() 109 | return true 110 | } 111 | 112 | func textFieldDidEndEditing(_ textField: UITextField) { 113 | guard let text = textField.text else { 114 | return 115 | } 116 | 117 | guard let convertedValue = Float(text) else { 118 | textField.text = numberFormatter.string(for: state.value) 119 | return 120 | } 121 | 122 | state.send(CGFloat(convertedValue)) 123 | 124 | return 125 | } 126 | 127 | func bindValueDidUpdate(closure: @escaping (CGFloat) -> ()) { 128 | self.valueDidUpdateClosure = closure 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/CommonUI/OutlinedButton.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class OutlinedButton: UIButton { 4 | init() { 5 | super.init(frame: .zero) 6 | 7 | translatesAutoresizingMaskIntoConstraints = false 8 | 9 | layer.borderWidth = 1.0 10 | layer.borderColor = UIColor.magenta.cgColor 11 | layer.masksToBounds = true 12 | layer.cornerRadius = 16 13 | heightAnchor.constraint(equalToConstant: 32).isActive = true 14 | titleEdgeInsets = .init(top: 0, left: 16, bottom: 0, right: 16) 15 | titleLabel?.font = .preferredFont(forTextStyle: .body) 16 | 17 | setTitleColor(.magenta, for: .normal) 18 | setTitleColor(.systemBackground, for: .selected) 19 | 20 | setBackgroundImage(UIColor.clear.image(size: CGSize(width: 1, height: 1)), for: .normal) 21 | setBackgroundImage(UIColor.magenta.image(size: CGSize(width: 1, height: 1)), for: .selected) 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | override var intrinsicContentSize: CGSize { 29 | let width = (titleLabel?.intrinsicContentSize.width ?? 0) + titleEdgeInsets.left + titleEdgeInsets.right 30 | return CGSize(width: width, height: super.intrinsicContentSize.height) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/CommonUI/SpacerView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SpacerView: UIView { 4 | 5 | private var lineView: UIView = { 6 | let view = UIView() 7 | view.translatesAutoresizingMaskIntoConstraints = false 8 | return view 9 | }() 10 | 11 | init(height: CGFloat, leftPadding: CGFloat = 0, backgroundColor: UIColor = .clear) { 12 | super.init(frame: .zero) 13 | 14 | self.translatesAutoresizingMaskIntoConstraints = false 15 | NSLayoutConstraint.activate([ 16 | heightAnchor.constraint(equalToConstant: height) 17 | ]) 18 | 19 | addSubview(lineView) 20 | NSLayoutConstraint.activate([ 21 | lineView.topAnchor.constraint(equalTo: topAnchor), 22 | lineView.leftAnchor.constraint(equalTo: leftAnchor, constant: leftPadding), 23 | lineView.rightAnchor.constraint(equalTo: rightAnchor), 24 | lineView.bottomAnchor.constraint(equalTo: bottomAnchor) 25 | ]) 26 | 27 | lineView.backgroundColor = backgroundColor 28 | } 29 | 30 | required init?(coder: NSCoder) { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/CommonUI/TextInputView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class TextInputView: UIView { 4 | 5 | private(set) lazy var textFiled: UITextField = { 6 | let view = UITextField() 7 | view.translatesAutoresizingMaskIntoConstraints = false 8 | view.font = .preferredFont(forTextStyle: .body) 9 | view.textAlignment = .center 10 | return view 11 | }() 12 | 13 | init() { 14 | super.init(frame: .zero) 15 | 16 | translatesAutoresizingMaskIntoConstraints = false 17 | 18 | backgroundColor = .secondarySystemBackground 19 | layer.cornerRadius = 4 20 | layer.masksToBounds = true 21 | 22 | heightAnchor.constraint(equalToConstant: 32).isActive = true 23 | 24 | addSubview(textFiled) 25 | NSLayoutConstraint.activate([ 26 | textFiled.topAnchor.constraint(equalTo: topAnchor), 27 | textFiled.leftAnchor.constraint(equalTo: leftAnchor, constant: 8), 28 | textFiled.rightAnchor.constraint(equalTo: rightAnchor, constant: -8), 29 | textFiled.bottomAnchor.constraint(equalTo: bottomAnchor) 30 | ]) 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Extensions/CurrentValueSubject+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | extension CurrentValueSubject { 5 | func mutate(closure: (inout Output) -> ()) { 6 | var _value = value 7 | closure(&_value) 8 | send(_value) 9 | } 10 | 11 | func mutate(_ keyPath: WritableKeyPath, _ newValue: T) { 12 | var _value = value 13 | _value[keyPath: keyPath] = newValue 14 | send(_value) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Extensions/DisposeBag.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | class DisposeBag { 4 | fileprivate var cancellables = [AnyCancellable]() 5 | } 6 | 7 | extension AnyCancellable { 8 | func add(to disposeBag: DisposeBag) { 9 | disposeBag.cancellables.append(self) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Extensions/UIColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | func image(size: CGSize) -> UIImage { 5 | let renderer = UIGraphicsImageRenderer(size: size) 6 | return renderer.image(actions: { rendererContext in 7 | rendererContext.cgContext.setFillColor(self.cgColor) 8 | rendererContext.fill(CGRect(origin: .zero, size: size)) 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/List/ListViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ShimmerView 3 | 4 | class ListViewCell: UIView, ShimmerReplicatorViewCell { 5 | @IBOutlet weak var thumbnailView: ShimmerView! 6 | @IBOutlet weak var firstLineView: ShimmerView! 7 | @IBOutlet weak var secondLineView: ShimmerView! 8 | @IBOutlet weak var thirdLineView: ShimmerView! 9 | 10 | required init?(coder aDecoder: NSCoder) { 11 | super.init(coder: aDecoder) 12 | } 13 | 14 | override func awakeFromNib() { 15 | super.awakeFromNib() 16 | } 17 | 18 | func startAnimating() { 19 | thumbnailView.startAnimating() 20 | firstLineView.startAnimating() 21 | secondLineView.startAnimating() 22 | thirdLineView.startAnimating() 23 | } 24 | 25 | func stopAnimating() { 26 | thumbnailView.stopAnimating() 27 | firstLineView.stopAnimating() 28 | secondLineView.stopAnimating() 29 | thirdLineView.stopAnimating() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/List/ListViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/List/ListViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ShimmerView 3 | 4 | class ListViewController: UIViewController, ShimmerSyncTarget { 5 | 6 | var style: ShimmerViewStyle = { 7 | var style = ShimmerViewStyle.default 8 | style.baseColor = UIColor(dynamicProvider: { trait -> UIColor in 9 | if trait.userInterfaceStyle == .dark { 10 | return UIColor(red: 205/255, green: 205/255, blue: 205/255, alpha: 1.0) 11 | } else { 12 | return UIColor(red: 220/255, green: 220/255, blue: 220/255, alpha: 1.0) 13 | } 14 | }) 15 | return style 16 | }() 17 | var effectBeginTime = CACurrentMediaTime() 18 | 19 | private var shimmerReplicatorView: ShimmerReplicatorView = { 20 | let view = ShimmerReplicatorView(itemSize: .fixedHeight(120)) { () -> ShimmerReplicatorViewCell in 21 | return UINib(nibName: "ListViewCell", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ListViewCell 22 | } 23 | view.translatesAutoresizingMaskIntoConstraints = false 24 | return view 25 | }() 26 | 27 | init() { 28 | super.init(nibName: nil, bundle: nil) 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | view.backgroundColor = .systemBackground 39 | 40 | view.addSubview(shimmerReplicatorView) 41 | NSLayoutConstraint.activate([ 42 | shimmerReplicatorView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 43 | shimmerReplicatorView.leftAnchor.constraint(equalTo: view.leftAnchor), 44 | shimmerReplicatorView.rightAnchor.constraint(equalTo: view.rightAnchor), 45 | shimmerReplicatorView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 46 | ]) 47 | 48 | shimmerReplicatorView.startAnimating() 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/MainViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class MainViewController: UITableViewController { 4 | 5 | enum Section { 6 | case main 7 | } 8 | 9 | enum Row: CaseIterable { 10 | static var allCases: [Row] { 11 | if #available(iOS 14, *) { 12 | return [.basic, .list, .swiftUI] 13 | } else { 14 | return [.basic, .list] 15 | } 16 | } 17 | 18 | case basic, list 19 | @available(iOS 14, *) 20 | case swiftUI 21 | 22 | var titleString: String { 23 | switch self { 24 | case .basic: 25 | return "Basic" 26 | case .list: 27 | return "List" 28 | case .swiftUI: 29 | return "SwiftUI" 30 | } 31 | } 32 | } 33 | 34 | private var dataSource: UITableViewDiffableDataSource? 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") 40 | tableView.delegate = self 41 | tableView.rowHeight = 56 42 | 43 | dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, data) -> UITableViewCell? in 44 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) 45 | cell.textLabel?.text = data.titleString 46 | cell.textLabel?.font = .preferredFont(forTextStyle: .body) 47 | return cell 48 | }) 49 | 50 | var snapshot = NSDiffableDataSourceSnapshot() 51 | snapshot.appendSections([.main]) 52 | snapshot.appendItems(Row.allCases) 53 | dataSource?.apply(snapshot, animatingDifferences: false) 54 | } 55 | } 56 | 57 | extension MainViewController { 58 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 59 | switch Row.allCases[indexPath.row] { 60 | case .basic: 61 | let vc = BasicViewController() 62 | navigationController?.pushViewController(vc, animated: true) 63 | 64 | case .list: 65 | let vc = ListViewController() 66 | navigationController?.pushViewController(vc, animated: true) 67 | 68 | case .swiftUI: 69 | if #available(iOS 14.0, *) { 70 | let vc = SwiftUIViewController() 71 | navigationController?.pushViewController(vc, animated: true) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | 5 | var window: UIWindow? 6 | 7 | 8 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 9 | guard let scene = (scene as? UIWindowScene) else { 10 | return 11 | } 12 | let window = UIWindow(windowScene: scene) 13 | self.window = window 14 | window.makeKeyAndVisible() 15 | 16 | let vc = MainViewController() 17 | let navigationController = UINavigationController(rootViewController: vc) 18 | window.rootViewController = navigationController 19 | } 20 | 21 | func sceneDidDisconnect(_ scene: UIScene) { 22 | // Called as the scene is being released by the system. 23 | // This occurs shortly after the scene enters the background, or when its session is discarded. 24 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 25 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 26 | } 27 | 28 | func sceneDidBecomeActive(_ scene: UIScene) { 29 | // Called when the scene has moved from an inactive state to an active state. 30 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 31 | } 32 | 33 | func sceneWillResignActive(_ scene: UIScene) { 34 | // Called when the scene will move from an active state to an inactive state. 35 | // This may occur due to temporary interruptions (ex. an incoming phone call). 36 | } 37 | 38 | func sceneWillEnterForeground(_ scene: UIScene) { 39 | // Called as the scene transitions from the background to the foreground. 40 | // Use this method to undo the changes made on entering the background. 41 | } 42 | 43 | func sceneDidEnterBackground(_ scene: UIScene) { 44 | // Called as the scene transitions from the foreground to the background. 45 | // Use this method to save data, release shared resources, and store enough scene-specific state information 46 | // to restore the scene back to its current state. 47 | } 48 | 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/SwiftUI/Placeholder.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import ShimmerView 3 | 4 | @available(iOS 14.0, *) 5 | public struct Placeholder: View { 6 | 7 | @State 8 | private var isAnimating: Bool = true 9 | 10 | public var body: some View { 11 | ShimmerScope(isAnimating: $isAnimating) { 12 | LazyVStack(alignment: .leading, spacing: 0) { 13 | Spacer(minLength: 16) 14 | ShimmerElement(width: 100, height: 12) 15 | Spacer(minLength: 16) 16 | ShimmerElement(height: 60) 17 | Spacer(minLength: 24) 18 | ShimmerElement(width: 100, height: 12) 19 | Spacer(minLength: 16) 20 | ForEach(0..<8) { _ in 21 | HStack(alignment: .center, spacing: 4) { 22 | ShimmerElement() 23 | .aspectRatio(1, contentMode: .fit) 24 | .cornerRadius(4) 25 | ShimmerElement() 26 | .aspectRatio(1, contentMode: .fit) 27 | .cornerRadius(4) 28 | ShimmerElement() 29 | .aspectRatio(1, contentMode: .fit) 30 | .cornerRadius(4) 31 | } 32 | .padding(.bottom, 4) 33 | } 34 | } 35 | .padding(.horizontal, 16) 36 | } 37 | } 38 | 39 | public init() {} 40 | } 41 | 42 | -------------------------------------------------------------------------------- /ShimmerViewSamples/ShimmerViewSamples/SwiftUI/SwiftUIViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | @available(iOS 14.0, *) 5 | final class SwiftUIViewController: UIHostingController { 6 | init() { 7 | super.init(rootView: Placeholder()) 8 | } 9 | 10 | @MainActor @objc required dynamic init?(coder aDecoder: NSCoder) { 11 | fatalError("init(coder:) has not been implemented") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Common/ShimmerCoreView.Animator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import simd 3 | 4 | internal extension ShimmerCoreView { 5 | struct Animator { 6 | var baseBounds: CGRect 7 | var elementFrame: CGRect 8 | var gradientFrame: CGRect 9 | var style: ShimmerViewStyle 10 | var effectBeginTime: CFTimeInterval 11 | 12 | init( 13 | baseBounds: CGRect, 14 | elementFrame: CGRect, 15 | gradientFrame: CGRect, 16 | style: ShimmerViewStyle, 17 | effectBeginTime: CFTimeInterval 18 | ) { 19 | self.baseBounds = baseBounds 20 | self.elementFrame = elementFrame 21 | self.gradientFrame = gradientFrame 22 | self.style = style 23 | self.effectBeginTime = effectBeginTime 24 | } 25 | 26 | var effectDiameter: CGFloat { 27 | return baseBounds.diagonalDistance 28 | } 29 | 30 | var effectWidth: CGFloat { 31 | switch style.effectSpan { 32 | case .ratio(let ratio): 33 | return effectDiameter*ratio 34 | case .points(let points): 35 | return points 36 | } 37 | } 38 | 39 | var interpolatedColors: [Any] { 40 | let baseColor = style.baseColor 41 | let highlightColor = style.highlightColor 42 | let numberOfSteps = 30 43 | let baseColors: [UIColor] = [baseColor, highlightColor, baseColor] 44 | var colors: [UIColor] = [] 45 | for i in 0.. CGPoint { 104 | // convert coordinate space from sync target view to gradient layer 105 | let pointOnScope = baseBounds.mid.add(vector: vectorFromViewCenter) 106 | let scopeToElement = baseBounds.origin.vector(to: elementFrame.origin) 107 | let pointOnElement = pointOnScope.subtract(vector: scopeToElement) 108 | let elementToGradient = CGPoint.zero.vector(to: gradientFrame.origin) 109 | let converted = pointOnElement.subtract(vector: elementToGradient) 110 | 111 | // convert point on gradient layer to ratio 112 | if gradientFrame.width == 0 { 113 | return .zero 114 | } else { 115 | return CGPoint(x: converted.x / gradientFrame.width, y: converted.y / gradientFrame.width) 116 | } 117 | } 118 | 119 | var startPointAnimationFromValue: CGPoint { 120 | calculateTargetPoint(with: vectorFromViewCenterToStartPointFrom) 121 | } 122 | 123 | var startPointAnimationToValue: CGPoint { 124 | return calculateTargetPoint(with: vectorFromViewCenterToStartPointTo) 125 | } 126 | 127 | var endPointAnimationFromValue: CGPoint { 128 | return calculateTargetPoint(with: vectorFromViewCenterToEndPointFrom) 129 | } 130 | 131 | var endPointAnimationToValue: CGPoint { 132 | calculateTargetPoint(with: vectorFromViewCenterToEndPointTo) 133 | } 134 | 135 | var startPointAnimation: CABasicAnimation { 136 | let animation = CABasicAnimation(keyPath: "startPoint") 137 | animation.fromValue = startPointAnimationFromValue 138 | animation.toValue = startPointAnimationToValue 139 | animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) 140 | 141 | return animation 142 | } 143 | 144 | var endPointAnimation: CABasicAnimation { 145 | let animation = CABasicAnimation(keyPath: "endPoint") 146 | animation.fromValue = endPointAnimationFromValue 147 | animation.toValue = endPointAnimationToValue 148 | animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) 149 | 150 | return animation 151 | } 152 | 153 | var gradientLayerAnimation: CAAnimationGroup { 154 | let groupAnimation = CAAnimationGroup() 155 | groupAnimation.animations = [startPointAnimation, endPointAnimation] 156 | groupAnimation.duration = style.duration 157 | groupAnimation.fillMode = .both 158 | 159 | let animation = CAAnimationGroup() 160 | animation.animations = [groupAnimation] 161 | animation.duration = style.duration + style.interval 162 | animation.repeatCount = .infinity 163 | animation.fillMode = .both 164 | animation.isRemovedOnCompletion = false 165 | animation.beginTime = effectBeginTime 166 | return animation 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/Common/ShimmerCoreView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | internal class ShimmerCoreView: UIView { 4 | static let animationKey = "ShimmerEffect" 5 | 6 | private(set) var gradientLayer: CAGradientLayer = { 7 | let layer = CAGradientLayer() 8 | return layer 9 | }() 10 | 11 | private(set) var isAnimating: Bool = false 12 | 13 | private(set) var baseBounds: CGRect = .zero 14 | private(set) var elementFrame: CGRect = .zero 15 | var gradientFrame: CGRect { 16 | gradientLayer.frame 17 | } 18 | private(set) var style: ShimmerViewStyle = .default 19 | private(set) var effectBeginTime: CFTimeInterval = 0 20 | 21 | override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | setup() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | setup() 29 | } 30 | 31 | private func setup() { 32 | layer.masksToBounds = true 33 | layer.addSublayer(gradientLayer) 34 | scaleGradientLayerToAspectFill() 35 | } 36 | 37 | func startAnimating() { 38 | isAnimating = true 39 | 40 | setupAnimation() 41 | } 42 | 43 | private func setupAnimation() { 44 | gradientLayer.removeAnimation(forKey: ShimmerCoreView.animationKey) 45 | let animator = ShimmerCoreView.Animator( 46 | baseBounds: baseBounds, 47 | elementFrame: elementFrame, 48 | gradientFrame: gradientFrame, 49 | style: style, 50 | effectBeginTime: effectBeginTime 51 | ) 52 | gradientLayer.colors = animator.interpolatedColors 53 | gradientLayer.add(animator.gradientLayerAnimation, forKey: ShimmerCoreView.animationKey) 54 | } 55 | 56 | func stopAnimating() { 57 | isAnimating = false 58 | gradientLayer.removeAnimation(forKey: ShimmerCoreView.animationKey) 59 | gradientLayer.colors = nil 60 | } 61 | 62 | func update( 63 | baseBounds: CGRect? = nil, 64 | elementFrame: CGRect? = nil, 65 | style: ShimmerViewStyle? = nil, 66 | effectBeginTime: CFTimeInterval? = nil 67 | ) { 68 | if let baseBounds = baseBounds { 69 | self.baseBounds = baseBounds 70 | } 71 | 72 | if let elementFrame = elementFrame { 73 | self.elementFrame = elementFrame 74 | } 75 | 76 | if let style = style { 77 | self.style = style 78 | } 79 | 80 | if let effectBeginTime = effectBeginTime { 81 | self.effectBeginTime = effectBeginTime 82 | } 83 | 84 | if isAnimating { 85 | setupAnimation() 86 | } 87 | } 88 | 89 | override func layoutSubviews() { 90 | super.layoutSubviews() 91 | 92 | scaleGradientLayerToAspectFill() 93 | 94 | if isAnimating { 95 | startAnimating() 96 | } 97 | } 98 | 99 | private func scaleGradientLayerToAspectFill() { 100 | if frame.width > frame.height { 101 | gradientLayer.frame.origin = CGPoint(x: 0, y: -(frame.width - frame.height)/2) 102 | gradientLayer.frame.size = CGSize(width: frame.width, height: frame.width) 103 | } else if frame.height > frame.width { 104 | gradientLayer.frame.origin = CGPoint(x: -(frame.height - frame.width)/2, y: 0) 105 | gradientLayer.frame.size = CGSize(width: frame.height, height: frame.height) 106 | } else { 107 | gradientLayer.frame = bounds 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Common/ShimmerViewStyle.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension ShimmerView { 4 | 5 | /// Effect Span 6 | /// Effect Span is the length of the gradation of shimmering effect. 7 | /// If it is specified in ratio, the effect span in points will be calculated as SyncTargetView.frame.diagonalDistance * ratio. 8 | enum EffectSpan: Equatable { 9 | case ratio(CGFloat) 10 | case points(CGFloat) 11 | } 12 | } 13 | 14 | public struct ShimmerViewStyle: Equatable { 15 | 16 | /// The base color of the skelton view/shimmering effect. 17 | public var baseColor: UIColor 18 | 19 | /// The highlight color of the shimmering effect. 20 | public var highlightColor: UIColor 21 | 22 | /// The duration of the shimmering effect. 23 | public var duration: CFTimeInterval 24 | 25 | /// The interval of the shimmering effect. The length of one repetition of shimmering effect will be `duration` + `interval`. 26 | public var interval: CFTimeInterval 27 | 28 | /// The length of the gradation of shimmering effect. 29 | public var effectSpan: ShimmerView.EffectSpan 30 | 31 | /// The tilt angle of the effect. Please specify using radian. 32 | public var effectAngle: CGFloat 33 | 34 | public init(baseColor: UIColor, highlightColor: UIColor, duration: CFTimeInterval, interval: CFTimeInterval, effectSpan: ShimmerView.EffectSpan, effectAngle: CGFloat) { 35 | self.baseColor = baseColor 36 | self.highlightColor = highlightColor 37 | self.duration = duration 38 | self.interval = interval 39 | self.effectSpan = effectSpan 40 | self.effectAngle = effectAngle 41 | } 42 | } 43 | 44 | public extension ShimmerViewStyle { 45 | static let `default` = ShimmerViewStyle(baseColor: UIColor(red: 239/255, green: 239/255, blue: 239/255, alpha: 1.0), highlightColor: UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0), duration: 1.2, interval: 0.4, effectSpan: .points(120), effectAngle: 0 * CGFloat.pi) 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Extensions/CGPoint+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | internal extension CGPoint { 4 | func add(vector: CGVector) -> CGPoint { 5 | CGPoint(x: x+vector.dx, y: y+vector.dy) 6 | } 7 | 8 | func subtract(vector: CGVector) -> CGPoint { 9 | CGPoint(x: x-vector.dx, y: y-vector.dy) 10 | } 11 | 12 | func vector(to: CGPoint) -> CGVector { 13 | CGVector(dx: to.x - x, dy: to.y - y) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Extensions/CGRect+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | internal extension CGRect { 4 | var mid: CGPoint { 5 | CGPoint(x: midX, y: midY) 6 | } 7 | 8 | var diagonalDistance: CGFloat { 9 | sqrt(width*width+height*height) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Extensions/UIColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIColor { 4 | var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { 5 | var red: CGFloat = 0.0 6 | var green: CGFloat = 0.0 7 | var blue: CGFloat = 0.0 8 | var alpha: CGFloat = 0.0 9 | getRed(&red, green: &green, blue: &blue, alpha: &alpha) 10 | return (red: red, green: green, blue: blue, alpha: alpha) 11 | } 12 | 13 | func interpolate(with secondColor: UIColor, degree: CGFloat) -> UIColor { 14 | let degree = min(1.0, max(0.0, degree)) 15 | let first = components 16 | let second = secondColor.components 17 | 18 | let red = (1-degree)*first.red + degree*second.red 19 | let green = (1-degree)*first.green + degree*second.green 20 | let blue = (1-degree)*first.blue + degree*second.blue 21 | let alpha = (1-degree)*first.alpha + degree*second.alpha 22 | 23 | return UIColor(red: red, green: green, blue: blue, alpha: alpha) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Extensions/UIResponder+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | internal extension UIResponder { 4 | var nearestShimmerSyncTarget: (ShimmerSyncTarget & UIResponder)? { 5 | var current: UIResponder? = self.next 6 | while current != nil { 7 | if let syncTarget = current as? (ShimmerSyncTarget & UIResponder) { 8 | return syncTarget 9 | } else { 10 | current = current?.next 11 | } 12 | } 13 | return nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/SwiftUI/ShimmerElement.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | @available(iOS 14.0, *) 5 | public struct ShimmerElement: View { 6 | @Environment(\.shimmerGeometry) 7 | private var geometry 8 | 9 | @State 10 | private var baseBounds: CGRect = .zero 11 | 12 | @State 13 | private var elementFrame: CGRect = .zero 14 | 15 | public var width: CGFloat? 16 | 17 | public var height: CGFloat? 18 | 19 | public init(width: CGFloat? = nil, height: CGFloat? = nil) { 20 | self.width = width 21 | self.height = height 22 | } 23 | 24 | public var body: some View { 25 | ShimmerViewWrapper( 26 | baseBounds: $baseBounds, 27 | elementFrame: $elementFrame 28 | ) 29 | .frame(width: width, height: height) 30 | .anchorPreference( 31 | key: FramePreferenceKey.self, 32 | value: .bounds, 33 | transform: { geometry?[$0] } 34 | ) 35 | .onPreferenceChange(FramePreferenceKey.self) { frame in 36 | guard let frame = frame, let geometry = geometry else { 37 | return 38 | } 39 | baseBounds = CGRect(origin: .zero, size: geometry.size) 40 | elementFrame = frame 41 | } 42 | } 43 | } 44 | 45 | @available(iOS 14.0, *) 46 | private struct FramePreferenceKey: PreferenceKey { 47 | typealias Value = CGRect? 48 | static var defaultValue: Value = nil 49 | static func reduce(value: inout Value, nextValue: () -> Value) {} 50 | } 51 | 52 | @available(iOS 14.0, *) 53 | struct ShimmerViewWrapper: UIViewRepresentable { 54 | 55 | @Binding 56 | var baseBounds: CGRect 57 | 58 | @Binding 59 | var elementFrame: CGRect 60 | 61 | @EnvironmentObject 62 | var state: ShimmerState 63 | 64 | func makeCoordinator() -> Coordinator { 65 | Coordinator() 66 | } 67 | 68 | func makeUIView(context: Context) -> ShimmerCoreView { 69 | ShimmerCoreView(frame: .zero) 70 | } 71 | 72 | func updateUIView(_ uiView: ShimmerCoreView, context: Context) { 73 | uiView.update( 74 | baseBounds: baseBounds, 75 | elementFrame: elementFrame, 76 | style: state.style, 77 | effectBeginTime: state.effectBeginTime 78 | ) 79 | 80 | if uiView.isAnimating != state.isAnimating { 81 | if state.isAnimating { 82 | uiView.startAnimating() 83 | } else { 84 | uiView.stopAnimating() 85 | } 86 | } 87 | } 88 | 89 | class Coordinator { 90 | init() {} 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/SwiftUI/ShimmerScope.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @available(iOS 14.0, *) 4 | public struct ShimmerScope: View { 5 | @StateObject private var state = ShimmerState() 6 | @Binding private var isAnimating: Bool 7 | private let style: ShimmerViewStyle 8 | private let content: () -> Content 9 | 10 | public init( 11 | style: ShimmerViewStyle = .default, 12 | isAnimating: Binding, 13 | @ViewBuilder content: @escaping () -> Content 14 | ) { 15 | self.style = style 16 | _isAnimating = isAnimating 17 | self.content = content 18 | } 19 | 20 | public var body: some View { 21 | GeometryReader { proxy in 22 | content() 23 | .environment(\.shimmerGeometry, proxy) 24 | .environmentObject(state) 25 | } 26 | .onAppear { 27 | state.style = style 28 | state.setAnimating(isAnimating) 29 | } 30 | .onChange(of: isAnimating) { isAnimating in 31 | state.setAnimating(isAnimating) 32 | } 33 | } 34 | } 35 | 36 | @available(iOS 14.0, *) 37 | internal class ShimmerState: ObservableObject { 38 | @Published var style: ShimmerViewStyle = .default 39 | @Published var isAnimating: Bool = false 40 | @Published var effectBeginTime: CFTimeInterval = 0 41 | 42 | func setAnimating(_ isAnimating: Bool) { 43 | if self.isAnimating != isAnimating && isAnimating { 44 | effectBeginTime = CACurrentMediaTime() 45 | } 46 | self.isAnimating = isAnimating 47 | } 48 | } 49 | 50 | @available(iOS 14.0, *) 51 | internal extension EnvironmentValues { 52 | var shimmerGeometry: GeometryProxy? { 53 | get { 54 | self[ShimmerGeometryKey.self] 55 | } 56 | set { 57 | self[ShimmerGeometryKey.self] = newValue 58 | } 59 | } 60 | } 61 | 62 | @available(iOS 14.0, *) 63 | private struct ShimmerGeometryKey: EnvironmentKey { 64 | static var defaultValue: GeometryProxy? = nil 65 | } 66 | -------------------------------------------------------------------------------- /Sources/UIKit/ShimmerReplicatorView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// ShimmerReplicatorView 4 | /// This class replicates the specified `ShimmerReplicatorViewCell` inside its frame in horizontal and vertical direction. 5 | /// This class make it easy to create list type loading indicator very easily. 6 | public class ShimmerReplicatorView: UIView { 7 | 8 | public enum ItemSize { 9 | case fixedSize(CGSize) 10 | case fixedHeight(CGFloat) 11 | } 12 | 13 | public enum EdgeMode { 14 | /// Replicate cells within the edge 15 | /* 16 | |----------------------------| 17 | | |--------| |--------| | 18 | | | cell | | cell | | 19 | | |--------| |--------| | 20 | |----------------------------| 21 | */ 22 | case within 23 | 24 | /// Replicate cells beyond the edge 25 | /* 26 | |----------------------------| 27 | | |--------| |--------| |----|---| 28 | | | cell | | cell | | | | 29 | | |--------| |--------| |----|---| 30 | |----------------------------| 31 | */ 32 | case beyond 33 | } 34 | 35 | public override class var layerClass: AnyClass { 36 | return CAReplicatorLayer.self 37 | } 38 | 39 | private var replicatorLayer: CAReplicatorLayer { 40 | return layer as! CAReplicatorLayer 41 | } 42 | 43 | private let stackView: UIStackView = { 44 | let view = UIStackView() 45 | view.axis = .horizontal 46 | view.alignment = .leading 47 | view.distribution = .equalSpacing 48 | view.translatesAutoresizingMaskIntoConstraints = false 49 | return view 50 | }() 51 | 52 | private(set) var isAnimating: Bool = false 53 | 54 | public private(set) var itemSize: ItemSize 55 | public private(set) var interitemSpacing: CGFloat 56 | public private(set) var lineSpacing: CGFloat 57 | public private(set) var inset: UIEdgeInsets 58 | public private(set) var horizontalEdgeMode: EdgeMode 59 | public private(set) var verticalEdgeMode: EdgeMode 60 | private let cellProvider: () -> ShimmerReplicatorViewCell 61 | 62 | private var cellSizeConstraints = [UIView: [NSLayoutConstraint]]() 63 | 64 | /// Initializer 65 | /// - Parameters: 66 | /// - itemSize: The size of each cells. 67 | /// - interitemSpacing: The spacing between items in a same row. 68 | /// - lineSpacing: The spacing between rows. 69 | /// - inset: Inset for the contents. 70 | /// - horizontalEdgeMode: The edge mode for the horizontal direction. 71 | /// - verticalEdgeMode: The edge mode for the vertical direction. 72 | /// - cellProvider: A closure to create an instance of `ShimmerReplicatorViewCell`. 73 | public init(itemSize: ItemSize, interitemSpacing: CGFloat = 0, lineSpacing: CGFloat = 0, inset: UIEdgeInsets = .zero, horizontalEdgeMode: EdgeMode = .beyond, verticalEdgeMode: EdgeMode = .beyond, cellProvider: @escaping () -> ShimmerReplicatorViewCell) { 74 | self.itemSize = itemSize 75 | self.interitemSpacing = interitemSpacing 76 | self.lineSpacing = lineSpacing 77 | self.inset = inset 78 | self.horizontalEdgeMode = horizontalEdgeMode 79 | self.verticalEdgeMode = verticalEdgeMode 80 | self.cellProvider = cellProvider 81 | 82 | super.init(frame: .zero) 83 | 84 | addSubview(stackView) 85 | stackView.spacing = interitemSpacing 86 | NSLayoutConstraint.activate([ 87 | stackView.topAnchor.constraint(equalTo: topAnchor, constant: inset.top), 88 | stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: inset.left) 89 | ]) 90 | 91 | if horizontalEdgeMode == .within { 92 | stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -inset.right).isActive = true 93 | stackView.distribution = .equalSpacing 94 | } 95 | } 96 | 97 | required init?(coder: NSCoder) { 98 | fatalError("init(coder:) has not been implemented") 99 | } 100 | 101 | public func set(itemSize: ItemSize? = nil, interitemSpacing: CGFloat? = nil, lineSpacing: CGFloat? = nil, inset: UIEdgeInsets? = nil) { 102 | if let itemSize = itemSize { 103 | self.itemSize = itemSize 104 | } 105 | 106 | if let interitemSpacing = interitemSpacing { 107 | self.interitemSpacing = interitemSpacing 108 | } 109 | 110 | if let lineSpacing = lineSpacing { 111 | self.lineSpacing = lineSpacing 112 | } 113 | 114 | if let inset = inset { 115 | self.inset = inset 116 | } 117 | 118 | setNeedsLayout() 119 | } 120 | 121 | public override func layoutSubviews() { 122 | super.layoutSubviews() 123 | 124 | // calculate item size 125 | var itemSize: CGSize = .zero 126 | switch self.itemSize { 127 | case .fixedSize(let size): 128 | itemSize = size 129 | case .fixedHeight(let height): 130 | itemSize.height = height 131 | itemSize.width = frame.inset(by: inset).width 132 | } 133 | 134 | let frameSize = frame.inset(by: inset) 135 | var horizontalNumber = floor((frameSize.width + interitemSpacing)/(itemSize.width + interitemSpacing)) 136 | if (horizontalNumber-1) * (itemSize.width + interitemSpacing) + itemSize.width < frameSize.width && horizontalEdgeMode == .beyond { 137 | horizontalNumber += 1 138 | } 139 | 140 | var verticalNumber = floor((frameSize.height + lineSpacing)/(itemSize.height + lineSpacing)) 141 | if (verticalNumber-1) * (itemSize.height + lineSpacing) + itemSize.height < frameSize.height && verticalEdgeMode == .beyond { 142 | verticalNumber += 1 143 | } 144 | 145 | replicatorLayer.instanceCount = Int(verticalNumber) 146 | replicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, itemSize.height + lineSpacing, 0) 147 | 148 | // Remove the previous cell constraints 149 | cellSizeConstraints.forEach { cell, constraints in 150 | constraints.forEach { constraint in 151 | constraint.isActive = false 152 | cell.removeConstraint(constraint) 153 | } 154 | } 155 | cellSizeConstraints.removeAll() 156 | 157 | let currentCount = stackView.arrangedSubviews.count 158 | // reset the cell 159 | for i in 0.. Int(horizontalNumber) { 185 | for _ in 0..<(currentCount-Int(horizontalNumber)) { 186 | if let lastView = stackView.arrangedSubviews.last { 187 | lastView.removeFromSuperview() 188 | } 189 | } 190 | } 191 | } 192 | 193 | public func startAnimating() { 194 | isAnimating = true 195 | stackView.arrangedSubviews.compactMap { $0 as? ShimmerReplicatorViewCell }.forEach { 196 | $0.startAnimating() 197 | } 198 | } 199 | 200 | public func stopAnimating() { 201 | isAnimating = false 202 | stackView.arrangedSubviews.compactMap { $0 as? ShimmerReplicatorViewCell }.forEach { 203 | $0.stopAnimating() 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Sources/UIKit/ShimmerReplicatorViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// ShimmerReplicatorViewCell 4 | /// ShimmerReplicatorView's each cell comforms to this protocol. The replicator view will replicate the cell as needed by the cell provider specified in its initializer. 5 | /// Also, `ShimmerReplicatorView` starts all the cells' animation when its `startAnimating` is called. 6 | public protocol ShimmerReplicatorViewCell: UIView { 7 | func startAnimating() 8 | func stopAnimating() 9 | } 10 | -------------------------------------------------------------------------------- /Sources/UIKit/ShimmerSyncTarget.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Shimmer Sync Target 4 | /// The shimmering effect would be effectively displayed when all the subviews’s effect in the screen is synced and animated together. 5 | /// Shimmer view finds its nearest ShimmerSyncTarget in its responder chain and adjusts its shimmering effect according to the sync target's configuration. 6 | public protocol ShimmerSyncTarget: UIResponder { 7 | 8 | /// The style of the shimmering effect. 9 | var style: ShimmerViewStyle { get } 10 | 11 | /// This effect begin time will be used in `ShimmerView`'s `CAAnimation` begin time and time offset. 12 | /// Please specify `CACurrentMediaTime()` when the effect should be started, for example, when the object is created. 13 | var effectBeginTime: CFTimeInterval { get } 14 | 15 | /// All the child `ShimmerView` under ShimmerSyncTarget will calculate its effect for this sync target view's frame. 16 | var syncTargetView: UIView { get } 17 | } 18 | 19 | public extension ShimmerSyncTarget where Self: UIView { 20 | var syncTargetView: UIView { 21 | return self 22 | } 23 | } 24 | 25 | public extension ShimmerSyncTarget where Self: UIViewController { 26 | var syncTargetView: UIView { 27 | return view 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/UIKit/ShimmerView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | open class ShimmerView: UIView, ShimmerSyncTarget { 5 | internal var coreView: ShimmerCoreView = { 6 | let view = ShimmerCoreView() 7 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 8 | return view 9 | }() 10 | 11 | public var style: ShimmerViewStyle = .default 12 | 13 | public var effectBeginTime: CFTimeInterval = 0 14 | 15 | internal var syncTarget: ShimmerSyncTarget { 16 | nearestShimmerSyncTarget ?? self 17 | } 18 | 19 | internal var baseBounds: CGRect { 20 | syncTarget.syncTargetView.bounds 21 | } 22 | 23 | internal var elementFrame: CGRect { 24 | convert(bounds, to: syncTarget.syncTargetView) 25 | } 26 | 27 | public override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | setup() 30 | } 31 | 32 | required public init?(coder: NSCoder) { 33 | super.init(coder: coder) 34 | setup() 35 | } 36 | 37 | private func setup() { 38 | coreView.frame = bounds 39 | addSubview(coreView) 40 | } 41 | 42 | public func startAnimating() { 43 | effectBeginTime = CACurrentMediaTime() 44 | 45 | coreView.update( 46 | baseBounds: baseBounds, 47 | elementFrame: elementFrame, 48 | style: style, 49 | effectBeginTime: syncTarget.effectBeginTime 50 | ) 51 | 52 | coreView.startAnimating() 53 | } 54 | 55 | public func stopAnimating() { 56 | coreView.stopAnimating() 57 | } 58 | 59 | public func apply(style: ShimmerViewStyle) { 60 | coreView.update(style: style) 61 | } 62 | 63 | override public func layoutSubviews() { 64 | super.layoutSubviews() 65 | 66 | coreView.update(baseBounds: baseBounds, elementFrame: elementFrame) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/ShimmerCoreViewAnimatorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import UIKit 3 | @testable import ShimmerView 4 | 5 | class ShimmerCoreViewAnimatorTests: XCTestCase { 6 | func testEffectRadius() { 7 | let style = ShimmerViewStyle( 8 | baseColor: .clear, 9 | highlightColor: .clear, 10 | duration: 10, 11 | interval: 10, 12 | effectSpan: .points(100), 13 | effectAngle: 0 14 | ) 15 | let baseBounds = CGRect(x: 0, y: 0, width: 300, height: 300) 16 | 17 | var animator = ShimmerCoreView.Animator( 18 | baseBounds: baseBounds, 19 | elementFrame: baseBounds, 20 | gradientFrame: baseBounds, 21 | style: style, 22 | effectBeginTime: 0 23 | ) 24 | 25 | XCTContext.runActivity(named: "Effect Angle = 0.0 pi") { _ in 26 | animator.style.effectAngle = 0.0 * CGFloat.pi 27 | XCTAssertEqual( 28 | animator.effectRadius, 29 | baseBounds.width / 2, 30 | accuracy: 0.001 31 | ) 32 | } 33 | 34 | XCTContext.runActivity(named: "Effect Angle = 0.25 pi") { _ in 35 | animator.style.effectAngle = 0.25 * CGFloat.pi 36 | XCTAssertEqual( 37 | animator.effectRadius, 38 | baseBounds.diagonalDistance/2, 39 | accuracy: 0.001 40 | ) 41 | } 42 | 43 | XCTContext.runActivity(named: "Effect Angle = 0.5 pi") { _ in 44 | animator.style.effectAngle = 0.5 * CGFloat.pi 45 | XCTAssertEqual( 46 | animator.effectRadius, 47 | baseBounds.height / 2, 48 | accuracy: 0.001 49 | ) 50 | } 51 | 52 | XCTContext.runActivity(named: "Effect Angle = 0.75 pi") { _ in 53 | animator.style.effectAngle = 0.75 * CGFloat.pi 54 | XCTAssertEqual( 55 | animator.effectRadius, 56 | baseBounds.diagonalDistance/2, 57 | accuracy: 0.001 58 | ) 59 | } 60 | } 61 | 62 | func testStartPoint() { 63 | let style = ShimmerViewStyle( 64 | baseColor: .clear, 65 | highlightColor: .clear, 66 | duration: 10, 67 | interval: 10, 68 | effectSpan: .points(100), 69 | effectAngle: 0 70 | ) 71 | let baseBounds = CGRect(x: 0, y: 0, width: 300, height: 300) 72 | let animator = ShimmerCoreView.Animator( 73 | baseBounds: baseBounds, 74 | elementFrame: baseBounds, 75 | gradientFrame: baseBounds, 76 | style: style, 77 | effectBeginTime: 0 78 | ) 79 | let denominator = max(baseBounds.width, baseBounds.height) 80 | let fromValue = animator.startPointAnimationFromValue 81 | XCTAssertEqual(fromValue.x, -100/denominator, accuracy: 0.01) 82 | XCTAssertEqual(fromValue.y, 150/denominator, accuracy: 0.01) 83 | 84 | let toValue = animator.startPointAnimationToValue 85 | XCTAssertEqual(toValue.x, 300/denominator, accuracy: 0.01) 86 | XCTAssertEqual(toValue.y, 150/denominator, accuracy: 0.01) 87 | } 88 | 89 | func testEndPoint() { 90 | let style = ShimmerViewStyle( 91 | baseColor: .clear, 92 | highlightColor: .clear, 93 | duration: 10, 94 | interval: 10, 95 | effectSpan: .points(100), 96 | effectAngle: 0 97 | ) 98 | let baseBounds = CGRect(x: 0, y: 0, width: 300, height: 300) 99 | let animator = ShimmerCoreView.Animator( 100 | baseBounds: baseBounds, 101 | elementFrame: baseBounds, 102 | gradientFrame: baseBounds, 103 | style: style, 104 | effectBeginTime: 0 105 | ) 106 | let denominator = max(baseBounds.width, baseBounds.height) 107 | let fromValue = animator.endPointAnimationFromValue 108 | XCTAssertEqual(fromValue.x, 0/denominator, accuracy: 0.01) 109 | XCTAssertEqual(fromValue.y, 150/denominator, accuracy: 0.01) 110 | 111 | let toValue = animator.endPointAnimationToValue 112 | XCTAssertEqual(toValue.x, 400/denominator, accuracy: 0.01) 113 | XCTAssertEqual(toValue.y, 150/denominator, accuracy: 0.01) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/ShimmerCoreViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ShimmerView 3 | 4 | class ShimmerCoreViewTests: XCTestCase { 5 | func testStartAnimating() { 6 | let view = ShimmerCoreView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 7 | 8 | XCTAssertFalse(view.isAnimating) 9 | XCTAssertNil(view.gradientLayer.colors) 10 | XCTAssertNil(view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey)) 11 | 12 | view.startAnimating() 13 | 14 | XCTAssertTrue(view.isAnimating) 15 | XCTAssertNotNil(view.gradientLayer.colors) 16 | XCTAssertNotNil(view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey)) 17 | 18 | sleep(1) 19 | 20 | view.startAnimating() 21 | XCTAssertTrue(view.isAnimating) 22 | XCTAssertNotNil(view.gradientLayer.colors) 23 | XCTAssertNotNil(view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey)) 24 | } 25 | 26 | func testStopAnimating() { 27 | let view = ShimmerCoreView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 28 | 29 | XCTAssertEqual(view.effectBeginTime, 0) 30 | XCTAssertFalse(view.isAnimating) 31 | XCTAssertNil(view.gradientLayer.colors) 32 | XCTAssertNil(view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey)) 33 | 34 | view.startAnimating() 35 | 36 | view.stopAnimating() 37 | XCTAssertFalse(view.isAnimating) 38 | XCTAssertNil(view.gradientLayer.colors) 39 | XCTAssertNil(view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey)) 40 | } 41 | 42 | func testUpdate() { 43 | let view = ShimmerCoreView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 44 | 45 | // ShimmerCoreView's default style is ShimmerViewStyle.default 46 | XCTAssertEqual(view.style, ShimmerViewStyle.default) 47 | XCTAssertNil(view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey)) 48 | 49 | view.startAnimating() 50 | XCTAssertTrue(view.isAnimating) 51 | XCTAssertNotNil(view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey)) 52 | 53 | let previousAnimation = view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey) 54 | let newStyle = ShimmerViewStyle( 55 | baseColor: .clear, 56 | highlightColor: .clear, 57 | duration: 10, 58 | interval: 10, 59 | effectSpan: .points(100), 60 | effectAngle: 0 61 | ) 62 | 63 | view.update(style: newStyle) 64 | 65 | // When the style is updated and the view is animating, the animation object should be updated. 66 | XCTAssertTrue(view.isAnimating) 67 | XCTAssertNotEqual(view.gradientLayer.animation(forKey: ShimmerCoreView.animationKey), previousAnimation) 68 | } 69 | 70 | func testLayoutSubviews() { 71 | let view = ShimmerCoreView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 72 | 73 | XCTAssertEqual(view.gradientLayer.frame.width, view.gradientLayer.frame.height) 74 | 75 | XCTContext.runActivity(named: "Height is bigger") { _ in 76 | view.frame.size = CGSize(width: 100, height: 300) 77 | view.layoutSubviews() 78 | XCTAssertEqual(view.gradientLayer.frame.origin, CGPoint(x: -100, y: 0)) 79 | XCTAssertEqual(view.gradientLayer.frame.width, view.gradientLayer.frame.height) 80 | } 81 | 82 | XCTContext.runActivity(named: "Width is bigger") { _ in 83 | view.frame.size = CGSize(width: 300, height: 100) 84 | view.layoutSubviews() 85 | XCTAssertEqual(view.gradientLayer.frame.origin, CGPoint(x: 0, y: -100)) 86 | XCTAssertEqual(view.gradientLayer.frame.width, view.gradientLayer.frame.height) 87 | } 88 | } 89 | 90 | func testGradientFrame() { 91 | let baseBounds = CGRect(x: 0, y: 0, width: 300, height: 300) 92 | let view = ShimmerCoreView(frame: baseBounds) 93 | view.layoutSubviews() 94 | 95 | XCTAssertEqual(view.gradientFrame, baseBounds) 96 | 97 | view.frame.size = CGSize(width: 100, height: 300) 98 | view.layoutSubviews() 99 | XCTAssertEqual(view.gradientFrame, CGRect(x: -100, y: 0, width: 300, height: 300)) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/ShimmerSyncTargetTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import UIKit 3 | @testable import ShimmerView 4 | 5 | class ShimmerSyncTargetTests: XCTestCase { 6 | func testSyncTarget() { 7 | class SyncTargetView: UIView, ShimmerSyncTarget { 8 | var style: ShimmerViewStyle = .default 9 | var effectBeginTime: CFTimeInterval = 0 10 | 11 | override init(frame: CGRect) { 12 | super.init(frame: frame) 13 | } 14 | 15 | required init?(coder: NSCoder) { 16 | fatalError("init(coder:) has not been implemented") 17 | } 18 | } 19 | 20 | XCTContext.runActivity(named: "There is no sync target in the UIReponder") { _ in 21 | let shimmerView = ShimmerView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 22 | let syncTargetView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 23 | syncTargetView.addSubview(shimmerView) 24 | 25 | XCTAssertEqual(shimmerView, shimmerView.syncTarget) 26 | } 27 | 28 | XCTContext.runActivity(named: "ShimmerView's parent view is ShimmerSyncTarget") { _ in 29 | let shimmerView = ShimmerView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 30 | let syncTargetView = SyncTargetView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 31 | syncTargetView.addSubview(shimmerView) 32 | 33 | XCTAssertEqual(syncTargetView, shimmerView.syncTarget) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/ShimmerViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import UIKit 3 | @testable import ShimmerView 4 | 5 | class ShimmerViewTests: XCTestCase { 6 | func testCoreViewFrame() { 7 | let baseBounds = CGRect(x: 0, y: 0, width: 300, height: 300) 8 | let view = ShimmerView(frame: baseBounds) 9 | 10 | XCTContext.runActivity(named: "When the shimmer view is initialized") { _ in 11 | XCTAssertEqual(view.bounds, view.coreView.frame) 12 | } 13 | 14 | view.frame.size = CGSize(width: 100, height: 100) 15 | 16 | XCTContext.runActivity(named: "When the size of shimmer view is changed") { _ in 17 | XCTAssertEqual(view.bounds, view.coreView.frame) 18 | } 19 | } 20 | 21 | func testBaseBoundsAndElementFrame() { 22 | let baseBounds = CGRect(x: 0, y: 0, width: 300, height: 300) 23 | let view = ShimmerView(frame: baseBounds) 24 | 25 | XCTContext.runActivity(named: "When the shimmer sync target is the view itself") { _ in 26 | XCTAssertEqual(view.baseBounds, view.bounds) 27 | XCTAssertEqual(view.elementFrame, view.bounds) 28 | } 29 | 30 | class SyncTargetView: UIView, ShimmerSyncTarget { 31 | var style: ShimmerViewStyle = .default 32 | var effectBeginTime: CFTimeInterval = 0 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | } 42 | 43 | let parentBounds = CGRect(x: 0, y: 0, width: 500, height: 500) 44 | let parentView = SyncTargetView(frame: parentBounds) 45 | parentView.addSubview(view) 46 | view.frame.origin = CGPoint(x: 100, y: 100) 47 | XCTContext.runActivity(named: "When the shimmer sync target is the parent view") { _ in 48 | XCTAssertEqual(view.baseBounds, parentBounds) 49 | XCTAssertEqual(view.elementFrame, CGRect(x: 100, y: 100, width: 300, height: 300)) 50 | } 51 | } 52 | 53 | func testStartAnimating() { 54 | let view = ShimmerView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 55 | 56 | XCTAssertEqual(view.effectBeginTime, 0) 57 | XCTAssertFalse(view.coreView.isAnimating) 58 | 59 | view.startAnimating() 60 | 61 | XCTAssertNotEqual(view.effectBeginTime, 0) 62 | XCTAssertTrue(view.coreView.isAnimating) 63 | 64 | sleep(1) 65 | 66 | let previousEffectBeginTime = view.effectBeginTime 67 | 68 | /// effectBeginTime should be updated everytime `startAnimating` is called. 69 | view.startAnimating() 70 | XCTAssertNotEqual(view.effectBeginTime, previousEffectBeginTime) 71 | XCTAssertTrue(view.coreView.isAnimating) 72 | } 73 | 74 | func testStopAnimating() { 75 | let view = ShimmerView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 76 | 77 | XCTAssertEqual(view.effectBeginTime, 0) 78 | XCTAssertFalse(view.coreView.isAnimating) 79 | 80 | view.startAnimating() 81 | XCTAssertTrue(view.coreView.isAnimating) 82 | 83 | view.stopAnimating() 84 | XCTAssertFalse(view.coreView.isAnimating) 85 | } 86 | 87 | func testApplyStyle() { 88 | let view = ShimmerView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 89 | XCTAssertEqual(view.coreView.style, .default) 90 | 91 | let newStyle = ShimmerViewStyle( 92 | baseColor: .clear, 93 | highlightColor: .clear, 94 | duration: 10, 95 | interval: 10, 96 | effectSpan: .points(100), 97 | effectAngle: 0 98 | ) 99 | view.apply(style: newStyle) 100 | XCTAssertEqual(view.coreView.style, newStyle) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /images/shimmer_view_example_0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mercari/ShimmerView/f34a268db28b49231ea236dafcd976507cef9dfd/images/shimmer_view_example_0.gif -------------------------------------------------------------------------------- /images/shimmer_view_list.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mercari/ShimmerView/f34a268db28b49231ea236dafcd976507cef9dfd/images/shimmer_view_list.gif -------------------------------------------------------------------------------- /images/shimmer_view_not_synced.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mercari/ShimmerView/f34a268db28b49231ea236dafcd976507cef9dfd/images/shimmer_view_not_synced.gif -------------------------------------------------------------------------------- /images/shimmer_view_swift_ui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mercari/ShimmerView/f34a268db28b49231ea236dafcd976507cef9dfd/images/shimmer_view_swift_ui.gif -------------------------------------------------------------------------------- /images/shimmer_view_synced.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mercari/ShimmerView/f34a268db28b49231ea236dafcd976507cef9dfd/images/shimmer_view_synced.gif --------------------------------------------------------------------------------