├── libTest ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── ViewController.swift ├── AppDelegate.swift └── activityIndicator.swift ├── libTestTests ├── Info.plist └── libTestTests.swift ├── libTestUITests ├── Info.plist └── libTestUITests.swift ├── README.md └── License.md /libTest/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /libTestTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /libTestUITests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Requirements 6 | 7 | iOS 8.0+ / 8 | 9 | Xcode 7.2+ 10 | 11 | A simple & stylish background activity Indicator which is easily integrated with just some line of code... 12 | 13 | how to implement ... 14 | 15 | Import the class file to your Project. 16 | 17 | Drag a view from object library to the view where you want to display the activity indicator. 18 | 19 | Click the view and set the class as "activityIndicator" and link to the view ie..@IBOutlet. 20 | 21 | Congrats you have successfully imported the cool activity indicator library. 22 | 23 | enjoy the lib.. 24 | 25 | Controlling function 26 | 27 | activityView.startLoading() 28 | 29 | activityView.completeLoading(true) 30 | 31 | activityView.completeLoading(false) 32 | 33 | activityView.strokeColor = UIColor.redColor() 34 | 35 | 36 | For reference see the example 37 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 abdul 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 | -------------------------------------------------------------------------------- /libTestTests/libTestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // libTestTests.swift 3 | // libTestTests 4 | // 5 | // Created by Rohan Pawar on 25/12/15. 6 | // Copyright © 2015 dhlabs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import libTest 11 | 12 | class libTestTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /libTest/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 | 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 | 40 | 41 | -------------------------------------------------------------------------------- /libTestUITests/libTestUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // libTestUITests.swift 3 | // libTestUITests 4 | // 5 | // Created by Rohan Pawar on 25/12/15. 6 | // Copyright © 2015 dhlabs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class libTestUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /libTest/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 | 27 | 28 | -------------------------------------------------------------------------------- /libTest/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // libTest 4 | // 5 | // Created by abdul karim on 25/12/15. 6 | // Copyright © 2015 dhlabs. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var tapCount = 0 14 | 15 | @IBOutlet weak var activityView: activityIndicator! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | // Do any additional setup after loading the view, typically from a nib. 20 | 21 | // activityView.lineWidth = 2 22 | } 23 | 24 | override func didReceiveMemoryWarning() { 25 | super.didReceiveMemoryWarning() 26 | // Dispose of any resources that can be recreated. 27 | } 28 | 29 | @IBAction func startAction(sender: AnyObject) { 30 | activityView.startLoading() 31 | 32 | } 33 | 34 | @IBAction func progressAction(sender: AnyObject) { 35 | let progress: Float = activityView.progress + 0.1043 36 | activityView.progress = progress 37 | } 38 | 39 | @IBAction func successAction(sender: AnyObject) { 40 | activityView.startLoading() 41 | activityView.completeLoading(true) 42 | } 43 | 44 | @IBAction func unsucessAct(sender: AnyObject) { 45 | activityView.startLoading() 46 | activityView.strokeColor = UIColor.redColor() 47 | activityView.completeLoading(false) 48 | } 49 | @IBAction func changeColorAct(sender: AnyObject) { 50 | tapCount++ 51 | 52 | if (tapCount == 1){ 53 | activityView.strokeColor = UIColor.redColor() 54 | } 55 | else 56 | if (tapCount == 2) { 57 | activityView.strokeColor = UIColor.blackColor() 58 | } 59 | else 60 | if (tapCount == 3) { 61 | tapCount = 0 62 | activityView.strokeColor = UIColor.purpleColor() 63 | 64 | } 65 | 66 | 67 | 68 | 69 | } 70 | 71 | } 72 | 73 | -------------------------------------------------------------------------------- /libTest/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // libTest 4 | // 5 | // Created by abdul karim on 25/12/15. 6 | // Copyright © 2015 dhlabs. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // 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. 24 | // 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. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // 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. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /libTest/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 | 28 | 35 | 42 | 49 | 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 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 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 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /libTest/activityIndicator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | let dhRingStorkeAnimationKey = "IDLoading.stroke" 4 | let dhRingRotationAnimationKey = "IDLoading.rotation" 5 | let dhCompletionAnimationDuration: TimeInterval = 0.3 6 | let dhHidesWhenCompletedDelay: TimeInterval = 0.5 7 | 8 | public typealias Block = () -> Void 9 | 10 | 11 | public class activityIndicator: UIView, CAAnimationDelegate { 12 | public enum ProgressStatus: Int { 13 | case Unknown, Loading, Progress, Completion 14 | } 15 | 16 | @IBInspectable public var lineWidth: CGFloat = 2.0 { 17 | didSet { 18 | progressLayer.lineWidth = lineWidth 19 | shapeLayer.lineWidth = lineWidth 20 | 21 | setProgressLayerPath() 22 | } 23 | } 24 | 25 | @IBInspectable public var strokeColor: UIColor = UIColor(red: 0.0, green: 122.0/255.0, blue: 1.0, alpha: 1.0){ 26 | didSet{ 27 | progressLayer.strokeColor = strokeColor.cgColor 28 | shapeLayer.strokeColor = strokeColor.cgColor 29 | progressLabel.textColor = strokeColor 30 | } 31 | } 32 | 33 | @IBInspectable public var fontSize: Float = 30 { 34 | didSet{ 35 | progressLabel.font = UIFont.systemFont(ofSize: CGFloat(fontSize)) 36 | } 37 | } 38 | 39 | public var hidesWhenCompleted: Bool = false 40 | public var hideAfterTime: TimeInterval = dhHidesWhenCompletedDelay 41 | public private(set) var status: ProgressStatus = .Unknown 42 | 43 | private var _progress: Float = 0.0 44 | public var progress: Float { 45 | get { 46 | return _progress 47 | } 48 | set(newProgress) { 49 | //Avoid calling excessively 50 | if (newProgress - _progress >= 0.01 || newProgress >= 100.0) { 51 | _progress = min(max(0, newProgress), 1) 52 | progressLayer.strokeEnd = CGFloat(_progress) 53 | 54 | if status == .Loading { 55 | progressLayer.removeAllAnimations() 56 | } else if(status == .Completion) { 57 | shapeLayer.strokeStart = 0 58 | shapeLayer.strokeEnd = 0 59 | shapeLayer.removeAllAnimations() 60 | } 61 | 62 | status = .Progress 63 | 64 | progressLabel.isHidden = false 65 | let progressInt: Int = Int(_progress * 100) 66 | progressLabel.text = "\(progressInt)" 67 | } 68 | } 69 | } 70 | 71 | private let progressLayer: CAShapeLayer! = CAShapeLayer() 72 | private let shapeLayer: CAShapeLayer! = CAShapeLayer() 73 | private let progressLabel: UILabel! = UILabel() 74 | 75 | private var completionBlock: Block? 76 | public override init(frame: CGRect) { 77 | super.init(frame: frame) 78 | initialize() 79 | } 80 | 81 | required public init?(coder aDecoder: NSCoder) { 82 | super.init(coder: aDecoder) 83 | initialize() 84 | } 85 | 86 | deinit{ 87 | NotificationCenter.default.removeObserver(self) 88 | } 89 | 90 | override public func layoutSubviews() { 91 | super.layoutSubviews() 92 | 93 | let width = self.bounds.width 94 | let height = self.bounds.height 95 | let square = min(width, height) 96 | 97 | let bounds = CGRect(origin: CGPoint(x:0, y:0), size: CGSize(width:square, height:square)) 98 | 99 | progressLayer.frame = CGRect(origin:CGPoint(x:0, y:0), size:CGSize(width:width, height:height)) 100 | setProgressLayerPath() 101 | 102 | shapeLayer.bounds = bounds 103 | shapeLayer.position = CGPoint(x:self.bounds.midX, y:self.bounds.midY) 104 | 105 | let labelSquare = sqrt(2) / 2.0 * square 106 | progressLabel.bounds = CGRect(origin:CGPoint(x:0, y:0), size:CGSize(width:labelSquare, height:labelSquare)) 107 | progressLabel.center = CGPoint(x:self.bounds.midX, y:self.bounds.midY) 108 | } 109 | 110 | public func startLoading() { 111 | if status == .Loading { 112 | return 113 | } 114 | 115 | status = .Loading 116 | 117 | progressLabel.isHidden = true 118 | progressLabel.text = "0" 119 | _progress = 0 120 | 121 | shapeLayer.strokeStart = 0 122 | shapeLayer.strokeEnd = 0 123 | shapeLayer.removeAllAnimations() 124 | 125 | self.isHidden = false 126 | progressLayer.strokeEnd = 0.0 127 | progressLayer.removeAllAnimations() 128 | 129 | let animation = CABasicAnimation(keyPath: "transform.rotation") 130 | animation.duration = 4.0 131 | animation.fromValue = 0.0 132 | animation.toValue = 2 * M_PI 133 | animation.repeatCount = Float.infinity 134 | progressLayer.add(animation, forKey: dhRingRotationAnimationKey) 135 | 136 | let totalDuration = 1.0 137 | let firstDuration = 2.0 * totalDuration / 3.0 138 | let secondDuration = totalDuration / 3.0 139 | 140 | let headAnimation = CABasicAnimation(keyPath: "strokeStart") 141 | headAnimation.duration = firstDuration 142 | headAnimation.fromValue = 0.0 143 | headAnimation.toValue = 0.25 144 | 145 | let tailAnimation = CABasicAnimation(keyPath: "strokeEnd") 146 | tailAnimation.duration = firstDuration 147 | tailAnimation.fromValue = 0.0 148 | tailAnimation.toValue = 1.0 149 | 150 | let endHeadAnimation = CABasicAnimation(keyPath: "strokeStart") 151 | endHeadAnimation.beginTime = firstDuration 152 | endHeadAnimation.duration = secondDuration 153 | endHeadAnimation.fromValue = 0.25 154 | endHeadAnimation.toValue = 1.0 155 | 156 | let endTailAnimation = CABasicAnimation(keyPath: "strokeEnd") 157 | endTailAnimation.beginTime = firstDuration 158 | endTailAnimation.duration = secondDuration 159 | endTailAnimation.fromValue = 1.0 160 | endTailAnimation.toValue = 1.0 161 | 162 | let animations = CAAnimationGroup() 163 | animations.duration = firstDuration + secondDuration 164 | animations.repeatCount = Float.infinity 165 | animations.animations = [headAnimation, tailAnimation, endHeadAnimation, endTailAnimation] 166 | progressLayer.add(animations, forKey: dhRingRotationAnimationKey) 167 | } 168 | 169 | public func completeLoading(success: Bool, completion: Block? = nil) { 170 | if status == .Completion { 171 | return 172 | } 173 | 174 | completionBlock = completion 175 | 176 | progressLabel.isHidden = true 177 | progressLayer.strokeEnd = 1.0 178 | progressLayer.removeAllAnimations() 179 | 180 | if success { 181 | setStrokeSuccessShapePath() 182 | self.strokeColor = UIColor.green 183 | } else { 184 | setStrokeFailureShapePath() 185 | self.strokeColor = UIColor.red 186 | } 187 | 188 | var strokeStart :CGFloat = 0.25 189 | var strokeEnd :CGFloat = 0.8 190 | var phase1Duration = 0.7 * dhCompletionAnimationDuration 191 | var phase2Duration = 0.3 * dhCompletionAnimationDuration 192 | var phase3Duration = 0.0 193 | 194 | if !success { 195 | let square = min(self.bounds.width, self.bounds.height) 196 | let point = errorJoinPoint() 197 | let increase = 1.0/3 * square - point.x 198 | let sum = 2.0/3 * square 199 | strokeStart = increase / (sum + increase) 200 | strokeEnd = (increase + sum/2) / (sum + increase) 201 | 202 | phase1Duration = 0.5 * dhCompletionAnimationDuration 203 | phase2Duration = 0.2 * dhCompletionAnimationDuration 204 | phase3Duration = 0.3 * dhCompletionAnimationDuration 205 | } 206 | 207 | shapeLayer.strokeEnd = 1.0 208 | shapeLayer.strokeStart = strokeStart 209 | let timeFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 210 | 211 | let headStartAnimation = CABasicAnimation(keyPath: "strokeStart") 212 | headStartAnimation.fromValue = 0.0 213 | headStartAnimation.toValue = 0.0 214 | headStartAnimation.duration = phase1Duration 215 | headStartAnimation.timingFunction = timeFunction 216 | 217 | let headEndAnimation = CABasicAnimation(keyPath: "strokeEnd") 218 | headEndAnimation.fromValue = 0.0 219 | headEndAnimation.toValue = strokeEnd 220 | headEndAnimation.duration = phase1Duration 221 | headEndAnimation.timingFunction = timeFunction 222 | 223 | let tailStartAnimation = CABasicAnimation(keyPath: "strokeStart") 224 | tailStartAnimation.fromValue = 0.0 225 | tailStartAnimation.toValue = strokeStart 226 | tailStartAnimation.beginTime = phase1Duration 227 | tailStartAnimation.duration = phase2Duration 228 | tailStartAnimation.timingFunction = timeFunction 229 | 230 | let tailEndAnimation = CABasicAnimation(keyPath: "strokeEnd") 231 | tailEndAnimation.fromValue = strokeEnd 232 | tailEndAnimation.toValue = success ? 1.0 : strokeEnd 233 | tailEndAnimation.beginTime = phase1Duration 234 | tailEndAnimation.duration = phase2Duration 235 | tailEndAnimation.timingFunction = timeFunction 236 | 237 | let extraAnimation = CABasicAnimation(keyPath: "strokeEnd") 238 | extraAnimation.fromValue = strokeEnd 239 | extraAnimation.toValue = 1.0 240 | extraAnimation.beginTime = phase1Duration + phase2Duration 241 | extraAnimation.duration = phase3Duration 242 | extraAnimation.timingFunction = timeFunction 243 | 244 | let groupAnimation = CAAnimationGroup() 245 | groupAnimation.animations = [headEndAnimation, headStartAnimation, tailStartAnimation, tailEndAnimation] 246 | if !success { 247 | groupAnimation.animations?.append(extraAnimation) 248 | } 249 | groupAnimation.duration = phase1Duration + phase2Duration + phase3Duration 250 | groupAnimation.delegate = self 251 | shapeLayer.add(groupAnimation, forKey: nil) 252 | } 253 | 254 | public func animationDidStop(_: CAAnimation, finished flag: Bool) { 255 | if hidesWhenCompleted { 256 | Timer.scheduledTimer(timeInterval: dhHidesWhenCompletedDelay, target: self, selector: #selector(DSActivityIndicator.hiddenLoadingView), userInfo: nil, repeats: false) 257 | } else { 258 | status = .Completion 259 | if completionBlock != nil { 260 | completionBlock!() 261 | } 262 | } 263 | } 264 | 265 | //MARK: - Private 266 | private func initialize() { 267 | //progressLabel 268 | progressLabel.font = UIFont.systemFont(ofSize: CGFloat(fontSize)) 269 | progressLabel.textColor = strokeColor 270 | progressLabel.textAlignment = .center 271 | progressLabel.adjustsFontSizeToFitWidth = true 272 | progressLabel.isHidden = true 273 | self.addSubview(progressLabel) 274 | 275 | //progressLayer 276 | progressLayer.strokeColor = strokeColor.cgColor 277 | progressLayer.fillColor = nil 278 | progressLayer.lineWidth = lineWidth 279 | self.layer.addSublayer(progressLayer) 280 | 281 | //shapeLayer 282 | shapeLayer.strokeColor = strokeColor.cgColor 283 | shapeLayer.fillColor = nil 284 | shapeLayer.lineWidth = lineWidth 285 | shapeLayer.lineCap = kCALineCapRound 286 | shapeLayer.lineJoin = kCALineJoinRound 287 | shapeLayer.strokeStart = 0.0 288 | shapeLayer.strokeEnd = 0.0 289 | self.layer.addSublayer(shapeLayer) 290 | 291 | NotificationCenter.default.addObserver(self, selector:#selector(DSActivityIndicator.resetAnimations), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) 292 | } 293 | 294 | private func setProgressLayerPath() { 295 | let center = CGPoint(x:self.bounds.midX, y:self.bounds.midY) 296 | let radius = (min(self.bounds.width, self.bounds.height) - progressLayer.lineWidth) / 2 297 | let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0.0), endAngle: CGFloat(2 * M_PI), clockwise: true) 298 | progressLayer.path = path.cgPath 299 | progressLayer.strokeStart = 0.0 300 | progressLayer.strokeEnd = 0.0 301 | } 302 | 303 | private func setStrokeSuccessShapePath() { 304 | let width = self.bounds.width 305 | let height = self.bounds.height 306 | let square = min(width, height) 307 | let b = square/2 308 | let oneTenth = square/10 309 | let xOffset = oneTenth 310 | let yOffset = 1.5 * oneTenth 311 | let ySpace = 3.2 * oneTenth 312 | let point = correctJoinPoint() 313 | 314 | 315 | let path = CGMutablePath() 316 | path.move(to: CGPoint(x:point.x, y:point.y)) 317 | path.addLine(to: CGPoint(x:point.x, y:point.y)) 318 | path.addLine(to: CGPoint(x: b - xOffset, y: b + yOffset)) 319 | path.addLine(to: CGPoint(x: 2 * b - xOffset + yOffset - ySpace, y:ySpace )) 320 | 321 | shapeLayer.path = path 322 | shapeLayer.cornerRadius = square/2 323 | shapeLayer.masksToBounds = true 324 | shapeLayer.strokeStart = 0.0 325 | shapeLayer.strokeEnd = 0.0 326 | } 327 | 328 | private func setStrokeFailureShapePath() { 329 | let width = self.bounds.width 330 | let height = self.bounds.height 331 | let square = min(width, height) 332 | let b = square/2 333 | let space = square/3 334 | let point = errorJoinPoint() 335 | 336 | 337 | let path = CGMutablePath() 338 | path.move(to: CGPoint(x:point.x, y:point.y)) 339 | path.addLine(to: CGPoint(x:2 * b - space, y: 2 * b - space)) 340 | path.move(to: CGPoint(x:2 * b - space, y: space)) 341 | path.addLine(to: CGPoint(x:space, y:2 * b - space)) 342 | 343 | shapeLayer.path = path 344 | shapeLayer.cornerRadius = square/2 345 | shapeLayer.masksToBounds = true 346 | shapeLayer.strokeStart = 0 347 | shapeLayer.strokeEnd = 0.0 348 | } 349 | 350 | private func correctJoinPoint() -> CGPoint { 351 | let r = min(self.bounds.width, self.bounds.height)/2 352 | let m = r/2 353 | let k = lineWidth/2 354 | 355 | let a: CGFloat = 2.0 356 | let b = -4 * r + 2 * m 357 | let c = (r - m) * (r - m) + 2 * r * k - k * k 358 | let x = (-b - sqrt(b * b - 4 * a * c))/(2 * a) 359 | let y = x + m 360 | 361 | return CGPoint(x:x, y:y) 362 | } 363 | 364 | private func errorJoinPoint() -> CGPoint { 365 | let r = min(self.bounds.width, self.bounds.height)/2 366 | let k = lineWidth/2 367 | 368 | let a: CGFloat = 2.0 369 | let b = -4 * r 370 | let c = r * r + 2 * r * k - k * k 371 | let x = (-b - sqrt(b * b - 4 * a * c))/(2 * a) 372 | 373 | return CGPoint(x:x, y:x) 374 | } 375 | 376 | @objc private func resetAnimations() { 377 | if status == .Loading { 378 | status = .Unknown 379 | progressLayer.removeAnimation(forKey: dhRingRotationAnimationKey) 380 | progressLayer.removeAnimation(forKey: dhRingStorkeAnimationKey) 381 | 382 | startLoading() 383 | } 384 | } 385 | 386 | @objc private func hiddenLoadingView() { 387 | status = .Completion 388 | self.isHidden = true 389 | 390 | if completionBlock != nil { 391 | completionBlock!() 392 | } 393 | } 394 | } 395 | --------------------------------------------------------------------------------