├── .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 | 
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 |
--------------------------------------------------------------------------------