├── .gitignore ├── Icon.sketch ├── LICENSE ├── Pulsar.podspec ├── Pulsar.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Pulsar.xcscheme ├── Pulsar ├── Classes │ ├── AnimationDelegate.swift │ ├── ApplicationObserver.swift │ ├── CALayer+Extensions.swift │ ├── Pulse.swift │ └── PulseLayer.swift ├── Info.plist └── Pulsar.h ├── PulsarDemo ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── CircleButton.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x.png │ │ ├── Icon-83.5@2x.png │ │ ├── Icon-Notification.png │ │ ├── Icon-Notification@2x.png │ │ ├── Icon-Notification@3x.png │ │ ├── Icon-Small-40.png │ │ ├── Icon-Small-40@2x.png │ │ ├── Icon-Small-40@3x.png │ │ ├── Icon-Small.png │ │ ├── Icon-Small@2x.png │ │ └── Icon-Small@3x.png ├── Info.plist ├── RoundedRectButton.swift ├── StarButton.swift ├── UIButton+Theme.swift └── ViewController.swift ├── README.md ├── jumbotron.png └── screencast.gif /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 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 | .build/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots 71 | fastlane/test_output 72 | 73 | # End of https://www.gitignore.io/api/swift 74 | .DS_Store 75 | -------------------------------------------------------------------------------- /Icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/Icon.sketch -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Vincent Esche 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | 4. Redistributions of any form whatsoever must retain the following acknowledgment: "This product includes code by Vincent Esche." where would be replaced by the name of the specific source-code package being made use of. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /Pulsar.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "Pulsar" 4 | s.version = "2.0.5" 5 | s.summary = "A generic wrapper implementation for copy-on-write data structures written in Swift." 6 | 7 | s.description = <<-DESC 8 | Pulsar is a versatile solution for displaying pulse animations as known from Apple Maps. 9 | DESC 10 | 11 | s.homepage = "https://github.com/regexident/Pulsar" 12 | s.license = { :type => 'BSD-3', :file => 'LICENSE' } 13 | s.author = { "Vincent Esche" => "regexident@gmail.com" } 14 | s.source = { :git => "https://github.com/regexident/Pulsar.git", :tag => '2.0.5' } 15 | s.source_files = "Pulsar/Classes/*.{swift,h,m}" 16 | # s.public_header_files = "Pulsar/*.h" 17 | s.requires_arc = true 18 | s.ios.deployment_target = "8.0" 19 | # s.osx.deployment_target = "10.9" 20 | s.swift_versions = ['5.0'] 21 | 22 | end 23 | -------------------------------------------------------------------------------- /Pulsar.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A411CF341A84D3090053A6F8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CF331A84D3090053A6F8 /* AppDelegate.swift */; }; 11 | A411CF361A84D3090053A6F8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CF351A84D3090053A6F8 /* ViewController.swift */; }; 12 | A411CF391A84D3090053A6F8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A411CF371A84D3090053A6F8 /* Main.storyboard */; }; 13 | A411CF3B1A84D3090053A6F8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A411CF3A1A84D3090053A6F8 /* Images.xcassets */; }; 14 | A411CF3E1A84D3090053A6F8 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = A411CF3C1A84D3090053A6F8 /* LaunchScreen.xib */; }; 15 | A411CF761A8501190053A6F8 /* Pulsar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A411CF5F1A8501180053A6F8 /* Pulsar.framework */; }; 16 | A411CF771A8501190053A6F8 /* Pulsar.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A411CF5F1A8501180053A6F8 /* Pulsar.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | A411CF7F1A85015B0053A6F8 /* CALayer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CF561A84D3700053A6F8 /* CALayer+Extensions.swift */; }; 18 | A411CF811A8511010053A6F8 /* RoundedRectButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CF801A8511010053A6F8 /* RoundedRectButton.swift */; }; 19 | A411CF831A85111C0053A6F8 /* CircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CF821A85111C0053A6F8 /* CircleButton.swift */; }; 20 | A411CF851A85112F0053A6F8 /* StarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CF841A85112F0053A6F8 /* StarButton.swift */; }; 21 | A411CF871A8511510053A6F8 /* UIButton+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CF861A8511510053A6F8 /* UIButton+Theme.swift */; }; 22 | CAD18152222E70C0008CFFB4 /* PulseLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD18151222E70C0008CFFB4 /* PulseLayer.swift */; }; 23 | CAD18154222E70E2008CFFB4 /* Pulse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD18153222E70E2008CFFB4 /* Pulse.swift */; }; 24 | CAD18156222E70F6008CFFB4 /* AnimationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD18155222E70F6008CFFB4 /* AnimationDelegate.swift */; }; 25 | CAD18158222E735D008CFFB4 /* ApplicationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD18157222E735D008CFFB4 /* ApplicationObserver.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | A411CF741A8501190053A6F8 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = A411CF061A84D2EA0053A6F8 /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = A411CF5E1A8501180053A6F8; 34 | remoteInfo = Pulsar; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXCopyFilesBuildPhase section */ 39 | A411CF7B1A8501190053A6F8 /* Embed Frameworks */ = { 40 | isa = PBXCopyFilesBuildPhase; 41 | buildActionMask = 2147483647; 42 | dstPath = ""; 43 | dstSubfolderSpec = 10; 44 | files = ( 45 | A411CF771A8501190053A6F8 /* Pulsar.framework in Embed Frameworks */, 46 | ); 47 | name = "Embed Frameworks"; 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXCopyFilesBuildPhase section */ 51 | 52 | /* Begin PBXFileReference section */ 53 | A411CF131A84D2EA0053A6F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | A411CF141A84D2EA0053A6F8 /* Pulsar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pulsar.h; sourceTree = ""; }; 55 | A411CF2F1A84D3090053A6F8 /* PulsarDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PulsarDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | A411CF321A84D3090053A6F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 57 | A411CF331A84D3090053A6F8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 58 | A411CF351A84D3090053A6F8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 59 | A411CF381A84D3090053A6F8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 60 | A411CF3A1A84D3090053A6F8 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 61 | A411CF3D1A84D3090053A6F8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 62 | A411CF561A84D3700053A6F8 /* CALayer+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CALayer+Extensions.swift"; sourceTree = ""; }; 63 | A411CF5F1A8501180053A6F8 /* Pulsar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pulsar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | A411CF801A8511010053A6F8 /* RoundedRectButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedRectButton.swift; sourceTree = ""; }; 65 | A411CF821A85111C0053A6F8 /* CircleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleButton.swift; sourceTree = ""; }; 66 | A411CF841A85112F0053A6F8 /* StarButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StarButton.swift; sourceTree = ""; }; 67 | A411CF861A8511510053A6F8 /* UIButton+Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Theme.swift"; sourceTree = ""; }; 68 | CAD18151222E70C0008CFFB4 /* PulseLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulseLayer.swift; sourceTree = ""; }; 69 | CAD18153222E70E2008CFFB4 /* Pulse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pulse.swift; sourceTree = ""; }; 70 | CAD18155222E70F6008CFFB4 /* AnimationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationDelegate.swift; sourceTree = ""; }; 71 | CAD18157222E735D008CFFB4 /* ApplicationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationObserver.swift; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | A411CF2C1A84D3090053A6F8 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | A411CF761A8501190053A6F8 /* Pulsar.framework in Frameworks */, 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | A411CF5B1A8501180053A6F8 /* Frameworks */ = { 84 | isa = PBXFrameworksBuildPhase; 85 | buildActionMask = 2147483647; 86 | files = ( 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | /* End PBXFrameworksBuildPhase section */ 91 | 92 | /* Begin PBXGroup section */ 93 | A411CF051A84D2EA0053A6F8 = { 94 | isa = PBXGroup; 95 | children = ( 96 | A411CF111A84D2EA0053A6F8 /* Pulsar */, 97 | A411CF301A84D3090053A6F8 /* PulsarDemo */, 98 | A411CF101A84D2EA0053A6F8 /* Products */, 99 | ); 100 | sourceTree = ""; 101 | }; 102 | A411CF101A84D2EA0053A6F8 /* Products */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | A411CF2F1A84D3090053A6F8 /* PulsarDemo.app */, 106 | A411CF5F1A8501180053A6F8 /* Pulsar.framework */, 107 | ); 108 | name = Products; 109 | sourceTree = ""; 110 | }; 111 | A411CF111A84D2EA0053A6F8 /* Pulsar */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | A411CF531A84D3700053A6F8 /* Classes */, 115 | A411CF141A84D2EA0053A6F8 /* Pulsar.h */, 116 | A411CF121A84D2EA0053A6F8 /* Supporting Files */, 117 | ); 118 | path = Pulsar; 119 | sourceTree = ""; 120 | }; 121 | A411CF121A84D2EA0053A6F8 /* Supporting Files */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | A411CF131A84D2EA0053A6F8 /* Info.plist */, 125 | ); 126 | name = "Supporting Files"; 127 | sourceTree = ""; 128 | }; 129 | A411CF301A84D3090053A6F8 /* PulsarDemo */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | A411CF331A84D3090053A6F8 /* AppDelegate.swift */, 133 | A411CF351A84D3090053A6F8 /* ViewController.swift */, 134 | A411CF801A8511010053A6F8 /* RoundedRectButton.swift */, 135 | A411CF821A85111C0053A6F8 /* CircleButton.swift */, 136 | A411CF841A85112F0053A6F8 /* StarButton.swift */, 137 | A411CF861A8511510053A6F8 /* UIButton+Theme.swift */, 138 | A411CF371A84D3090053A6F8 /* Main.storyboard */, 139 | A411CF3A1A84D3090053A6F8 /* Images.xcassets */, 140 | A411CF3C1A84D3090053A6F8 /* LaunchScreen.xib */, 141 | A411CF311A84D3090053A6F8 /* Supporting Files */, 142 | ); 143 | path = PulsarDemo; 144 | sourceTree = ""; 145 | }; 146 | A411CF311A84D3090053A6F8 /* Supporting Files */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | A411CF321A84D3090053A6F8 /* Info.plist */, 150 | ); 151 | name = "Supporting Files"; 152 | sourceTree = ""; 153 | }; 154 | A411CF531A84D3700053A6F8 /* Classes */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | CAD18151222E70C0008CFFB4 /* PulseLayer.swift */, 158 | CAD18153222E70E2008CFFB4 /* Pulse.swift */, 159 | CAD18155222E70F6008CFFB4 /* AnimationDelegate.swift */, 160 | CAD18157222E735D008CFFB4 /* ApplicationObserver.swift */, 161 | A411CF561A84D3700053A6F8 /* CALayer+Extensions.swift */, 162 | ); 163 | path = Classes; 164 | sourceTree = ""; 165 | }; 166 | /* End PBXGroup section */ 167 | 168 | /* Begin PBXHeadersBuildPhase section */ 169 | A411CF5C1A8501180053A6F8 /* Headers */ = { 170 | isa = PBXHeadersBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXHeadersBuildPhase section */ 177 | 178 | /* Begin PBXNativeTarget section */ 179 | A411CF2E1A84D3090053A6F8 /* PulsarDemo */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = A411CF4B1A84D3090053A6F8 /* Build configuration list for PBXNativeTarget "PulsarDemo" */; 182 | buildPhases = ( 183 | A411CF2B1A84D3090053A6F8 /* Sources */, 184 | A411CF2C1A84D3090053A6F8 /* Frameworks */, 185 | A411CF2D1A84D3090053A6F8 /* Resources */, 186 | A411CF7B1A8501190053A6F8 /* Embed Frameworks */, 187 | ); 188 | buildRules = ( 189 | ); 190 | dependencies = ( 191 | A411CF751A8501190053A6F8 /* PBXTargetDependency */, 192 | ); 193 | name = PulsarDemo; 194 | productName = PulsarDemo; 195 | productReference = A411CF2F1A84D3090053A6F8 /* PulsarDemo.app */; 196 | productType = "com.apple.product-type.application"; 197 | }; 198 | A411CF5E1A8501180053A6F8 /* Pulsar */ = { 199 | isa = PBXNativeTarget; 200 | buildConfigurationList = A411CF781A8501190053A6F8 /* Build configuration list for PBXNativeTarget "Pulsar" */; 201 | buildPhases = ( 202 | A411CF5A1A8501180053A6F8 /* Sources */, 203 | A411CF5B1A8501180053A6F8 /* Frameworks */, 204 | A411CF5C1A8501180053A6F8 /* Headers */, 205 | A411CF5D1A8501180053A6F8 /* Resources */, 206 | ); 207 | buildRules = ( 208 | ); 209 | dependencies = ( 210 | ); 211 | name = Pulsar; 212 | productName = Pulsar; 213 | productReference = A411CF5F1A8501180053A6F8 /* Pulsar.framework */; 214 | productType = "com.apple.product-type.framework"; 215 | }; 216 | /* End PBXNativeTarget section */ 217 | 218 | /* Begin PBXProject section */ 219 | A411CF061A84D2EA0053A6F8 /* Project object */ = { 220 | isa = PBXProject; 221 | attributes = { 222 | LastSwiftUpdateCheck = 0700; 223 | LastUpgradeCheck = 1130; 224 | ORGANIZATIONNAME = Regexident; 225 | TargetAttributes = { 226 | A411CF2E1A84D3090053A6F8 = { 227 | CreatedOnToolsVersion = 6.2; 228 | DevelopmentTeam = C4K7WZKHXH; 229 | LastSwiftMigration = 1130; 230 | }; 231 | A411CF5E1A8501180053A6F8 = { 232 | CreatedOnToolsVersion = 6.2; 233 | DevelopmentTeam = RHSV5Y8MLD; 234 | LastSwiftMigration = 1130; 235 | }; 236 | }; 237 | }; 238 | buildConfigurationList = A411CF091A84D2EA0053A6F8 /* Build configuration list for PBXProject "Pulsar" */; 239 | compatibilityVersion = "Xcode 3.2"; 240 | developmentRegion = en; 241 | hasScannedForEncodings = 0; 242 | knownRegions = ( 243 | en, 244 | Base, 245 | ); 246 | mainGroup = A411CF051A84D2EA0053A6F8; 247 | productRefGroup = A411CF101A84D2EA0053A6F8 /* Products */; 248 | projectDirPath = ""; 249 | projectRoot = ""; 250 | targets = ( 251 | A411CF5E1A8501180053A6F8 /* Pulsar */, 252 | A411CF2E1A84D3090053A6F8 /* PulsarDemo */, 253 | ); 254 | }; 255 | /* End PBXProject section */ 256 | 257 | /* Begin PBXResourcesBuildPhase section */ 258 | A411CF2D1A84D3090053A6F8 /* Resources */ = { 259 | isa = PBXResourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | A411CF391A84D3090053A6F8 /* Main.storyboard in Resources */, 263 | A411CF3E1A84D3090053A6F8 /* LaunchScreen.xib in Resources */, 264 | A411CF3B1A84D3090053A6F8 /* Images.xcassets in Resources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | A411CF5D1A8501180053A6F8 /* Resources */ = { 269 | isa = PBXResourcesBuildPhase; 270 | buildActionMask = 2147483647; 271 | files = ( 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | }; 275 | /* End PBXResourcesBuildPhase section */ 276 | 277 | /* Begin PBXSourcesBuildPhase section */ 278 | A411CF2B1A84D3090053A6F8 /* Sources */ = { 279 | isa = PBXSourcesBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | A411CF851A85112F0053A6F8 /* StarButton.swift in Sources */, 283 | A411CF361A84D3090053A6F8 /* ViewController.swift in Sources */, 284 | A411CF341A84D3090053A6F8 /* AppDelegate.swift in Sources */, 285 | A411CF871A8511510053A6F8 /* UIButton+Theme.swift in Sources */, 286 | A411CF811A8511010053A6F8 /* RoundedRectButton.swift in Sources */, 287 | A411CF831A85111C0053A6F8 /* CircleButton.swift in Sources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | A411CF5A1A8501180053A6F8 /* Sources */ = { 292 | isa = PBXSourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | CAD18156222E70F6008CFFB4 /* AnimationDelegate.swift in Sources */, 296 | CAD18154222E70E2008CFFB4 /* Pulse.swift in Sources */, 297 | A411CF7F1A85015B0053A6F8 /* CALayer+Extensions.swift in Sources */, 298 | CAD18152222E70C0008CFFB4 /* PulseLayer.swift in Sources */, 299 | CAD18158222E735D008CFFB4 /* ApplicationObserver.swift in Sources */, 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | }; 303 | /* End PBXSourcesBuildPhase section */ 304 | 305 | /* Begin PBXTargetDependency section */ 306 | A411CF751A8501190053A6F8 /* PBXTargetDependency */ = { 307 | isa = PBXTargetDependency; 308 | target = A411CF5E1A8501180053A6F8 /* Pulsar */; 309 | targetProxy = A411CF741A8501190053A6F8 /* PBXContainerItemProxy */; 310 | }; 311 | /* End PBXTargetDependency section */ 312 | 313 | /* Begin PBXVariantGroup section */ 314 | A411CF371A84D3090053A6F8 /* Main.storyboard */ = { 315 | isa = PBXVariantGroup; 316 | children = ( 317 | A411CF381A84D3090053A6F8 /* Base */, 318 | ); 319 | name = Main.storyboard; 320 | sourceTree = ""; 321 | }; 322 | A411CF3C1A84D3090053A6F8 /* LaunchScreen.xib */ = { 323 | isa = PBXVariantGroup; 324 | children = ( 325 | A411CF3D1A84D3090053A6F8 /* Base */, 326 | ); 327 | name = LaunchScreen.xib; 328 | sourceTree = ""; 329 | }; 330 | /* End PBXVariantGroup section */ 331 | 332 | /* Begin XCBuildConfiguration section */ 333 | A411CF231A84D2EA0053A6F8 /* Debug */ = { 334 | isa = XCBuildConfiguration; 335 | buildSettings = { 336 | ALWAYS_SEARCH_USER_PATHS = NO; 337 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 338 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 339 | CLANG_CXX_LIBRARY = "libc++"; 340 | CLANG_ENABLE_MODULES = YES; 341 | CLANG_ENABLE_OBJC_ARC = YES; 342 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 343 | CLANG_WARN_BOOL_CONVERSION = YES; 344 | CLANG_WARN_COMMA = YES; 345 | CLANG_WARN_CONSTANT_CONVERSION = YES; 346 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 347 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 348 | CLANG_WARN_EMPTY_BODY = YES; 349 | CLANG_WARN_ENUM_CONVERSION = YES; 350 | CLANG_WARN_INFINITE_RECURSION = YES; 351 | CLANG_WARN_INT_CONVERSION = YES; 352 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 354 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 356 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 357 | CLANG_WARN_STRICT_PROTOTYPES = YES; 358 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | COPY_PHASE_STRIP = NO; 362 | CURRENT_PROJECT_VERSION = 1; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | ENABLE_TESTABILITY = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu99; 366 | GCC_DYNAMIC_NO_PIC = NO; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_OPTIMIZATION_LEVEL = 0; 369 | GCC_PREPROCESSOR_DEFINITIONS = ( 370 | "DEBUG=1", 371 | "$(inherited)", 372 | ); 373 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 374 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 375 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 376 | GCC_WARN_UNDECLARED_SELECTOR = YES; 377 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 378 | GCC_WARN_UNUSED_FUNCTION = YES; 379 | GCC_WARN_UNUSED_VARIABLE = YES; 380 | MACOSX_DEPLOYMENT_TARGET = 10.10; 381 | MTL_ENABLE_DEBUG_INFO = YES; 382 | ONLY_ACTIVE_ARCH = YES; 383 | SDKROOT = macosx; 384 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 385 | SWIFT_VERSION = 4.2; 386 | VERSIONING_SYSTEM = "apple-generic"; 387 | VERSION_INFO_PREFIX = ""; 388 | }; 389 | name = Debug; 390 | }; 391 | A411CF241A84D2EA0053A6F8 /* Release */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 396 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 397 | CLANG_CXX_LIBRARY = "libc++"; 398 | CLANG_ENABLE_MODULES = YES; 399 | CLANG_ENABLE_OBJC_ARC = YES; 400 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 401 | CLANG_WARN_BOOL_CONVERSION = YES; 402 | CLANG_WARN_COMMA = YES; 403 | CLANG_WARN_CONSTANT_CONVERSION = YES; 404 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 405 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 406 | CLANG_WARN_EMPTY_BODY = YES; 407 | CLANG_WARN_ENUM_CONVERSION = YES; 408 | CLANG_WARN_INFINITE_RECURSION = YES; 409 | CLANG_WARN_INT_CONVERSION = YES; 410 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 411 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 412 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 413 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 414 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 415 | CLANG_WARN_STRICT_PROTOTYPES = YES; 416 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 417 | CLANG_WARN_UNREACHABLE_CODE = YES; 418 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 419 | COPY_PHASE_STRIP = NO; 420 | CURRENT_PROJECT_VERSION = 1; 421 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 422 | ENABLE_NS_ASSERTIONS = NO; 423 | ENABLE_STRICT_OBJC_MSGSEND = YES; 424 | GCC_C_LANGUAGE_STANDARD = gnu99; 425 | GCC_NO_COMMON_BLOCKS = YES; 426 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 427 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 428 | GCC_WARN_UNDECLARED_SELECTOR = YES; 429 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 430 | GCC_WARN_UNUSED_FUNCTION = YES; 431 | GCC_WARN_UNUSED_VARIABLE = YES; 432 | MACOSX_DEPLOYMENT_TARGET = 10.10; 433 | MTL_ENABLE_DEBUG_INFO = NO; 434 | SDKROOT = macosx; 435 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 436 | SWIFT_VERSION = 4.2; 437 | VERSIONING_SYSTEM = "apple-generic"; 438 | VERSION_INFO_PREFIX = ""; 439 | }; 440 | name = Release; 441 | }; 442 | A411CF4C1A84D3090053A6F8 /* Debug */ = { 443 | isa = XCBuildConfiguration; 444 | buildSettings = { 445 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 446 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 447 | DEVELOPMENT_TEAM = C4K7WZKHXH; 448 | GCC_PREPROCESSOR_DEFINITIONS = ( 449 | "DEBUG=1", 450 | "$(inherited)", 451 | ); 452 | INFOPLIST_FILE = PulsarDemo/Info.plist; 453 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 454 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 455 | PRODUCT_BUNDLE_IDENTIFIER = "com.regexident.$(PRODUCT_NAME:rfc1034identifier)"; 456 | PRODUCT_NAME = "$(TARGET_NAME)"; 457 | SDKROOT = iphoneos; 458 | SWIFT_VERSION = 5.0; 459 | TARGETED_DEVICE_FAMILY = "1,2"; 460 | }; 461 | name = Debug; 462 | }; 463 | A411CF4D1A84D3090053A6F8 /* Release */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 467 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 468 | DEVELOPMENT_TEAM = C4K7WZKHXH; 469 | INFOPLIST_FILE = PulsarDemo/Info.plist; 470 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 471 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 472 | PRODUCT_BUNDLE_IDENTIFIER = "com.regexident.$(PRODUCT_NAME:rfc1034identifier)"; 473 | PRODUCT_NAME = "$(TARGET_NAME)"; 474 | SDKROOT = iphoneos; 475 | SWIFT_VERSION = 5.0; 476 | TARGETED_DEVICE_FAMILY = "1,2"; 477 | VALIDATE_PRODUCT = YES; 478 | }; 479 | name = Release; 480 | }; 481 | A411CF791A8501190053A6F8 /* Debug */ = { 482 | isa = XCBuildConfiguration; 483 | buildSettings = { 484 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 485 | DEFINES_MODULE = YES; 486 | DYLIB_COMPATIBILITY_VERSION = 1; 487 | DYLIB_CURRENT_VERSION = 1; 488 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 489 | GCC_PREPROCESSOR_DEFINITIONS = ( 490 | "DEBUG=1", 491 | "$(inherited)", 492 | ); 493 | INFOPLIST_FILE = Pulsar/Info.plist; 494 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 495 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 496 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 497 | PRODUCT_BUNDLE_IDENTIFIER = "com.regexident.$(PRODUCT_NAME:rfc1034identifier)"; 498 | PRODUCT_NAME = "$(TARGET_NAME)"; 499 | SDKROOT = iphoneos; 500 | SKIP_INSTALL = YES; 501 | SWIFT_VERSION = 5.0; 502 | TARGETED_DEVICE_FAMILY = "1,2"; 503 | }; 504 | name = Debug; 505 | }; 506 | A411CF7A1A8501190053A6F8 /* Release */ = { 507 | isa = XCBuildConfiguration; 508 | buildSettings = { 509 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 510 | DEFINES_MODULE = YES; 511 | DYLIB_COMPATIBILITY_VERSION = 1; 512 | DYLIB_CURRENT_VERSION = 1; 513 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 514 | INFOPLIST_FILE = Pulsar/Info.plist; 515 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 516 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 517 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 518 | PRODUCT_BUNDLE_IDENTIFIER = "com.regexident.$(PRODUCT_NAME:rfc1034identifier)"; 519 | PRODUCT_NAME = "$(TARGET_NAME)"; 520 | SDKROOT = iphoneos; 521 | SKIP_INSTALL = YES; 522 | SWIFT_VERSION = 5.0; 523 | TARGETED_DEVICE_FAMILY = "1,2"; 524 | VALIDATE_PRODUCT = YES; 525 | }; 526 | name = Release; 527 | }; 528 | /* End XCBuildConfiguration section */ 529 | 530 | /* Begin XCConfigurationList section */ 531 | A411CF091A84D2EA0053A6F8 /* Build configuration list for PBXProject "Pulsar" */ = { 532 | isa = XCConfigurationList; 533 | buildConfigurations = ( 534 | A411CF231A84D2EA0053A6F8 /* Debug */, 535 | A411CF241A84D2EA0053A6F8 /* Release */, 536 | ); 537 | defaultConfigurationIsVisible = 0; 538 | defaultConfigurationName = Release; 539 | }; 540 | A411CF4B1A84D3090053A6F8 /* Build configuration list for PBXNativeTarget "PulsarDemo" */ = { 541 | isa = XCConfigurationList; 542 | buildConfigurations = ( 543 | A411CF4C1A84D3090053A6F8 /* Debug */, 544 | A411CF4D1A84D3090053A6F8 /* Release */, 545 | ); 546 | defaultConfigurationIsVisible = 0; 547 | defaultConfigurationName = Release; 548 | }; 549 | A411CF781A8501190053A6F8 /* Build configuration list for PBXNativeTarget "Pulsar" */ = { 550 | isa = XCConfigurationList; 551 | buildConfigurations = ( 552 | A411CF791A8501190053A6F8 /* Debug */, 553 | A411CF7A1A8501190053A6F8 /* Release */, 554 | ); 555 | defaultConfigurationIsVisible = 0; 556 | defaultConfigurationName = Release; 557 | }; 558 | /* End XCConfigurationList section */ 559 | }; 560 | rootObject = A411CF061A84D2EA0053A6F8 /* Project object */; 561 | } 562 | -------------------------------------------------------------------------------- /Pulsar.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Pulsar.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Pulsar.xcodeproj/xcshareddata/xcschemes/Pulsar.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Pulsar/Classes/AnimationDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationDelegate.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 3/5/19. 6 | // Copyright © 2019 Regexident. All rights reserved. 7 | // 8 | 9 | import QuartzCore 10 | 11 | internal class AnimationDelegate: NSObject { 12 | let pulseLayer: PulseLayer 13 | let startBlock: Pulse.StartClosure? = nil 14 | let stopBlock: Pulse.StopClosure? = nil 15 | 16 | init(pulseLayer: PulseLayer) { 17 | self.pulseLayer = pulseLayer 18 | } 19 | } 20 | 21 | extension AnimationDelegate: CAAnimationDelegate { 22 | func animationDidStart(_ animation: CAAnimation) { 23 | if let startBlock = self.startBlock { 24 | startBlock(animation.duration) 25 | } 26 | } 27 | 28 | func animationDidStop(_ animation: CAAnimation, finished: Bool) { 29 | guard var pulseLayers = self.pulseLayer.superlayer?.pulseLayers else { 30 | return 31 | } 32 | if let index = pulseLayers.firstIndex(of: self.pulseLayer) { 33 | pulseLayers.remove(at: index) 34 | self.pulseLayer.removeFromSuperlayer() 35 | if let stopBlock = self.stopBlock { 36 | stopBlock(finished) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Pulsar/Classes/ApplicationObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationObserver.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 3/5/19. 6 | // Copyright © 2019 Regexident. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal protocol ApplicationObserverDelegate: class { 12 | func applicationWillEnterForeground() 13 | func applicationDidEnterBackground() 14 | } 15 | 16 | internal class ApplicationObserver { 17 | internal weak var delegate: ApplicationObserverDelegate? 18 | 19 | public init() { 20 | self.addApplicationObservers() 21 | } 22 | 23 | deinit { 24 | self.removeApplicationObservers() 25 | } 26 | 27 | private func addApplicationObservers() { 28 | NotificationCenter.default.addObserver( 29 | self, 30 | selector: #selector(applicationWillEnterForeground), 31 | name: UIApplication.willEnterForegroundNotification, 32 | object: nil 33 | ) 34 | NotificationCenter.default.addObserver( 35 | self, 36 | selector: #selector(applicationDidEnterBackground), 37 | name: UIApplication.didEnterBackgroundNotification, 38 | object: nil 39 | ) 40 | } 41 | 42 | private func removeApplicationObservers() { 43 | NotificationCenter.default.removeObserver(self) 44 | } 45 | 46 | @objc private func applicationWillEnterForeground() { 47 | self.delegate?.applicationWillEnterForeground() 48 | } 49 | 50 | @objc private func applicationDidEnterBackground() { 51 | self.delegate?.applicationDidEnterBackground() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Pulsar/Classes/CALayer+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+Pulsar.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 2/6/15. 6 | // Copyright (c) 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import QuartzCore 10 | 11 | extension CALayer { 12 | public func addPulse(_ closure: ((Pulse) -> ())? = nil) -> PulseLayer { 13 | if self.masksToBounds == true { 14 | NSLog("Warning: CALayers with 'self.masksToBounds = true' might not show a pulse.") 15 | } 16 | 17 | let pulse = Pulse(self) 18 | if let closure = closure { 19 | closure(pulse) 20 | } 21 | 22 | let pulseLayer = PulseLayer(pulse: pulse) 23 | self.insertSublayer(pulseLayer, at:0) 24 | 25 | let animation = PulseLayer.pulseAnimation(from: pulse) 26 | animation.delegate = AnimationDelegate(pulseLayer: pulseLayer) 27 | pulseLayer.add(animation, forKey: PulseLayer.Constants.animationKey) 28 | 29 | self.pulseLayers.append(pulseLayer) 30 | 31 | return pulseLayer 32 | } 33 | 34 | public func removePulse(_ pulse: PulseLayer) { 35 | if let index = self.pulseLayers.firstIndex(where: { $0 === pulse }) { 36 | pulse.removeAllAnimations() 37 | pulse.removeFromSuperlayer() 38 | self.pulseLayers.remove(at: index) 39 | } 40 | } 41 | 42 | public func removePulses() { 43 | for pulseLayer in self.pulseLayers { 44 | pulseLayer.removeAllAnimations() 45 | pulseLayer.removeFromSuperlayer() 46 | } 47 | self.pulseLayers = [] 48 | } 49 | 50 | var pulseLayers: [PulseLayer] { 51 | set { 52 | self.setValue(newValue, forKey: PulseLayer.Constants.layersKey) 53 | } 54 | get { 55 | return self.value(forKey: PulseLayer.Constants.layersKey) as? [PulseLayer] ?? [] 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Pulsar/Classes/Pulse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pulse.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 3/5/19. 6 | // Copyright © 2019 Regexident. All rights reserved. 7 | // 8 | 9 | import QuartzCore 10 | 11 | public class Pulse { 12 | public typealias StartClosure = (TimeInterval) -> () 13 | public typealias StopClosure = (Bool) -> () 14 | 15 | public var borderColors: [CGColor] 16 | public var backgroundColors: [CGColor] 17 | public var frame: CGRect 18 | public var path: CGPath 19 | public var duration: TimeInterval = 1.0 20 | public var repeatDelay: TimeInterval = 0.0 21 | public var repeatCount: Int = 0 22 | public var lineWidth: CGFloat = 3.0 23 | public var transformBefore: CATransform3D = CATransform3DIdentity 24 | public var transformAfter: CATransform3D 25 | public var startBlock: StartClosure? = nil 26 | public var stopBlock: StopClosure? = nil 27 | 28 | init(_ layer: CALayer) { 29 | self.borderColors = Pulse.defaultBorderColors(for: layer) 30 | self.backgroundColors = Pulse.defaultBackgroundColors(for: layer) 31 | self.frame = Pulse.defaultFrame(for: layer) 32 | self.path = Pulse.defaultPath(for: layer) 33 | self.transformAfter = Pulse.defaultTransformAfter(for: layer) 34 | } 35 | 36 | private class func defaultBackgroundColors(for layer: CALayer) -> [CGColor] { 37 | switch layer { 38 | case let shapeLayer as CAShapeLayer: 39 | if let fillColor = shapeLayer.fillColor { 40 | let halfAlpha = fillColor.alpha * 0.5 41 | return [fillColor.copy(alpha: halfAlpha)!] 42 | } 43 | default: 44 | if let backgroundColor = layer.backgroundColor { 45 | let halfAlpha = backgroundColor.alpha * 0.5 46 | return [backgroundColor.copy(alpha: halfAlpha)!] 47 | } 48 | } 49 | let colorSpace = CGColorSpaceCreateDeviceRGB() 50 | let components: [CGFloat] = [1.0, 0.0, 0.0, 0.0] 51 | return [CGColor(colorSpace: colorSpace, components: components)!] 52 | } 53 | 54 | private class func defaultBorderColors(for layer: CALayer) -> [CGColor] { 55 | switch layer { 56 | case let shapeLayer as CAShapeLayer: 57 | if shapeLayer.lineWidth > 0.0 { 58 | if let strokeColor = shapeLayer.strokeColor { 59 | return [strokeColor] 60 | } 61 | } else { 62 | if let fillColor = shapeLayer.fillColor { 63 | return [fillColor] 64 | } 65 | } 66 | default: 67 | if layer.borderWidth > 0.0 { 68 | if let borderColor = layer.borderColor { 69 | return [borderColor] 70 | } 71 | } else { 72 | if let backgroundColor = layer.backgroundColor { 73 | return [backgroundColor] 74 | } 75 | } 76 | } 77 | let colorSpace = CGColorSpaceCreateDeviceRGB() 78 | let components: [CGFloat] = [1.0, 0.0, 0.0, 0.0] 79 | return [CGColor(colorSpace: colorSpace, components: components)!] 80 | } 81 | 82 | private class func defaultFrame(for layer: CALayer) -> CGRect { 83 | return layer.bounds 84 | } 85 | 86 | private class func defaultPath(for layer: CALayer) -> CGPath { 87 | switch layer { 88 | case let shapeLayer as CAShapeLayer: 89 | return shapeLayer.path! 90 | default: 91 | let rect = layer.bounds 92 | let minSize = min(rect.width, rect.height) 93 | let cornerRadius = min(max(0.0, layer.cornerRadius), minSize / 2.0) 94 | if cornerRadius > 0.0 { 95 | return CGPath( 96 | roundedRect: rect, 97 | cornerWidth: cornerRadius, 98 | cornerHeight: cornerRadius, 99 | transform: nil 100 | ) 101 | } else { 102 | return CGPath(rect: rect, transform: nil) 103 | } 104 | } 105 | } 106 | 107 | private class func defaultTransformAfter(for layer: CALayer) -> CATransform3D { 108 | let anchorPoint = layer.anchorPoint 109 | var transform = CATransform3DIdentity 110 | transform = CATransform3DTranslate(transform, anchorPoint.x, anchorPoint.y, 0.0); 111 | transform = CATransform3DScale(transform, 2.0, 2.0, 1.0); 112 | transform = CATransform3DTranslate(transform, -anchorPoint.x, -anchorPoint.y, 0.0); 113 | return transform 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Pulsar/Classes/PulseLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulseLayer.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 3/5/19. 6 | // Copyright © 2019 Regexident. All rights reserved. 7 | // 8 | 9 | import QuartzCore 10 | 11 | public class PulseLayer: CAShapeLayer { 12 | enum Constants { 13 | static let layersKey: String = "Pulsar.layers" 14 | static let animationKey: String = "Pulsar.animation" 15 | static let persistenceKey: String = "Pulsar.persistence" 16 | } 17 | 18 | fileprivate struct Persistence { 19 | let animations: [String: CAAnimation] 20 | let speed: Float 21 | } 22 | 23 | public var isAnimationsPaused: Bool { 24 | return self.speed == 0.0 25 | } 26 | 27 | fileprivate let pulse: Pulse 28 | fileprivate var persistence: Persistence? 29 | fileprivate let applicationObserver: ApplicationObserver = .init() 30 | 31 | init(pulse: Pulse) { 32 | self.pulse = pulse 33 | 34 | super.init() 35 | 36 | self.reset() 37 | 38 | self.applicationObserver.delegate = self 39 | } 40 | 41 | public override init(layer anyLayer: Any) { 42 | guard let layer = anyLayer as? PulseLayer else { 43 | fatalError("Expected \(PulseLayer.self), found \(type(of: anyLayer)).") 44 | } 45 | self.pulse = layer.pulse 46 | 47 | super.init(layer: layer) 48 | } 49 | 50 | public required init?(coder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | fileprivate func reset(animated: Bool = false) { 55 | CATransaction.begin() 56 | CATransaction.setDisableActions(!animated) 57 | self.frame = pulse.frame 58 | self.fillColor = pulse.backgroundColors.first 59 | self.opacity = 0.0 60 | self.path = pulse.path 61 | self.strokeColor = pulse.borderColors.first 62 | self.lineWidth = pulse.lineWidth 63 | CATransaction.commit() 64 | } 65 | 66 | class func pulseAnimation(from pulse: Pulse) -> CAAnimation { 67 | var animations: [CAAnimation] = [] 68 | 69 | let opacityAnimation = CABasicAnimation(keyPath: "opacity") 70 | opacityAnimation.fromValue = 1.0 71 | opacityAnimation.toValue = 0.0 72 | opacityAnimation.duration = max(pulse.duration, 0.0) 73 | opacityAnimation.isRemovedOnCompletion = false 74 | animations.append(opacityAnimation) 75 | 76 | let transformAnimation = CABasicAnimation(keyPath: "transform") 77 | transformAnimation.fromValue = NSValue(caTransform3D: pulse.transformBefore) 78 | transformAnimation.toValue = NSValue(caTransform3D: pulse.transformAfter) 79 | transformAnimation.duration = max(pulse.duration, 0.0) 80 | transformAnimation.isRemovedOnCompletion = false 81 | animations.append(transformAnimation) 82 | 83 | if pulse.borderColors.count > 1 { 84 | let strokeColorAnimation = CAKeyframeAnimation(keyPath: "strokeColor") 85 | strokeColorAnimation.values = pulse.borderColors 86 | strokeColorAnimation.duration = max(pulse.duration, 0.0) 87 | strokeColorAnimation.isRemovedOnCompletion = false 88 | animations.append(strokeColorAnimation) 89 | } 90 | 91 | if pulse.backgroundColors.count > 1 { 92 | let fillColorAnimation = CAKeyframeAnimation(keyPath: "fillColor") 93 | fillColorAnimation.values = pulse.backgroundColors 94 | fillColorAnimation.duration = max(pulse.duration, 0.0) 95 | fillColorAnimation.isRemovedOnCompletion = false 96 | animations.append(fillColorAnimation) 97 | } 98 | 99 | let animationGroup = CAAnimationGroup() 100 | animationGroup.duration = max(pulse.duration, 0.0) 101 | if pulse.repeatCount > 0 { 102 | animationGroup.duration += max(pulse.repeatDelay, 0.0) 103 | } 104 | animationGroup.animations = animations 105 | animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) 106 | animationGroup.repeatCount = min(Float(pulse.repeatCount), Float.infinity) 107 | animationGroup.isRemovedOnCompletion = false 108 | 109 | return animationGroup 110 | } 111 | 112 | public func pauseAnimations() { 113 | // https://developer.apple.com/library/archive/qa/qa1673/_index.html 114 | 115 | let currentTime = CACurrentMediaTime() 116 | let pausedTime = self.convertTime(currentTime, from: nil) 117 | self.speed = 0.0 118 | self.timeOffset = pausedTime 119 | } 120 | 121 | public func resumeAnimations(speed: Float = 1.0) { 122 | // https://developer.apple.com/library/archive/qa/qa1673/_index.html 123 | 124 | let pausedTime = self.timeOffset 125 | self.speed = speed 126 | self.timeOffset = 0.0 127 | self.beginTime = 0.0 128 | let currentTime = CACurrentMediaTime() 129 | let timeSincePause = self.convertTime(currentTime, from: nil) - pausedTime 130 | self.beginTime = timeSincePause 131 | } 132 | 133 | fileprivate func restoreAnimations() -> Float { 134 | guard let persistence = self.persistence else { 135 | return self.speed 136 | } 137 | 138 | defer { 139 | self.persistence = nil 140 | } 141 | 142 | self.speed = persistence.speed 143 | 144 | for (key, animation) in persistence.animations { 145 | self.removeAnimation(forKey: key) 146 | self.add(animation, forKey: key) 147 | } 148 | 149 | return persistence.speed 150 | } 151 | 152 | fileprivate func persistAnimations() { 153 | guard self.persistence == nil else { 154 | return 155 | } 156 | 157 | let animationKeys = self.animationKeys() ?? [] 158 | let animationsByKey = animationKeys.compactMap { key in 159 | self.animation(forKey: key).map { (key, $0) } 160 | } 161 | let animations = Dictionary(uniqueKeysWithValues: animationsByKey) 162 | 163 | self.persistence = Persistence( 164 | animations: animations, 165 | speed: speed 166 | ) 167 | } 168 | } 169 | 170 | extension PulseLayer: ApplicationObserverDelegate { 171 | public func applicationWillEnterForeground() { 172 | let speed = self.restoreAnimations() 173 | self.resumeAnimations(speed: speed) 174 | } 175 | 176 | public func applicationDidEnterBackground() { 177 | self.persistAnimations() 178 | self.pauseAnimations() 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Pulsar/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.0.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pulsar/Pulsar.h: -------------------------------------------------------------------------------- 1 | // 2 | // Pulsar.h 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 2/6/15. 6 | // Copyright (c) 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Pulsar. 12 | FOUNDATION_EXPORT double PulsarVersionNumber; 13 | 14 | //! Project version string for Pulsar. 15 | FOUNDATION_EXPORT const unsigned char PulsarVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /PulsarDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PulsarDemo 4 | // 5 | // Created by Vincent Esche on 2/6/15. 6 | // Copyright (c) 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PulsarDemo/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /PulsarDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 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 | 83 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /PulsarDemo/CircleButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleButton.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 2/6/15. 6 | // Copyright (c) 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CircleButton: UIButton { 12 | required init?(coder decoder: NSCoder) { 13 | super.init(coder: decoder) 14 | self.layer.cornerRadius = CircleButton.cornerRadiusForRect(self.bounds) 15 | self.applyTheme() 16 | } 17 | 18 | class func cornerRadiusForRect(_ rect: CGRect) -> CGFloat { 19 | return min(rect.width, rect.height) / 2.0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-Notification@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-Notification@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-Small@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-Small@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-Small-40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-Small-40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-Notification.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-Notification@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-Small.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-Small@2x.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-Small-40.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-Small-40@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-83.5@2x.png", 103 | "scale" : "2x" 104 | } 105 | ] 106 | } -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Notification.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Notification@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Notification@2x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Notification@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Notification@3x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/PulsarDemo/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /PulsarDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Pulsar 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /PulsarDemo/RoundedRectButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedRectButton.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 2/6/15. 6 | // Copyright (c) 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RoundedRectButton: UIButton { 12 | required init?(coder decoder: NSCoder) { 13 | super.init(coder: decoder) 14 | self.layer.cornerRadius = CircleButton.cornerRadiusForRect(self.bounds) 15 | self.applyTheme() 16 | } 17 | 18 | class func cornerRadiusForRect(_ rect: CGRect) -> CGFloat { 19 | return min(rect.width, rect.height) / 4.0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PulsarDemo/StarButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StarButton.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 2/6/15. 6 | // Copyright (c) 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StarButton: UIButton { 12 | 13 | required init?(coder decoder: NSCoder) { 14 | super.init(coder: decoder) 15 | let shapeLayer = self.layer as! CAShapeLayer 16 | shapeLayer.path = StarButton.pathForRect(self.bounds) 17 | self.applyTheme() 18 | } 19 | 20 | override class var layerClass : AnyClass { 21 | return CAShapeLayer.self 22 | } 23 | 24 | class func pathForRect(_ rect: CGRect) -> CGPath { 25 | let origin = rect.origin 26 | let width = rect.width 27 | let height = rect.height 28 | let transform = CGAffineTransform(translationX: -origin.x, y: -origin.y) 29 | let path = CGMutablePath() 30 | path.move(to: CGPoint(x: width * 0.5000, y: height * 0.0200), transform: transform) 31 | path.addLine(to: CGPoint(x: width * 0.6834, y: height * 0.2876), transform: transform) 32 | path.addLine(to: CGPoint(x: width * 0.9945, y: height * 0.3793), transform: transform) 33 | path.addLine(to: CGPoint(x: width * 0.7967, y: height * 0.6364), transform: transform) 34 | path.addLine(to: CGPoint(x: width * 0.8056, y: height * 0.9607), transform: transform) 35 | path.addLine(to: CGPoint(x: width * 0.5000, y: height * 0.8520), transform: transform) 36 | path.addLine(to: CGPoint(x: width * 0.1944, y: height * 0.9607), transform: transform) 37 | path.addLine(to: CGPoint(x: width * 0.2033, y: height * 0.6364), transform: transform) 38 | path.addLine(to: CGPoint(x: width * 0.0055, y: height * 0.3793), transform: transform) 39 | path.addLine(to: CGPoint(x: width * 0.3166, y: height * 0.2876), transform: transform) 40 | path.closeSubpath() 41 | return path 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /PulsarDemo/UIButton+Theme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+Theme.swift 3 | // Pulsar 4 | // 5 | // Created by Vincent Esche on 2/6/15. 6 | // Copyright (c) 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIButton { 12 | func applyTheme() { 13 | let fillColor = UIColor(white: 0.0, alpha: 0.25).cgColor 14 | let borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor 15 | if let shapeLayer = self.layer as? CAShapeLayer { 16 | shapeLayer.fillColor = fillColor 17 | shapeLayer.strokeColor = borderColor 18 | shapeLayer.lineWidth = 3.0 19 | } else { 20 | layer.backgroundColor = fillColor 21 | layer.borderColor = borderColor 22 | layer.borderWidth = 3.0 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PulsarDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PulsarDemo 4 | // 5 | // Created by Vincent Esche on 2/6/15. 6 | // Copyright (c) 2015 Vincent Esche. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import Pulsar 12 | 13 | func colorsWithHalfOpacity(_ colors: [CGColor]) -> [CGColor] { 14 | return colors.map({ $0.copy(alpha: $0.alpha * 0.5)! }) 15 | } 16 | 17 | class ViewController: UIViewController { 18 | 19 | @IBOutlet var containerView: UIView! 20 | @IBOutlet var activityIndicatorView: UIActivityIndicatorView! 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | UIApplication.shared.statusBarStyle = .lightContent 26 | 27 | self.view.backgroundColor = UIColor.lightGray 28 | } 29 | 30 | override func viewWillAppear(_ animated: Bool) { 31 | super.viewWillAppear(animated) 32 | 33 | self.addRepeatingPulseToProgressIndicator() 34 | self.activityIndicatorView.startAnimating() 35 | } 36 | 37 | func addRepeatingPulseToProgressIndicator() { 38 | let _ = self.activityIndicatorView.layer.addPulse { pulse in 39 | pulse.borderColors = [UIColor.clear.cgColor, UIColor.black.cgColor] 40 | pulse.backgroundColors = colorsWithHalfOpacity(pulse.borderColors) 41 | pulse.path = UIBezierPath(ovalIn: self.activityIndicatorView.bounds).cgPath 42 | pulse.transformBefore = CATransform3DMakeScale(0.65, 0.65, 0.0) 43 | pulse.duration = 2.0 44 | pulse.repeatDelay = 0.0 45 | pulse.repeatCount = Int.max 46 | pulse.lineWidth = 2.0 47 | pulse.backgroundColors = [] 48 | } 49 | } 50 | 51 | @IBAction func didTriggerActionOnStarButton(_ sender: StarButton) { 52 | let _ = sender.layer.addPulse { pulse in 53 | pulse.borderColors = [ 54 | UIColor.green.cgColor, 55 | UIColor.yellow.cgColor, 56 | UIColor.yellow.cgColor, 57 | UIColor.red.cgColor 58 | ] 59 | pulse.backgroundColors = colorsWithHalfOpacity(pulse.borderColors) 60 | } 61 | } 62 | 63 | @IBAction func didTriggerActionOnRoundedRectButton(_ sender: RoundedRectButton) { 64 | let _ = sender.layer.addPulse() 65 | } 66 | 67 | @IBAction func didTriggerActionOnCircleButton(_ sender: CircleButton) { 68 | let _ = sender.layer.addPulse { pulse in 69 | pulse.borderColors = [ 70 | UIColor(hue: CGFloat(arc4random()) / CGFloat(RAND_MAX), saturation: 1.0, brightness: 1.0, alpha: 1.0).cgColor 71 | ] 72 | pulse.backgroundColors = colorsWithHalfOpacity(pulse.borderColors) 73 | } 74 | } 75 | 76 | @IBAction func didTriggerActionOnSlider(_ sender: UISlider) { 77 | let subviews = sender.subviews 78 | let view = subviews[2] 79 | let delayTime = DispatchTime.now() + Double(Int64(0.2 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) 80 | DispatchQueue.main.asyncAfter(deadline: delayTime) { 81 | let bounds = view.bounds 82 | let path = CGPath(ellipseIn: bounds, transform: nil) 83 | let saturation = CGFloat(sender.value) 84 | let _ = view.layer.addPulse { pulse in 85 | pulse.borderColors = [UIColor(hue: 0.6, saturation: saturation, brightness: 1.0, alpha: 1.0).cgColor] 86 | pulse.backgroundColors = colorsWithHalfOpacity(pulse.borderColors) 87 | pulse.path = path 88 | } 89 | } 90 | } 91 | 92 | @IBAction func didTriggerActionOnSwitch(_ sender: UISwitch) { 93 | let internalSubview = sender.subviews.first! 94 | let subviews = internalSubview.subviews 95 | let view = subviews[3] 96 | let delayTime = DispatchTime.now() + Double(Int64(0.4 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) 97 | DispatchQueue.main.asyncAfter(deadline: delayTime) { 98 | var bounds = view.bounds 99 | bounds = CGRect( 100 | x: bounds.minX + (bounds.width - bounds.height) / 2, 101 | y: bounds.minY, 102 | width: bounds.height, 103 | height: bounds.height 104 | ) 105 | let _ = view.layer.addPulse { pulse in 106 | pulse.borderColors = [(sender.isOn) ? UIColor.green.cgColor : UIColor.white.cgColor] 107 | pulse.backgroundColors = colorsWithHalfOpacity(pulse.borderColors) 108 | pulse.path = UIBezierPath(ovalIn: bounds).cgPath 109 | } 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![jumbotron](jumbotron.png) 2 | # Pulsar 3 | 4 | **Pulsar** is a versatile solution for **displaying pulse animations** as known from **Apple Maps**. 5 | 6 | Being implemented on **CALayer**, **Pulsar** is compatible with **just about any UI control** thinkable, given that every **UIView** is backed by a **CALayer**. 7 | 8 | ## Preview 9 | ![screencast](screencast.gif) 10 | 11 | ## Features 12 | 13 | **Pulsar** consists of a **simple category** on **CALayer** making use of the **builder pattern** for **hassle-free customization**: 14 | 15 | - Custom **duration** and **repeat count**. 16 | - Custom **line width**. 17 | - Custom **background** and/or **border colors**. (optional) 18 | - Custom **path**. (optional) 19 | - **Start/stop blocks** for attaching a **callback**. (optional) 20 | 21 | While **all** these attributes **can be set**, **none** of them **have to be set explicitly**. 22 | 23 | **Pulsar** will try to **figure out the most likely default colors and paths on its own** (inferred from the host layer's properties, such as its border color), unless one supplies it with custom properties via its builder. 24 | 25 | For more info take a look at these methods: 26 | 27 | ```swift 28 | class func defaultBackgroundColorsForLayer(layer: CALayer) -> [CGColor] 29 | class func defaultBorderColorsForLayer(layer: CALayer) -> [CGColor] 30 | class func defaultPathForLayer(layer: CALayer) -> CGPathRef 31 | ``` 32 | 33 | ## Usage 34 | 35 | To add a (one-time) pulse using **smart default properties** to a layer (e.g., the layer of a **UIView**) simply call `addPulse()` on it: 36 | 37 | ```swift 38 | layer.addPulse() 39 | ``` 40 | 41 | Or if you want to set a custom appearance: 42 | 43 | ```swift 44 | layer.addPulse { builder in 45 | builder.borderColors = [UIColor.redColor().CGColor] 46 | builder.backgroundColors = [] 47 | } 48 | ``` 49 | 50 | To have a pulse repeat a given number of times set a `repeatCount` on the `builder`: 51 | 52 | ```swift 53 | builder.repeatCount = 42 // or Int.max for infinity 54 | ``` 55 | 56 | ## Installation 57 | 58 | Just copy the files in `"Pulsar/Classes/..."` into your project. 59 | 60 | Alternatively you can install **Pulsar** into your project with [CocoaPods](http://cocoapods.org/). 61 | Just add it to your Podfile: `pod 'Pulsar'` 62 | 63 | ## Demos 64 | 65 | **Pulsar** contains a demo app giving you a quick overview of some of the possible use cases. 66 | 67 | ## Swift 68 | 69 | **Pulsar** is implemented in **Swift 3**. 70 | 71 | ## Dependencies 72 | 73 | None. 74 | 75 | ## Creator 76 | 77 | Vincent Esche ([@regexident](http://twitter.com/regexident)) 78 | 79 | ## License 80 | 81 | **Pulsar** is available under a **modified BSD-3 clause license** with the **additional requirement of attribution**. See the `LICENSE` file for more info. 82 | -------------------------------------------------------------------------------- /jumbotron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/jumbotron.png -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regexident/Pulsar/90a96e6a58135d41b6e18a7bfc36b497e2cfcbbe/screencast.gif --------------------------------------------------------------------------------