├── .swift-version ├── ShowMeThatStatus.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── panicztomek.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcuserdata │ └── panicztomek.xcuserdatad │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── ShowMeThatStatus.xcscheme └── project.pbxproj ├── ShowMeThatStatus ├── ShowMeThatStatus.h ├── Info.plist ├── SMTSConstants.swift ├── SMTSAction.swift ├── SMTSStyle.swift ├── SMTSTransitionManager.swift ├── SMTSViewController.xib ├── SMTSViewController.swift └── StatusIndicator.swift ├── ShowMeThatStatus.podspec ├── LICENSE └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 -------------------------------------------------------------------------------- /ShowMeThatStatus.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ShowMeThatStatus.xcodeproj/project.xcworkspace/xcuserdata/panicztomek.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noolis/ShowMeThatStatus/HEAD/ShowMeThatStatus.xcodeproj/project.xcworkspace/xcuserdata/panicztomek.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ShowMeThatStatus/ShowMeThatStatus.h: -------------------------------------------------------------------------------- 1 | // 2 | // ShowMeThatStatus.h 3 | // ShowMeThatStatus 4 | // 5 | // Created by Tomasz Kopycki on 21/04/16. 6 | // Copyright © 2016 Noolis. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ShowMeThatStatus. 12 | FOUNDATION_EXPORT double ShowMeThatStatusVersionNumber; 13 | 14 | //! Project version string for ShowMeThatStatus. 15 | FOUNDATION_EXPORT const unsigned char ShowMeThatStatusVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /ShowMeThatStatus.xcodeproj/xcuserdata/panicztomek.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ShowMeThatStatus.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3D9A69781CD0D86E00857568 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ShowMeThatStatus.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.platform = :ios 4 | s.ios.deployment_target = '9.0' 5 | s.name = "ShowMeThatStatus" 6 | s.summary = "This is a component written in Swift for iOS 9.0+ to display statuses of a process in a nicely animated manner." 7 | s.requires_arc = true 8 | 9 | s.version = "0.2.0" 10 | 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | 13 | s.author = { "Tomasz Kopycki" => "t.kopycki@noolis.co" } 14 | 15 | s.homepage = "https://github.com/wssj/ShowMeThatStatus" 16 | 17 | s.source = { :git => "https://github.com/wssj/ShowMeThatStatus.git", :tag => "#{s.version}"} 18 | 19 | s.framework = "UIKit" 20 | 21 | s.source_files = "ShowMeThatStatus/**/*.{swift}" 22 | 23 | s.resources = "ShowMeThatStatus/**/*.{png,jpeg,jpg,storyboard,xib}" 24 | 25 | end 26 | -------------------------------------------------------------------------------- /ShowMeThatStatus/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 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ShowMeThatStatus/SMTSConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMTSConstants.swift 3 | // ShowMeThatStatus 4 | // 5 | // Created by Tomasz Kopycki on 21/04/16. 6 | // Copyright © 2016 Noolis. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias StatusIndicatorBlock = () -> Void 12 | 13 | public enum SMTSProgressStatus { 14 | case unknown, loading, progress, failure, success, all 15 | } 16 | 17 | public enum SMTSActionType { 18 | case `default`, cancel 19 | } 20 | 21 | public struct SMTSConstants { 22 | 23 | static let tm = SMTSTransitionManager() 24 | 25 | public static let SMTSVCNibName = "SMTSViewController" 26 | public static let smtsStyle = SMTSStyle() 27 | 28 | public static let ringStrokeAnimationKey = "StatusIndicator.stroke" 29 | public static let ringRotationAnimationKey = "StatusIndicator.rotation" 30 | public static let completionAnimationDuration: TimeInterval = 0.3 31 | public static let hidesWhenCompletedDelay: TimeInterval = 0.5 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Noolis, http://www.noolis.co 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 | -------------------------------------------------------------------------------- /ShowMeThatStatus/SMTSAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMTSAction.swift 3 | // ShowMeThatStatus 4 | // 5 | // Created by Tomasz Kopycki on 21/04/16. 6 | // Copyright © 2016 Noolis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SMTSAction: NSObject { 12 | 13 | var title: String 14 | var actionBlock: (() -> ()) 15 | var visibleStates: [SMTSProgressStatus] 16 | var type: SMTSActionType 17 | 18 | var button: UIButton 19 | 20 | 21 | init(title: String, actionBlock: @escaping () -> (), visibleStates: [SMTSProgressStatus], 22 | type: SMTSActionType = .default) { 23 | 24 | self.title = title 25 | self.actionBlock = actionBlock 26 | self.visibleStates = visibleStates 27 | self.type = type 28 | 29 | button = UIButton() 30 | 31 | super.init() 32 | 33 | button.clipsToBounds = true 34 | button.addTarget(self, action: #selector(performActionBlock), 35 | for: .touchUpInside) 36 | button.setTitle(title, for: UIControlState()) 37 | 38 | button.titleLabel?.font = type == .default ? 39 | SMTSConstants.smtsStyle.defaultButtonFont : SMTSConstants.smtsStyle.cancelButtonFont 40 | button.setTitleColor(type == .default ? 41 | SMTSConstants.smtsStyle.defaultButtonTextColor : SMTSConstants.smtsStyle.cancelButtonTextColor, 42 | for: UIControlState()) 43 | button.backgroundColor = type == .default ? 44 | SMTSConstants.smtsStyle.defaultButtonBackgroundColor : 45 | SMTSConstants.smtsStyle.cancelButtonBackgroundColor 46 | button.layer.cornerRadius = SMTSConstants.smtsStyle.buttonsCornerRadius 47 | button.layer.borderWidth = 1.0 48 | button.layer.borderColor = type == .default ? 49 | SMTSConstants.smtsStyle.defaultButtonBorderColor : 50 | SMTSConstants.smtsStyle.cancelButtonBorderColor 51 | 52 | } 53 | 54 | func performActionBlock() { 55 | 56 | actionBlock() 57 | 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /ShowMeThatStatus/SMTSStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMTSStyle.swift 3 | // ShowMeThatStatus 4 | // 5 | // Created by Tomasz Kopycki on 21/04/16. 6 | // Copyright © 2016 Noolis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SMTSStyle: NSObject { 12 | 13 | //Colors 14 | open var backgroundColor = UIColor(red: 237.0/255.0, green: 237.0/255.0, 15 | blue: 237.0/255.0, alpha: 1.0) 16 | open var successColor = UIColor(red: 30.0/255.0, green: 205.0/255.0, 17 | blue: 151.0/255.0, alpha: 1.0) 18 | open var failureColor = UIColor(red: 194.0/255.0, green: 59.0/255.0, 19 | blue: 34.0/255.0, alpha: 1.0) 20 | open var progressColor = UIColor.gray 21 | open var defaultButtonTextColor = UIColor.white 22 | open var cancelButtonTextColor = UIColor.white 23 | open var defaultButtonBackgroundColor = UIColor(red: 162.0/255.0, green: 164.0/255.0, 24 | blue: 165.0/255.0, alpha: 1.0) 25 | open var cancelButtonBackgroundColor = UIColor(red: 162.0/255.0, green: 164.0/255.0, 26 | blue: 165.0/255.0, alpha: 1.0) 27 | 28 | //Borders 29 | open var backgroundBorderColor = UIColor.clear.cgColor 30 | open var defaultButtonBorderColor = UIColor.clear.cgColor 31 | open var cancelButtonBorderColor = UIColor.clear.cgColor 32 | 33 | //Fonts 34 | open var statusFont = UIFont.systemFont(ofSize: 17) 35 | open var progressFont = UIFont.systemFont(ofSize: 19) 36 | open var defaultButtonFont = UIFont.systemFont(ofSize: 15) 37 | open var cancelButtonFont = UIFont.boldSystemFont(ofSize: 15) 38 | 39 | //Corners 40 | open var buttonsCornerRadius: CGFloat = 4.0 41 | open var viewCornerRadius: CGFloat = 4.0 42 | 43 | //Size 44 | open var width = floor(UIScreen.main.bounds.size.width * 0.8) 45 | open var lineWidth: CGFloat = 6.0 46 | open var borderWidth: CGFloat = 1.0 47 | 48 | //Shadow 49 | open var shadowColor = UIColor.clear.cgColor 50 | open var shadowOpacity: Float = 0.5 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShowMeThatStatus 2 | 3 | View controller that can be used for presenting status changes to the user, for example during API calls. 4 | 5 | * It can be presented modally just like UIAlertView. 6 | 7 | * It has 4 different visible states - loading, progress (loading with completion percentage), success, failure. 8 | 9 | ## Requirements: ## 10 | 11 | - iOS 9.0+ 12 | - XCode 8.0+ 13 | - Swift 3.0+ 14 | 15 | (For Swift 2 compatibility use **swift2** branch) 16 | 17 | ## GIF example: ## 18 | 19 | ![](https://dl.dropboxusercontent.com/u/66515478/smts.gif) 20 | 21 | ## Usage: ## 22 | 23 | * To add it to your project best way is to use *Cocoapods*: 24 | 25 | ``` 26 | pod 'ShowMeThatStatus' 27 | ``` 28 | 29 | * Example usage in your project: 30 | 31 | ``` 32 | 33 | let vc = SMTSViewController(status: .loading, message: "Loading...") 34 | 35 | vc.addAction(withTitle: "Cancel", visibleForStates: [.progress, .loading], ofType: .cancel) { 36 | self.dismiss(animated: true, completion: nil) 37 | 38 | //Your action.. 39 | } 40 | 41 | vc.addAction(withTitle: "Retry", visibleForStates: [.failure]) { 42 | vc.changeStatus(to: .loading, withMessage: "Loading...") 43 | 44 | //Your action.. 45 | } 46 | 47 | vc.addAction(withTitle: "Done", visibleForStates: [.success]) { 48 | self.dismiss(animated: true, completion: nil) 49 | 50 | //Your action.. 51 | } 52 | 53 | present(vc, animated: true, completion: nil) 54 | ``` 55 | * This will present StatusVC centered and will be changing height of its view automatically with simple animation based on the amount of action buttons and length of status text. 56 | * When adding action you can determine controller's states that action is available for which hides/unhides specific buttons on status change. 57 | * There are 2 types of buttons *.Default* and *.Cancel* which can have different appearance based on settings available in *SMTSStyle.swift*. 58 | 59 | * It's possible to customise appearance in a following way (all customisable appearance parameters available in *SMTSStyle.swift*): 60 | 61 | 62 | ``` 63 | 64 | SMTSConstants.smtsStyle.backgroundColor = .blackColor() 65 | SMTSConstants.smtsStyle.cancelButtonFont = UIFont.systemFontOfSize(15) 66 | 67 | //etc... 68 | 69 | ``` 70 | -------------------------------------------------------------------------------- /ShowMeThatStatus.xcodeproj/xcuserdata/panicztomek.xcuserdatad/xcschemes/ShowMeThatStatus.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ShowMeThatStatus/SMTSTransitionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMTSTransitionManager.swift 3 | // ShowMeThatStatus 4 | // 5 | // Created by Tomasz Kopycki on 21/04/16. 6 | // Copyright © 2016 Noolis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SMTSTransitionManager: NSObject, 12 | UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate { 13 | 14 | var isPresenting = false 15 | 16 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 17 | 18 | let transitionContainer = transitionContext.containerView 19 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) 20 | let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) 21 | 22 | 23 | let duration = self.transitionDuration(using: transitionContext) 24 | 25 | let bounds = UIScreen.main.bounds 26 | let toFrame = toVC!.view.frame 27 | var endFrame = CGRect(x: (bounds.width - toFrame.width) / 2, 28 | y: (bounds.height - toFrame.height) / 2, 29 | width: toFrame.width, height: toFrame.height) 30 | 31 | if isPresenting { 32 | 33 | fromVC?.view.isUserInteractionEnabled = false 34 | 35 | transitionContainer.addSubview(fromVC!.view) 36 | transitionContainer.addSubview(toVC!.view) 37 | 38 | var startFrame = endFrame 39 | startFrame.origin.y += 1000 40 | 41 | toVC?.view.frame = startFrame 42 | 43 | UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.5, 44 | initialSpringVelocity: 0.8, options: [], animations: { 45 | 46 | fromVC?.view.tintAdjustmentMode = .dimmed 47 | toVC?.view.frame = endFrame 48 | 49 | }, completion: { (finished) in 50 | transitionContext.completeTransition(true) 51 | }) 52 | 53 | } else { 54 | 55 | toVC?.view.isUserInteractionEnabled = true 56 | 57 | transitionContainer.addSubview(toVC!.view) 58 | transitionContainer.addSubview(fromVC!.view) 59 | 60 | endFrame.origin.y += 1000 61 | 62 | UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.5, 63 | initialSpringVelocity: 0.8, options: [], animations: { 64 | 65 | toVC?.view.tintAdjustmentMode = .automatic 66 | fromVC?.view.frame = endFrame 67 | 68 | }, completion: { (finished) in 69 | transitionContext.completeTransition(true) 70 | UIApplication.shared.keyWindow!.addSubview(toVC!.view) 71 | }) 72 | } 73 | 74 | } 75 | 76 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 77 | return 0.5 78 | } 79 | 80 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 81 | 82 | isPresenting = true 83 | 84 | return self 85 | } 86 | 87 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 88 | 89 | isPresenting = false 90 | 91 | return self 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /ShowMeThatStatus/SMTSViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /ShowMeThatStatus/SMTSViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SMTSViewController.swift 3 | // ShowMeThatStatus 4 | // 5 | // Created by Tomasz Kopycki on 21/04/16. 6 | // Copyright © 2016 Noolis. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class SMTSViewController: UIViewController { 12 | 13 | fileprivate var statusMessage: String! 14 | fileprivate var actions = [SMTSAction]() 15 | fileprivate var status: SMTSProgressStatus = .unknown 16 | 17 | var style = SMTSConstants.smtsStyle 18 | 19 | //MARK: - Outlets 20 | 21 | @IBOutlet fileprivate weak var statusIndicator: StatusIndicator! 22 | @IBOutlet fileprivate weak var lblStatus: UILabel! 23 | @IBOutlet fileprivate weak var svActions: UIStackView! 24 | @IBOutlet fileprivate weak var constrStackViewHeight: NSLayoutConstraint! 25 | @IBOutlet fileprivate weak var vPlaceholder: UIView! 26 | 27 | //MARK: - Init 28 | 29 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 30 | 31 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 32 | 33 | transitioningDelegate = SMTSConstants.tm 34 | modalPresentationStyle = .custom 35 | } 36 | 37 | public required init?(coder aDecoder: NSCoder) { 38 | 39 | super.init(coder: aDecoder) 40 | } 41 | 42 | public convenience init(status: SMTSProgressStatus, message: String = "") { 43 | 44 | self.init(nibName: SMTSConstants.SMTSVCNibName, 45 | bundle: Bundle(for: SMTSViewController.self)) 46 | self.status = status 47 | self.statusMessage = message 48 | } 49 | 50 | public convenience init() { 51 | 52 | self.init(nibName: SMTSConstants.SMTSVCNibName, 53 | bundle: Bundle(for: SMTSViewController.self)) 54 | } 55 | 56 | //MARK: - View life cycle 57 | 58 | open override func viewDidLoad() { 59 | super.viewDidLoad() 60 | 61 | setAppearance() 62 | } 63 | 64 | open override func viewWillAppear(_ animated: Bool) { 65 | super.viewWillAppear(animated) 66 | 67 | for action in actions { 68 | 69 | action.button.isHidden = true 70 | if self.svActions != nil { 71 | self.svActions.addArrangedSubview(action.button) 72 | } 73 | 74 | } 75 | 76 | changeStatus(to: status, withMessage: statusMessage, progress: 0) 77 | } 78 | 79 | //MARK: - Updates 80 | 81 | open func changeStatus(to status: SMTSProgressStatus, withMessage message: String? = nil, 82 | progress: Float? = nil, 83 | didFinishAnimating: StatusIndicatorBlock? = nil) { 84 | 85 | self.status = status 86 | 87 | if message != nil { 88 | statusMessage = message 89 | lblStatus.text = message 90 | } 91 | 92 | if let progress = progress { 93 | statusIndicator.progress = progress 94 | } 95 | 96 | switch status { 97 | case .loading: 98 | statusIndicator.startLoading() 99 | statusIndicator.strokeColor = style.progressColor 100 | lblStatus.textColor = style.progressColor 101 | case .failure: 102 | statusIndicator.completeLoading(false, completion: didFinishAnimating) 103 | statusIndicator.strokeColor = style.failureColor 104 | lblStatus.textColor = style.failureColor 105 | case .success: 106 | statusIndicator.completeLoading(true, completion: didFinishAnimating) 107 | statusIndicator.strokeColor = style.successColor 108 | lblStatus.textColor = style.successColor 109 | default: break 110 | } 111 | 112 | updateStackView() 113 | updateSize() 114 | } 115 | 116 | 117 | open func addAction(withTitle title: String, 118 | visibleForStates states: [SMTSProgressStatus] = [.all], 119 | ofType type: SMTSActionType = .default, 120 | actionBlock: @escaping () -> ()) { 121 | 122 | let action = SMTSAction(title: title, 123 | actionBlock: actionBlock, 124 | visibleStates: states, 125 | type: type) 126 | actions.append(action) 127 | } 128 | 129 | fileprivate func updateSize() { 130 | 131 | let size: CGSize = statusMessage 132 | .boundingRect(with: CGSize(width: style.width - 20, height: 2000), 133 | options:NSStringDrawingOptions.usesLineFragmentOrigin, 134 | attributes: [NSFontAttributeName: style.statusFont], 135 | context: nil).size as CGSize 136 | 137 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, 138 | initialSpringVelocity: 0.8, options: [], animations: { 139 | 140 | self.view.frame.size = CGSize(width: self.style.width, 141 | height: 40 + 90 + size.height + self.constrStackViewHeight.constant) 142 | 143 | }, completion: nil) 144 | } 145 | 146 | fileprivate func updateStackView() { 147 | 148 | let shown = actions.filter({return $0.visibleStates.contains(self.status) || 149 | $0.visibleStates.contains(.all)}).count 150 | 151 | switch shown { 152 | case 0, 1: 153 | constrStackViewHeight.constant = 45 154 | default: 155 | constrStackViewHeight.constant = CGFloat(shown * 50 - 5) 156 | } 157 | 158 | DispatchQueue.main.async { 159 | UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, 160 | initialSpringVelocity: 0.8, options: [], animations: { 161 | self.view.layoutIfNeeded() 162 | 163 | self.vPlaceholder.isHidden = shown != 0 164 | 165 | for action in self.actions { 166 | action.button.isHidden = !(action.visibleStates.contains(self.status) || 167 | action.visibleStates.contains(.all)) 168 | } 169 | }, completion: { finished in 170 | 171 | for action in self.actions { 172 | action.button.isHidden = !(action.visibleStates.contains(self.status) || 173 | action.visibleStates.contains(.all)) 174 | } 175 | }) 176 | } 177 | 178 | } 179 | 180 | 181 | //MARK: - Appearance 182 | 183 | fileprivate func setAppearance() { 184 | 185 | lblStatus.font = style.statusFont 186 | statusIndicator.strokeColor = style.progressColor 187 | statusIndicator.lineWidth = style.lineWidth 188 | lblStatus.textColor = style.progressColor 189 | view.backgroundColor = style.backgroundColor 190 | view.layer.borderColor = style.backgroundBorderColor 191 | view.layer.borderWidth = style.borderWidth 192 | view.layer.cornerRadius = style.viewCornerRadius 193 | 194 | view.layer.shadowColor = style.shadowColor 195 | view.layer.shadowRadius = 5.0 196 | view.layer.shadowOpacity = style.shadowOpacity 197 | 198 | svActions.alignment = .fill 199 | svActions.distribution = .fillEqually 200 | svActions.axis = .vertical 201 | svActions.spacing = 5 202 | svActions.translatesAutoresizingMaskIntoConstraints = false 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /ShowMeThatStatus.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3D9A697D1CD0D86E00857568 /* ShowMeThatStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D9A697C1CD0D86E00857568 /* ShowMeThatStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 3D9A698B1CD0D8A200857568 /* SMTSAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9A69841CD0D8A200857568 /* SMTSAction.swift */; }; 12 | 3D9A698C1CD0D8A200857568 /* SMTSConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9A69851CD0D8A200857568 /* SMTSConstants.swift */; }; 13 | 3D9A698D1CD0D8A200857568 /* SMTSStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9A69861CD0D8A200857568 /* SMTSStyle.swift */; }; 14 | 3D9A698E1CD0D8A200857568 /* SMTSTransitionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9A69871CD0D8A200857568 /* SMTSTransitionManager.swift */; }; 15 | 3D9A698F1CD0D8A200857568 /* SMTSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9A69881CD0D8A200857568 /* SMTSViewController.swift */; }; 16 | 3D9A69901CD0D8A200857568 /* SMTSViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3D9A69891CD0D8A200857568 /* SMTSViewController.xib */; }; 17 | 3D9A69911CD0D8A200857568 /* StatusIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9A698A1CD0D8A200857568 /* StatusIndicator.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 3D9A69791CD0D86E00857568 /* ShowMeThatStatus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ShowMeThatStatus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 3D9A697C1CD0D86E00857568 /* ShowMeThatStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShowMeThatStatus.h; sourceTree = ""; }; 23 | 3D9A697E1CD0D86F00857568 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | 3D9A69841CD0D8A200857568 /* SMTSAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMTSAction.swift; sourceTree = ""; }; 25 | 3D9A69851CD0D8A200857568 /* SMTSConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMTSConstants.swift; sourceTree = ""; }; 26 | 3D9A69861CD0D8A200857568 /* SMTSStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMTSStyle.swift; sourceTree = ""; }; 27 | 3D9A69871CD0D8A200857568 /* SMTSTransitionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMTSTransitionManager.swift; sourceTree = ""; }; 28 | 3D9A69881CD0D8A200857568 /* SMTSViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMTSViewController.swift; sourceTree = ""; }; 29 | 3D9A69891CD0D8A200857568 /* SMTSViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SMTSViewController.xib; sourceTree = ""; }; 30 | 3D9A698A1CD0D8A200857568 /* StatusIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusIndicator.swift; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 3D9A69751CD0D86E00857568 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 3D9A696F1CD0D86E00857568 = { 45 | isa = PBXGroup; 46 | children = ( 47 | 3D9A697B1CD0D86E00857568 /* ShowMeThatStatus */, 48 | 3D9A697A1CD0D86E00857568 /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 3D9A697A1CD0D86E00857568 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 3D9A69791CD0D86E00857568 /* ShowMeThatStatus.framework */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 3D9A697B1CD0D86E00857568 /* ShowMeThatStatus */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 3D9A69841CD0D8A200857568 /* SMTSAction.swift */, 64 | 3D9A69851CD0D8A200857568 /* SMTSConstants.swift */, 65 | 3D9A69861CD0D8A200857568 /* SMTSStyle.swift */, 66 | 3D9A69871CD0D8A200857568 /* SMTSTransitionManager.swift */, 67 | 3D9A69881CD0D8A200857568 /* SMTSViewController.swift */, 68 | 3D9A69891CD0D8A200857568 /* SMTSViewController.xib */, 69 | 3D9A698A1CD0D8A200857568 /* StatusIndicator.swift */, 70 | 3D9A697C1CD0D86E00857568 /* ShowMeThatStatus.h */, 71 | 3D9A697E1CD0D86F00857568 /* Info.plist */, 72 | ); 73 | path = ShowMeThatStatus; 74 | sourceTree = ""; 75 | }; 76 | /* End PBXGroup section */ 77 | 78 | /* Begin PBXHeadersBuildPhase section */ 79 | 3D9A69761CD0D86E00857568 /* Headers */ = { 80 | isa = PBXHeadersBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | 3D9A697D1CD0D86E00857568 /* ShowMeThatStatus.h in Headers */, 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | /* End PBXHeadersBuildPhase section */ 88 | 89 | /* Begin PBXNativeTarget section */ 90 | 3D9A69781CD0D86E00857568 /* ShowMeThatStatus */ = { 91 | isa = PBXNativeTarget; 92 | buildConfigurationList = 3D9A69811CD0D86F00857568 /* Build configuration list for PBXNativeTarget "ShowMeThatStatus" */; 93 | buildPhases = ( 94 | 3D9A69741CD0D86E00857568 /* Sources */, 95 | 3D9A69751CD0D86E00857568 /* Frameworks */, 96 | 3D9A69761CD0D86E00857568 /* Headers */, 97 | 3D9A69771CD0D86E00857568 /* Resources */, 98 | ); 99 | buildRules = ( 100 | ); 101 | dependencies = ( 102 | ); 103 | name = ShowMeThatStatus; 104 | productName = ShowMeThatStatus; 105 | productReference = 3D9A69791CD0D86E00857568 /* ShowMeThatStatus.framework */; 106 | productType = "com.apple.product-type.framework"; 107 | }; 108 | /* End PBXNativeTarget section */ 109 | 110 | /* Begin PBXProject section */ 111 | 3D9A69701CD0D86E00857568 /* Project object */ = { 112 | isa = PBXProject; 113 | attributes = { 114 | LastUpgradeCheck = 0800; 115 | ORGANIZATIONNAME = Noolis; 116 | TargetAttributes = { 117 | 3D9A69781CD0D86E00857568 = { 118 | CreatedOnToolsVersion = 7.3; 119 | LastSwiftMigration = 0800; 120 | }; 121 | }; 122 | }; 123 | buildConfigurationList = 3D9A69731CD0D86E00857568 /* Build configuration list for PBXProject "ShowMeThatStatus" */; 124 | compatibilityVersion = "Xcode 3.2"; 125 | developmentRegion = English; 126 | hasScannedForEncodings = 0; 127 | knownRegions = ( 128 | en, 129 | ); 130 | mainGroup = 3D9A696F1CD0D86E00857568; 131 | productRefGroup = 3D9A697A1CD0D86E00857568 /* Products */; 132 | projectDirPath = ""; 133 | projectRoot = ""; 134 | targets = ( 135 | 3D9A69781CD0D86E00857568 /* ShowMeThatStatus */, 136 | ); 137 | }; 138 | /* End PBXProject section */ 139 | 140 | /* Begin PBXResourcesBuildPhase section */ 141 | 3D9A69771CD0D86E00857568 /* Resources */ = { 142 | isa = PBXResourcesBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | 3D9A69901CD0D8A200857568 /* SMTSViewController.xib in Resources */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXResourcesBuildPhase section */ 150 | 151 | /* Begin PBXSourcesBuildPhase section */ 152 | 3D9A69741CD0D86E00857568 /* Sources */ = { 153 | isa = PBXSourcesBuildPhase; 154 | buildActionMask = 2147483647; 155 | files = ( 156 | 3D9A698C1CD0D8A200857568 /* SMTSConstants.swift in Sources */, 157 | 3D9A698D1CD0D8A200857568 /* SMTSStyle.swift in Sources */, 158 | 3D9A698B1CD0D8A200857568 /* SMTSAction.swift in Sources */, 159 | 3D9A69911CD0D8A200857568 /* StatusIndicator.swift in Sources */, 160 | 3D9A698E1CD0D8A200857568 /* SMTSTransitionManager.swift in Sources */, 161 | 3D9A698F1CD0D8A200857568 /* SMTSViewController.swift in Sources */, 162 | ); 163 | runOnlyForDeploymentPostprocessing = 0; 164 | }; 165 | /* End PBXSourcesBuildPhase section */ 166 | 167 | /* Begin XCBuildConfiguration section */ 168 | 3D9A697F1CD0D86F00857568 /* Debug */ = { 169 | isa = XCBuildConfiguration; 170 | buildSettings = { 171 | ALWAYS_SEARCH_USER_PATHS = NO; 172 | CLANG_ANALYZER_NONNULL = YES; 173 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 174 | CLANG_CXX_LIBRARY = "libc++"; 175 | CLANG_ENABLE_MODULES = YES; 176 | CLANG_ENABLE_OBJC_ARC = YES; 177 | CLANG_WARN_BOOL_CONVERSION = YES; 178 | CLANG_WARN_CONSTANT_CONVERSION = YES; 179 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 180 | CLANG_WARN_EMPTY_BODY = YES; 181 | CLANG_WARN_ENUM_CONVERSION = YES; 182 | CLANG_WARN_INFINITE_RECURSION = YES; 183 | CLANG_WARN_INT_CONVERSION = YES; 184 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 185 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 186 | CLANG_WARN_UNREACHABLE_CODE = YES; 187 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 188 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 189 | COPY_PHASE_STRIP = NO; 190 | CURRENT_PROJECT_VERSION = 1; 191 | DEBUG_INFORMATION_FORMAT = dwarf; 192 | ENABLE_STRICT_OBJC_MSGSEND = YES; 193 | ENABLE_TESTABILITY = YES; 194 | GCC_C_LANGUAGE_STANDARD = gnu99; 195 | GCC_DYNAMIC_NO_PIC = NO; 196 | GCC_NO_COMMON_BLOCKS = YES; 197 | GCC_OPTIMIZATION_LEVEL = 0; 198 | GCC_PREPROCESSOR_DEFINITIONS = ( 199 | "DEBUG=1", 200 | "$(inherited)", 201 | ); 202 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 203 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 204 | GCC_WARN_UNDECLARED_SELECTOR = YES; 205 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 206 | GCC_WARN_UNUSED_FUNCTION = YES; 207 | GCC_WARN_UNUSED_VARIABLE = YES; 208 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 209 | MTL_ENABLE_DEBUG_INFO = YES; 210 | ONLY_ACTIVE_ARCH = YES; 211 | SDKROOT = iphoneos; 212 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 213 | TARGETED_DEVICE_FAMILY = "1,2"; 214 | VERSIONING_SYSTEM = "apple-generic"; 215 | VERSION_INFO_PREFIX = ""; 216 | }; 217 | name = Debug; 218 | }; 219 | 3D9A69801CD0D86F00857568 /* Release */ = { 220 | isa = XCBuildConfiguration; 221 | buildSettings = { 222 | ALWAYS_SEARCH_USER_PATHS = NO; 223 | CLANG_ANALYZER_NONNULL = YES; 224 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 225 | CLANG_CXX_LIBRARY = "libc++"; 226 | CLANG_ENABLE_MODULES = YES; 227 | CLANG_ENABLE_OBJC_ARC = YES; 228 | CLANG_WARN_BOOL_CONVERSION = YES; 229 | CLANG_WARN_CONSTANT_CONVERSION = YES; 230 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 231 | CLANG_WARN_EMPTY_BODY = YES; 232 | CLANG_WARN_ENUM_CONVERSION = YES; 233 | CLANG_WARN_INFINITE_RECURSION = YES; 234 | CLANG_WARN_INT_CONVERSION = YES; 235 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 236 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 237 | CLANG_WARN_UNREACHABLE_CODE = YES; 238 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 239 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 240 | COPY_PHASE_STRIP = NO; 241 | CURRENT_PROJECT_VERSION = 1; 242 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 243 | ENABLE_NS_ASSERTIONS = NO; 244 | ENABLE_STRICT_OBJC_MSGSEND = YES; 245 | GCC_C_LANGUAGE_STANDARD = gnu99; 246 | GCC_NO_COMMON_BLOCKS = YES; 247 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 248 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 249 | GCC_WARN_UNDECLARED_SELECTOR = YES; 250 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 251 | GCC_WARN_UNUSED_FUNCTION = YES; 252 | GCC_WARN_UNUSED_VARIABLE = YES; 253 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 254 | MTL_ENABLE_DEBUG_INFO = NO; 255 | SDKROOT = iphoneos; 256 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 257 | TARGETED_DEVICE_FAMILY = "1,2"; 258 | VALIDATE_PRODUCT = YES; 259 | VERSIONING_SYSTEM = "apple-generic"; 260 | VERSION_INFO_PREFIX = ""; 261 | }; 262 | name = Release; 263 | }; 264 | 3D9A69821CD0D86F00857568 /* Debug */ = { 265 | isa = XCBuildConfiguration; 266 | buildSettings = { 267 | CLANG_ENABLE_MODULES = YES; 268 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 269 | DEFINES_MODULE = YES; 270 | DYLIB_COMPATIBILITY_VERSION = 1; 271 | DYLIB_CURRENT_VERSION = 1; 272 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 273 | INFOPLIST_FILE = ShowMeThatStatus/Info.plist; 274 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 275 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 276 | PRODUCT_BUNDLE_IDENTIFIER = co.noolis.ShowMeThatStatus; 277 | PRODUCT_NAME = "$(TARGET_NAME)"; 278 | SKIP_INSTALL = YES; 279 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 280 | SWIFT_VERSION = 3.0; 281 | }; 282 | name = Debug; 283 | }; 284 | 3D9A69831CD0D86F00857568 /* Release */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | CLANG_ENABLE_MODULES = YES; 288 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 289 | DEFINES_MODULE = YES; 290 | DYLIB_COMPATIBILITY_VERSION = 1; 291 | DYLIB_CURRENT_VERSION = 1; 292 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 293 | INFOPLIST_FILE = ShowMeThatStatus/Info.plist; 294 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 295 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 296 | PRODUCT_BUNDLE_IDENTIFIER = co.noolis.ShowMeThatStatus; 297 | PRODUCT_NAME = "$(TARGET_NAME)"; 298 | SKIP_INSTALL = YES; 299 | SWIFT_VERSION = 3.0; 300 | }; 301 | name = Release; 302 | }; 303 | /* End XCBuildConfiguration section */ 304 | 305 | /* Begin XCConfigurationList section */ 306 | 3D9A69731CD0D86E00857568 /* Build configuration list for PBXProject "ShowMeThatStatus" */ = { 307 | isa = XCConfigurationList; 308 | buildConfigurations = ( 309 | 3D9A697F1CD0D86F00857568 /* Debug */, 310 | 3D9A69801CD0D86F00857568 /* Release */, 311 | ); 312 | defaultConfigurationIsVisible = 0; 313 | defaultConfigurationName = Release; 314 | }; 315 | 3D9A69811CD0D86F00857568 /* Build configuration list for PBXNativeTarget "ShowMeThatStatus" */ = { 316 | isa = XCConfigurationList; 317 | buildConfigurations = ( 318 | 3D9A69821CD0D86F00857568 /* Debug */, 319 | 3D9A69831CD0D86F00857568 /* Release */, 320 | ); 321 | defaultConfigurationIsVisible = 0; 322 | defaultConfigurationName = Release; 323 | }; 324 | /* End XCConfigurationList section */ 325 | }; 326 | rootObject = 3D9A69701CD0D86E00857568 /* Project object */; 327 | } 328 | -------------------------------------------------------------------------------- /ShowMeThatStatus/StatusIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusIndicator.swift 3 | // 4 | // Originally created by Abdul Karim 5 | // Copyright © 2015 dhlabs. All rights reserved. 6 | // 7 | // Minor modifications to better suit ShowMeThatStatus 8 | 9 | 10 | import UIKit 11 | 12 | open class StatusIndicator: UIView, CAAnimationDelegate { 13 | 14 | @IBInspectable open var lineWidth: CGFloat = SMTSConstants.smtsStyle.lineWidth { 15 | didSet { 16 | progressLayer.lineWidth = lineWidth 17 | shapeLayer.lineWidth = lineWidth 18 | 19 | setProgressLayerPath() 20 | } 21 | } 22 | 23 | @IBInspectable open var strokeColor: UIColor = SMTSConstants.smtsStyle.progressColor { 24 | didSet{ 25 | progressLayer.strokeColor = strokeColor.cgColor 26 | shapeLayer.strokeColor = strokeColor.cgColor 27 | progressLabel.textColor = strokeColor 28 | } 29 | } 30 | 31 | @IBInspectable open var font: UIFont = SMTSConstants.smtsStyle.progressFont { 32 | didSet{ 33 | progressLabel.font = font 34 | } 35 | } 36 | 37 | open var hidesWhenCompleted: Bool = false 38 | open var hideAfterTime: TimeInterval = SMTSConstants.hidesWhenCompletedDelay 39 | var status: SMTSProgressStatus = .unknown 40 | 41 | fileprivate var _progress: Float = 0.0 42 | open var progress: Float { 43 | get { 44 | return _progress 45 | } 46 | set(newProgress) { 47 | //Avoid calling excessively 48 | if (newProgress - _progress >= 0.01 || newProgress >= 100.0) { 49 | _progress = min(max(0, newProgress), 1) 50 | progressLayer.strokeEnd = CGFloat(_progress) 51 | 52 | if status == .loading { 53 | progressLayer.removeAllAnimations() 54 | } else if(status == .failure || status == .success) { 55 | shapeLayer.strokeStart = 0 56 | shapeLayer.strokeEnd = 0 57 | shapeLayer.removeAllAnimations() 58 | } 59 | 60 | status = .progress 61 | 62 | progressLabel.isHidden = false 63 | let progressInt: Int = Int(_progress * 100) 64 | progressLabel.text = "\(progressInt)" 65 | } 66 | } 67 | } 68 | 69 | fileprivate let progressLayer: CAShapeLayer! = CAShapeLayer() 70 | fileprivate let shapeLayer: CAShapeLayer! = CAShapeLayer() 71 | fileprivate let progressLabel: UILabel! = UILabel() 72 | 73 | fileprivate var completionBlock: StatusIndicatorBlock? 74 | public override init(frame: CGRect) { 75 | super.init(frame: frame) 76 | initialize() 77 | } 78 | 79 | required public init?(coder aDecoder: NSCoder) { 80 | super.init(coder: aDecoder) 81 | initialize() 82 | } 83 | 84 | deinit{ 85 | NotificationCenter.default.removeObserver(self) 86 | } 87 | 88 | override open func layoutSubviews() { 89 | super.layoutSubviews() 90 | 91 | let width = self.bounds.width 92 | let height = self.bounds.height 93 | let square = min(width, height) 94 | 95 | let bounds = CGRect(x: 0, y: 0, width: square, height: square) 96 | 97 | progressLayer.frame = CGRect(x: 0, y: 0, width: width, height: height) 98 | setProgressLayerPath() 99 | 100 | shapeLayer.bounds = bounds 101 | shapeLayer.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY) 102 | 103 | let labelSquare = sqrt(2) / 2.0 * square 104 | progressLabel.bounds = CGRect(x: 0, y: 0, width: labelSquare, height: labelSquare) 105 | progressLabel.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY) 106 | } 107 | 108 | //MARK: - Public 109 | open func startLoading() { 110 | if status == .loading { 111 | return 112 | } 113 | 114 | status = .loading 115 | 116 | progressLabel.isHidden = true 117 | progressLabel.text = "0" 118 | _progress = 0 119 | 120 | shapeLayer.strokeStart = 0 121 | shapeLayer.strokeEnd = 0 122 | shapeLayer.removeAllAnimations() 123 | 124 | self.isHidden = false 125 | progressLayer.strokeEnd = 0.0 126 | progressLayer.removeAllAnimations() 127 | 128 | let animation = CABasicAnimation(keyPath: "transform.rotation") 129 | animation.duration = 4.0 130 | animation.fromValue = 0.0 131 | animation.toValue = 2 * M_PI 132 | animation.repeatCount = Float.infinity 133 | progressLayer.add(animation, forKey: SMTSConstants.ringRotationAnimationKey) 134 | 135 | let totalDuration = 1.0 136 | let firstDuration = 2.0 * totalDuration / 3.0 137 | let secondDuration = totalDuration / 3.0 138 | 139 | let headAnimation = CABasicAnimation(keyPath: "strokeStart") 140 | headAnimation.duration = firstDuration 141 | headAnimation.fromValue = 0.0 142 | headAnimation.toValue = 0.25 143 | 144 | let tailAnimation = CABasicAnimation(keyPath: "strokeEnd") 145 | tailAnimation.duration = firstDuration 146 | tailAnimation.fromValue = 0.0 147 | tailAnimation.toValue = 1.0 148 | 149 | let endHeadAnimation = CABasicAnimation(keyPath: "strokeStart") 150 | endHeadAnimation.beginTime = firstDuration 151 | endHeadAnimation.duration = secondDuration 152 | endHeadAnimation.fromValue = 0.25 153 | endHeadAnimation.toValue = 1.0 154 | 155 | let endTailAnimation = CABasicAnimation(keyPath: "strokeEnd") 156 | endTailAnimation.beginTime = firstDuration 157 | endTailAnimation.duration = secondDuration 158 | endTailAnimation.fromValue = 1.0 159 | endTailAnimation.toValue = 1.0 160 | 161 | let animations = CAAnimationGroup() 162 | animations.duration = firstDuration + secondDuration 163 | animations.repeatCount = Float.infinity 164 | animations.animations = [headAnimation, tailAnimation, endHeadAnimation, endTailAnimation] 165 | progressLayer.add(animations, forKey: SMTSConstants.ringRotationAnimationKey) 166 | } 167 | 168 | open func completeLoading(_ success: Bool, completion: StatusIndicatorBlock? = nil) { 169 | if status == .success || status == .failure { 170 | return 171 | } 172 | 173 | completionBlock = completion 174 | 175 | progressLabel.isHidden = true 176 | progressLayer.strokeEnd = 1.0 177 | progressLayer.removeAllAnimations() 178 | 179 | if success { 180 | setStrokeSuccessShapePath() 181 | } else { 182 | setStrokeFailureShapePath() 183 | } 184 | 185 | var strokeStart :CGFloat = 0.25 186 | var strokeEnd :CGFloat = 0.8 187 | var phase1Duration = 0.7 * SMTSConstants.completionAnimationDuration 188 | var phase2Duration = 0.3 * SMTSConstants.completionAnimationDuration 189 | var phase3Duration = 0.0 190 | 191 | if !success { 192 | let square = min(self.bounds.width, self.bounds.height) 193 | let point = errorJoinPoint() 194 | let increase = 1.0/3 * square - point.x 195 | let sum = 2.0/3 * square 196 | strokeStart = increase / (sum + increase) 197 | strokeEnd = (increase + sum/2) / (sum + increase) 198 | 199 | phase1Duration = 0.5 * SMTSConstants.completionAnimationDuration 200 | phase2Duration = 0.2 * SMTSConstants.completionAnimationDuration 201 | phase3Duration = 0.3 * SMTSConstants.completionAnimationDuration 202 | } 203 | 204 | shapeLayer.strokeEnd = 1.0 205 | shapeLayer.strokeStart = strokeStart 206 | let timeFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) 207 | 208 | let headStartAnimation = CABasicAnimation(keyPath: "strokeStart") 209 | headStartAnimation.fromValue = 0.0 210 | headStartAnimation.toValue = 0.0 211 | headStartAnimation.duration = phase1Duration 212 | headStartAnimation.timingFunction = timeFunction 213 | 214 | let headEndAnimation = CABasicAnimation(keyPath: "strokeEnd") 215 | headEndAnimation.fromValue = 0.0 216 | headEndAnimation.toValue = strokeEnd 217 | headEndAnimation.duration = phase1Duration 218 | headEndAnimation.timingFunction = timeFunction 219 | 220 | let tailStartAnimation = CABasicAnimation(keyPath: "strokeStart") 221 | tailStartAnimation.fromValue = 0.0 222 | tailStartAnimation.toValue = strokeStart 223 | tailStartAnimation.beginTime = phase1Duration 224 | tailStartAnimation.duration = phase2Duration 225 | tailStartAnimation.timingFunction = timeFunction 226 | 227 | let tailEndAnimation = CABasicAnimation(keyPath: "strokeEnd") 228 | tailEndAnimation.fromValue = strokeEnd 229 | tailEndAnimation.toValue = success ? 1.0 : strokeEnd 230 | tailEndAnimation.beginTime = phase1Duration 231 | tailEndAnimation.duration = phase2Duration 232 | tailEndAnimation.timingFunction = timeFunction 233 | 234 | let extraAnimation = CABasicAnimation(keyPath: "strokeEnd") 235 | extraAnimation.fromValue = strokeEnd 236 | extraAnimation.toValue = 1.0 237 | extraAnimation.beginTime = phase1Duration + phase2Duration 238 | extraAnimation.duration = phase3Duration 239 | extraAnimation.timingFunction = timeFunction 240 | 241 | let groupAnimation = CAAnimationGroup() 242 | groupAnimation.animations = [headEndAnimation, headStartAnimation, tailStartAnimation, tailEndAnimation] 243 | if !success { 244 | groupAnimation.animations?.append(extraAnimation) 245 | } 246 | groupAnimation.duration = phase1Duration + phase2Duration + phase3Duration 247 | groupAnimation.delegate = self 248 | shapeLayer.add(groupAnimation, forKey: nil) 249 | } 250 | 251 | //MARK: CAAnimationDelegate 252 | 253 | open func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { 254 | if hidesWhenCompleted { 255 | Timer.scheduledTimer(timeInterval: SMTSConstants.hidesWhenCompletedDelay, 256 | target: self, 257 | selector: #selector(StatusIndicator.hiddenLoadingView), 258 | userInfo: nil, 259 | repeats: false) 260 | } else { 261 | status = .success 262 | if completionBlock != nil { 263 | completionBlock!() 264 | } 265 | } 266 | } 267 | 268 | //MARK: - Private 269 | fileprivate func initialize() { 270 | //progressLabel 271 | progressLabel.font = font 272 | progressLabel.textColor = strokeColor 273 | progressLabel.textAlignment = .center 274 | progressLabel.adjustsFontSizeToFitWidth = true 275 | progressLabel.isHidden = true 276 | self.addSubview(progressLabel) 277 | 278 | //progressLayer 279 | progressLayer.strokeColor = strokeColor.cgColor 280 | progressLayer.fillColor = nil 281 | progressLayer.lineWidth = lineWidth 282 | self.layer.addSublayer(progressLayer) 283 | 284 | //shapeLayer 285 | shapeLayer.strokeColor = strokeColor.cgColor 286 | shapeLayer.fillColor = nil 287 | shapeLayer.lineWidth = lineWidth 288 | shapeLayer.lineCap = kCALineCapRound 289 | shapeLayer.lineJoin = kCALineJoinRound 290 | shapeLayer.strokeStart = 0.0 291 | shapeLayer.strokeEnd = 0.0 292 | self.layer.addSublayer(shapeLayer) 293 | 294 | NotificationCenter.default 295 | .addObserver(self, 296 | selector:#selector(StatusIndicator.resetAnimations), 297 | name: NSNotification.Name.UIApplicationDidBecomeActive, 298 | object: nil) 299 | } 300 | 301 | fileprivate func setProgressLayerPath() { 302 | let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY) 303 | let radius = (min(self.bounds.width, self.bounds.height) - progressLayer.lineWidth) / 2 304 | let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0.0), endAngle: CGFloat(2 * M_PI), clockwise: true) 305 | progressLayer.path = path.cgPath 306 | progressLayer.strokeStart = 0.0 307 | progressLayer.strokeEnd = 0.0 308 | } 309 | 310 | fileprivate func setStrokeSuccessShapePath() { 311 | let width = self.bounds.width 312 | let height = self.bounds.height 313 | let square = min(width, height) 314 | let b = square/2 315 | let oneTenth = square/10 316 | let xOffset = oneTenth 317 | let yOffset = 1.5 * oneTenth 318 | let ySpace = 3.2 * oneTenth 319 | let point = correctJoinPoint() 320 | 321 | //y1 = x1 + xOffset + yOffset 322 | //y2 = -x2 + 2b - xOffset + yOffset 323 | let path = CGMutablePath() 324 | path.move(to: point) 325 | path.addLine(to: CGPoint(x: b - xOffset, y: b + yOffset)) 326 | path.addLine(to: CGPoint(x: 2 * b - xOffset + yOffset - ySpace, y: ySpace)) 327 | 328 | shapeLayer.path = path 329 | shapeLayer.cornerRadius = square/2 330 | shapeLayer.masksToBounds = true 331 | shapeLayer.strokeStart = 0.0 332 | shapeLayer.strokeEnd = 0.0 333 | } 334 | 335 | fileprivate func setStrokeFailureShapePath() { 336 | let width = self.bounds.width 337 | let height = self.bounds.height 338 | let square = min(width, height) 339 | let b = square/2 340 | let space = square/3 341 | let point = errorJoinPoint() 342 | 343 | //y1 = x1 344 | //y2 = -x2 + 2b 345 | let path = CGMutablePath() 346 | path.move(to: point) 347 | path.addLine(to: CGPoint(x: 2 * b - space, y: 2 * b - space)) 348 | path.move(to: CGPoint(x: 2 * b - space, y: space)) 349 | path.addLine(to: CGPoint(x: space, y: 2 * b - space)) 350 | 351 | shapeLayer.path = path 352 | shapeLayer.cornerRadius = square/2 353 | shapeLayer.masksToBounds = true 354 | shapeLayer.strokeStart = 0 355 | shapeLayer.strokeEnd = 0.0 356 | } 357 | 358 | fileprivate func correctJoinPoint() -> CGPoint { 359 | let r = min(self.bounds.width, self.bounds.height)/2 360 | let m = r/2 361 | let k = lineWidth/2 362 | 363 | let a: CGFloat = 2.0 364 | let b = -4 * r + 2 * m 365 | let c = (r - m) * (r - m) + 2 * r * k - k * k 366 | let x = (-b - sqrt(b * b - 4 * a * c))/(2 * a) 367 | let y = x + m 368 | 369 | return CGPoint(x: x, y: y) 370 | } 371 | 372 | fileprivate func errorJoinPoint() -> CGPoint { 373 | let r = min(self.bounds.width, self.bounds.height)/2 374 | let k = lineWidth/2 375 | 376 | let a: CGFloat = 2.0 377 | let b = -4 * r 378 | let c = r * r + 2 * r * k - k * k 379 | let x = (-b - sqrt(b * b - 4 * a * c))/(2 * a) 380 | 381 | return CGPoint(x: x, y: x) 382 | } 383 | 384 | @objc fileprivate func resetAnimations() { 385 | if status == .loading { 386 | status = .unknown 387 | progressLayer.removeAnimation(forKey: SMTSConstants.ringRotationAnimationKey) 388 | progressLayer.removeAnimation(forKey: SMTSConstants.ringStrokeAnimationKey) 389 | 390 | startLoading() 391 | } 392 | } 393 | 394 | @objc fileprivate func hiddenLoadingView() { 395 | status = .success 396 | self.isHidden = true 397 | 398 | if completionBlock != nil { 399 | completionBlock!() 400 | } 401 | } 402 | } 403 | 404 | 405 | 406 | --------------------------------------------------------------------------------