2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amerhukic/AHDownloadButton/5ea4c1d39d7931201dac0b08eaadb1a5904e27be/Logo.png
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "AHDownloadButton",
7 | platforms: [
8 | .iOS(.v8)
9 | ],
10 | products: [
11 | .library(
12 | name: "AHDownloadButton",
13 | targets: ["AHDownloadButton"]),
14 | ],
15 | targets: [
16 | .target(
17 | name: "AHDownloadButton",
18 | dependencies: [])
19 | ],
20 | swiftLanguageVersions: [
21 | .v5
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | **AHDownloadButton** is a customizable download button similar to the download button in the latest version of Apple's App Store app (since iOS 11).
25 | It features download progress animation as well as animated transitions between download states: start download, pending, downloading and downloaded. [You can find more details about the implementation on my blog](https://amerhukic.com/replicating-app-store-download-button).
26 |
27 |
28 |
29 |
30 | ## Requirements
31 |
32 | - iOS 8.0+
33 | - Xcode 10.2+
34 | - Swift 5.0+
35 |
36 | ## Usage
37 |
38 | ### Code
39 | To use `AHDownloadButton` in code, you simply create a new instance and add it as a subview to your desired view:
40 | ```swift
41 | let downloadButton = AHDownloadButton()
42 | downloadButton.frame = CGRect(origin: origin, size: size)
43 | view.addSubview(downloadButton)
44 | ```
45 | The button can have 4 different states:
46 | - `startDownload` - initial state before downloading
47 | - `pending` - state for preparing for download
48 | - `downloading` - state when the user is downloading
49 | - `downloaded` - state when the user finished downloading
50 |
51 | The state of the button can be changed through its `state` property.
52 |
53 | ### Delegate
54 | You can use the `AHDownloadButtonDelegate` to monitor taps on the button and update button's state if needed. To update the current download progress, use the `progress` property. Here is an example how it could be implemented:
55 |
56 | ```swift
57 | extension DownloadViewController: AHDownloadButtonDelegate {
58 |
59 | func downloadButton(_ downloadButton: AHDownloadButton, tappedWithState state: AHDownloadButton.State)
60 | switch state {
61 | case .startDownload:
62 |
63 | // set the download progress to 0
64 | downloadButton.progress = 0
65 |
66 | // change state to pending and wait for download to start
67 | downloadButton.state = .pending
68 |
69 | // initiate download and update state to .downloading
70 | startDownloadingFile()
71 |
72 | case .pending:
73 |
74 | // button tapped while in pending state
75 | break
76 |
77 | case .downloading:
78 |
79 | // button tapped while in downloading state - stop downloading
80 | downloadButton.progress = 0
81 | downloadButton.state = .startDownload
82 |
83 | case .downloaded:
84 |
85 | // file is downloaded and can be opened
86 | openDownloadedFile()
87 |
88 | }
89 | }
90 | }
91 | ```
92 |
93 | You can also use closures instead of the `AHDownloadButtonDelegate` by setting the `didTapDownloadButtonAction` and `downloadButtonStateChangedAction` properties.
94 |
95 | ### Customisation
96 |
97 | `AHDownloadButton` can be customized. These are the properties that can be used for customizing the button:
98 |
99 | 1. Use the custom initializer `init(alignment: HorizontalAlignment)` to set the horizontal alignment property. `HorizontalAlignment` determines the position of the pending and downloading circles. The position can either be `center` , `left` or `right`. The default value is `center`.
100 |
101 |
102 | 2. Customization properties when button is in `startDownload` state:
103 |
104 | - `startDownloadButtonTitle` - button's title
105 | - `startDownloadButtonTitleFont` - button's title font
106 | - `startDownloadButtonTitleSidePadding` - padding for left and right side of button's title
107 | - `startDownloadButtonHighlightedBackgroundColor` - background color for the button when it's in highlighted state (when the user presses the button)
108 | - `startDownloadButtonNonhighlightedBackgroundColor` - background color for the button when it's in nonhighlighted state (when the button is not pressed)
109 | - `startDownloadButtonHighlightedTitleColor` - title color for the button when it's in highlighted state (when the user presses the button)
110 | - `startDownloadButtonNonhighlightedTitleColor` - title color for the button when it's in nonhighlighted state (when the button is not pressed)
111 |
112 |
113 | 3. Customization properties when button is in `pending` state:
114 |
115 | - `pendingCircleColor` - color of the pending circle
116 | - `pendingCircleLineWidth` - width of the pending circle
117 |
118 |
119 | 4. Customization properties when button is in `downloading` state:
120 |
121 | - `downloadingButtonHighlightedTrackCircleColor` - color for the track circle when it's in highlighted state (when the user presses the button)
122 | - `downloadingButtonNonhighlightedTrackCircleColor` - color for the track circle when it's in nonhighlighted state (when the button is not pressed)
123 | - `downloadingButtonHighlightedProgressCircleColor` - color for the progress circle when it's in highlighted state (when the user presses the button)
124 | - `downloadingButtonNonhighlightedProgressCircleColor` - color for the progress circle when it's in nonhighlighted state (when the button is not pressed)
125 | - `downloadingButtonHighlightedStopViewColor` - color for the stop view in the middle of the progress circle when it's in highlighted state (when the user presses the button)
126 | - `downloadingButtonNonhighlightedStopViewColor` - color for the stop view in the middle of the progress circle when it's in nonhighlighted state (when the button is not pressed)
127 | - `downloadingButtonCircleLineWidth` - width of the downloading circle
128 |
129 |
130 | 5. Customization properties when button is in `downloaded` state:
131 |
132 | - `downloadedButtonTitle` - button's title
133 | - `downloadedButtonTitleFont` - button's title font
134 | - `downloadedButtonTitleSidePadding` - padding for left and right side of button's title
135 | - `downloadedButtonHighlightedBackgroundColor` - background color for the button when it's in highlighted state (when the user presses the button)
136 | - `downloadedButtonNonhighlightedBackgroundColor` - background color for the button when it's in nonhighlighted state (when the button is not pressed)
137 | - `downloadedButtonHighlightedTitleColor` - title color for the button when it's in highlighted state (when the user presses the button)
138 | - `downloadedButtonNonhighlightedTitleColor` - title color for the button when it's in nonhighlighted state (when the button is not pressed)
139 |
140 | 6. `transitionAnimationDuration` - animation duration between the different states of the button
141 |
142 | ### Special note
143 |
144 | `AHDownloadButton` in `startDownload` and `downloaded` states calculates its width based on **button title**. Use the `startDownloadButtonTitleSidePadding` and `downloadedButtonTitleSidePadding` properties to customise the width when the button is in the aforementioned states.
145 |
146 | ## Example
147 |
148 | To run the example project, clone the repo, and run `pod install` from the Example directory first.
149 |
150 | ## Installation
151 |
152 | ### CocoaPods
153 |
154 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command:
155 |
156 | ```bash
157 | $ gem install cocoapods
158 | ```
159 |
160 | To integrate AHDownloadButton into your Xcode project using CocoaPods, specify it in your `Podfile`:
161 |
162 | ```ruby
163 | source 'https://github.com/CocoaPods/Specs.git'
164 | platform :ios, '8.0'
165 | use_frameworks!
166 |
167 | target '' do
168 | pod 'AHDownloadButton'
169 | end
170 | ```
171 |
172 | Then, run the following command:
173 |
174 | ```bash
175 | $ pod install
176 | ```
177 |
178 | ### Carthage
179 |
180 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate AHDownloadButton into your Xcode project using Carthage, specify it in your `Cartfile`:
181 |
182 | ```ogdl
183 | github "amerhukic/AHDownloadButton" ~> 1.3.0
184 | ```
185 |
186 | ### Swift Package Manager
187 |
188 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler.
189 |
190 | Once you have your Swift package set up, adding AHDownloadButton as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
191 |
192 | ```swift
193 | dependencies: [
194 | .package(url: "https://github.com/amerhukic/AHDownloadButton", .upToNextMajor(from: "1.3.0"))
195 | ]
196 | ```
197 |
198 | ## Author
199 |
200 | [Amer Hukić](https://amerhukic.com)
201 |
202 | ## License
203 |
204 | AHDownloadButton is licensed under the MIT license. Check the [LICENSE](LICENSE) file for details.
205 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amerhukic/AHDownloadButton/5ea4c1d39d7931201dac0b08eaadb1a5904e27be/Sources/AHDownloadButton/Assets/.gitkeep
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amerhukic/AHDownloadButton/5ea4c1d39d7931201dac0b08eaadb1a5904e27be/Sources/AHDownloadButton/Classes/.gitkeep
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/AHDownloadButton+StateTransitionAnimation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AHDownloadButton+StateTransitionAnimation.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 04/09/2018.
6 | // Copyright © 2018 Amer Hukic. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension AHDownloadButton {
12 |
13 | func animateTransition(from oldState: State, to newState: State) {
14 |
15 | let completion: (Bool) -> Void = { _ in
16 | self.animationDispatchGroup.leave()
17 | self.resetStateViews(except: newState)
18 | }
19 |
20 | switch (oldState, newState) {
21 | case (.startDownload, .pending):
22 | animateTransitionFromStartDownloadToPending(completion: completion)
23 |
24 | case (.startDownload, .downloading):
25 | animateTransitionFromStartDownloadToDownloading(completion: completion)
26 |
27 | case (.pending, .startDownload):
28 | animateTransitionFromPendingToStartDownload(completion: completion)
29 |
30 | case (.pending, .downloading):
31 | animateTransitionFromPendingToDownloading(completion: completion)
32 |
33 | case (.downloading, .downloaded):
34 | animateTransitionFromDownloadingToDownloaded(completion: completion)
35 |
36 | case (.downloading, .startDownload):
37 | animateTransitionFromDownloadingToStartDownload(completion: completion)
38 |
39 | default:
40 | handleUnsupportedTransitionAnimation(toState: newState)
41 | }
42 | }
43 |
44 | private func animateTransitionFromStartDownloadToPending(completion: @escaping (Bool) -> Void) {
45 | startDownloadButton.titleLabel?.alpha = 0
46 | startDownloadButtonWidthConstraint.constant = pendingViewWidthConstraint.constant
47 | UIView.animate(withDuration: transitionAnimationDuration, animations: {
48 | self.layoutIfNeeded()
49 | }, completion: { completed in
50 | completion(completed)
51 | self.pendingCircleView.alpha = 1
52 | self.pendingCircleView.startSpinning()
53 | })
54 | }
55 |
56 | private func animateTransitionFromStartDownloadToDownloading(completion: @escaping (Bool) -> Void) {
57 | startDownloadButton.titleLabel?.alpha = 0
58 | startDownloadButtonWidthConstraint.constant = downloadingButtonWidthConstraint.constant
59 | UIView.animate(withDuration: transitionAnimationDuration, animations: {
60 | self.layoutIfNeeded()
61 | }, completion: { completed in
62 | completion(completed)
63 | self.downloadingButton.alpha = 1
64 | })
65 | }
66 |
67 | private func animateTransitionFromPendingToStartDownload(completion: @escaping (Bool) -> Void) {
68 | startDownloadButtonWidthConstraint.constant = pendingViewWidthConstraint.constant
69 | layoutIfNeeded()
70 |
71 | startDownloadButton.alpha = 1
72 | startDownloadButtonWidthConstraint.constant = startDownloadButtonFullWidth
73 | UIView.animate(withDuration: transitionAnimationDuration, animations: {
74 | self.pendingCircleView.alpha = 0
75 | self.startDownloadButton.titleLabel?.alpha = 1
76 | self.layoutIfNeeded()
77 | }, completion: completion)
78 | }
79 |
80 | private func animateTransitionFromPendingToDownloading(completion: @escaping (Bool) -> Void) {
81 | pendingCircleView.alpha = 1
82 | downloadingButton.alpha = 0
83 | UIView.animate(withDuration: transitionAnimationDuration, animations: {
84 | self.pendingCircleView.alpha = 0
85 | self.downloadingButton.alpha = 1
86 | }, completion: completion)
87 | }
88 |
89 | private func animateTransitionFromDownloadingToDownloaded(completion: @escaping (Bool) -> Void) {
90 | downloadedButton.alpha = 1
91 | downloadedButtonWidthConstraint.constant = downloadingButtonWidthConstraint.constant
92 | layoutIfNeeded()
93 |
94 | downloadedButton.titleLabel?.alpha = 0
95 | downloadedButtonWidthConstraint.constant = downloadedButtonFullWidth
96 | UIView.animate(withDuration: transitionAnimationDuration, animations: {
97 | self.downloadingButton.alpha = 0
98 | self.downloadedButton.titleLabel?.alpha = 1
99 | self.layoutIfNeeded()
100 | }, completion: completion)
101 | }
102 |
103 | private func animateTransitionFromDownloadingToStartDownload(completion: @escaping (Bool) -> Void) {
104 | startDownloadButtonWidthConstraint.constant = downloadingButtonWidthConstraint.constant
105 | layoutIfNeeded()
106 |
107 | downloadingButton.alpha = 0
108 | startDownloadButton.alpha = 1
109 | startDownloadButtonWidthConstraint.constant = startDownloadButtonFullWidth
110 | UIView.animate(withDuration: transitionAnimationDuration, animations: {
111 | self.startDownloadButton.titleLabel?.alpha = 1
112 | self.layoutIfNeeded()
113 | }, completion: completion)
114 | }
115 |
116 | private func handleUnsupportedTransitionAnimation(toState newState: State) {
117 | switch newState {
118 | case .startDownload:
119 | startDownloadButton.alpha = 1
120 | case .pending:
121 | pendingCircleView.alpha = 1
122 | case .downloading:
123 | downloadingButton.alpha = 1
124 | case .downloaded:
125 | downloadedButton.alpha = 1
126 | }
127 | resetStateViews(except: newState)
128 | animationDispatchGroup.leave()
129 | }
130 |
131 | private func resetStateViews(except state: State) {
132 |
133 | if state != .startDownload {
134 | startDownloadButton.alpha = 0
135 | startDownloadButton.titleLabel?.alpha = 1
136 | startDownloadButtonWidthConstraint.constant = startDownloadButtonFullWidth
137 | }
138 |
139 | if state != .pending {
140 | pendingCircleView.alpha = 0
141 | }
142 |
143 | if state != .downloading {
144 | downloadingButton.alpha = 0
145 | progress = 0
146 | }
147 |
148 | if state != .downloaded {
149 | downloadedButton.alpha = 0
150 | downloadedButton.titleLabel?.alpha = 1
151 | downloadedButtonWidthConstraint.constant = downloadedButtonFullWidth
152 | }
153 |
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/AHDownloadButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AHDownloadButton.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 03/09/2018.
6 | // Copyright © 2018 Amer Hukic. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public protocol AHDownloadButtonDelegate: AnyObject {
12 | @available(*, deprecated, message: "Use downloadButton(_:, tappedWithState:) method")
13 | func didTapDownloadButton(_ downloadButton: AHDownloadButton, withState state: AHDownloadButton.State)
14 | func downloadButton(_ downloadButton: AHDownloadButton, stateChanged state: AHDownloadButton.State)
15 | func downloadButton(_ downloadButton: AHDownloadButton, tappedWithState state: AHDownloadButton.State)
16 | }
17 |
18 | public extension AHDownloadButtonDelegate {
19 | func didTapDownloadButton(_ downloadButton: AHDownloadButton, withState state: AHDownloadButton.State) { }
20 | func downloadButton(_ downloadButton: AHDownloadButton, stateChanged state: AHDownloadButton.State) { }
21 | func downloadButton(_ downloadButton: AHDownloadButton, tappedWithState state: AHDownloadButton.State) { }
22 | }
23 |
24 | public final class AHDownloadButton: UIView {
25 |
26 | public enum State {
27 | case startDownload
28 | case pending
29 | case downloading
30 | case downloaded
31 | }
32 |
33 | public enum HorizontalAlignment: Int {
34 | case center, left, right
35 |
36 | var relativeLayoutAttribute: NSLayoutConstraint.Attribute {
37 | switch self {
38 | case .center: return .centerX
39 | case .right: return .right
40 | case .left: return .left
41 | }
42 | }
43 | }
44 |
45 | // MARK: Public properties
46 |
47 | /// Start download button customisation properties
48 |
49 | public var startDownloadButtonTitle: String = "GET" {
50 | didSet {
51 | startDownloadButton.setTitle(startDownloadButtonTitle, for: .normal)
52 | startDownloadButtonTitleWidth = 0
53 | }
54 | }
55 |
56 | public var startDownloadButtonTitleFont: UIFont = .boldSystemFont(ofSize: 15) {
57 | didSet {
58 | startDownloadButton.titleLabel?.font = startDownloadButtonTitleFont
59 | }
60 | }
61 |
62 | public var startDownloadButtonTitleSidePadding: CGFloat = 12
63 |
64 | public var startDownloadButtonHighlightedBackgroundColor: UIColor = Color.Gray.light {
65 | didSet {
66 | startDownloadButton.highlightedBackgroundColor = startDownloadButtonHighlightedBackgroundColor
67 | }
68 | }
69 |
70 | public var startDownloadButtonNonhighlightedBackgroundColor: UIColor = Color.Gray.medium {
71 | didSet {
72 | startDownloadButton.nonhighlightedBackgroundColor = startDownloadButtonNonhighlightedBackgroundColor
73 | }
74 | }
75 |
76 | public var startDownloadButtonHighlightedTitleColor: UIColor = Color.Blue.light {
77 | didSet {
78 | startDownloadButton.highlightedTitleColor = startDownloadButtonHighlightedTitleColor
79 | }
80 | }
81 |
82 | public var startDownloadButtonNonhighlightedTitleColor: UIColor = Color.Blue.medium {
83 | didSet {
84 | startDownloadButton.nonhighlightedTitleColor = startDownloadButtonNonhighlightedTitleColor
85 | }
86 | }
87 |
88 | /// Pending view customisation properties
89 |
90 | public var pendingCircleColor: UIColor = Color.Gray.dark {
91 | didSet {
92 | pendingCircleView.circleColor = pendingCircleColor
93 | }
94 | }
95 |
96 | public var pendingCircleLineWidth: CGFloat = 2 {
97 | didSet {
98 | pendingCircleView.lineWidth = pendingCircleLineWidth
99 | }
100 | }
101 |
102 | /// Downloading button customisation properties
103 |
104 | public var downloadingButtonNonhighlightedTrackCircleColor: UIColor = Color.Gray.medium {
105 | didSet {
106 | downloadingButton.nonhighlightedTrackCircleColor = downloadingButtonNonhighlightedTrackCircleColor
107 | }
108 | }
109 |
110 | public var downloadingButtonHighlightedTrackCircleColor: UIColor = Color.Gray.light {
111 | didSet {
112 | downloadingButton.highlightedTrackCircleColor = downloadingButtonHighlightedTrackCircleColor
113 | }
114 | }
115 |
116 | public var downloadingButtonNonhighlightedProgressCircleColor: UIColor = Color.Blue.medium {
117 | didSet {
118 | downloadingButton.nonhighlightedProgressCircleColor = downloadingButtonNonhighlightedProgressCircleColor
119 | }
120 | }
121 |
122 | public var downloadingButtonHighlightedProgressCircleColor: UIColor = Color.Blue.light {
123 | didSet {
124 | downloadingButton.highlightedProgressCircleColor = downloadingButtonHighlightedProgressCircleColor
125 | }
126 | }
127 |
128 | public var downloadingButtonNonhighlightedStopViewColor: UIColor = Color.Blue.medium {
129 | didSet {
130 | downloadingButton.nonhighlightedStopViewColor = downloadingButtonNonhighlightedStopViewColor
131 | }
132 | }
133 |
134 | public var downloadingButtonHighlightedStopViewColor: UIColor = Color.Blue.light {
135 | didSet {
136 | downloadingButton.highlightedStopViewColor = downloadingButtonHighlightedStopViewColor
137 | }
138 | }
139 |
140 | public var downloadingButtonCircleLineWidth: CGFloat = 6 {
141 | didSet {
142 | downloadingButton.circleViewLineWidth = downloadingButtonCircleLineWidth
143 | }
144 | }
145 |
146 | public var progress: CGFloat = 0 {
147 | didSet {
148 | downloadingButton.progress = progress
149 | }
150 | }
151 |
152 | /// Downloaded button customisation properties
153 |
154 | public var downloadedButtonTitle: String = "OPEN" {
155 | didSet {
156 | downloadedButton.setTitle(downloadedButtonTitle, for: .normal)
157 | downloadedButtonTitleWidth = 0
158 | }
159 | }
160 |
161 | public var downloadedButtonTitleFont: UIFont = .boldSystemFont(ofSize: 15) {
162 | didSet {
163 | downloadedButton.titleLabel?.font = downloadedButtonTitleFont
164 | }
165 | }
166 |
167 | public var downloadedButtonTitleSidePadding: CGFloat = 12
168 |
169 | public var downloadedButtonHighlightedBackgroundColor: UIColor = Color.Gray.light {
170 | didSet {
171 | downloadedButton.highlightedBackgroundColor = downloadedButtonHighlightedBackgroundColor
172 | }
173 | }
174 |
175 | public var downloadedButtonNonhighlightedBackgroundColor: UIColor = Color.Gray.medium {
176 | didSet {
177 | downloadedButton.nonhighlightedBackgroundColor = downloadedButtonNonhighlightedBackgroundColor
178 | }
179 | }
180 |
181 | public var downloadedButtonHighlightedTitleColor: UIColor = Color.Blue.light {
182 | didSet {
183 | downloadedButton.highlightedTitleColor = downloadedButtonHighlightedTitleColor
184 | }
185 | }
186 |
187 | public var downloadedButtonNonhighlightedTitleColor: UIColor = Color.Blue.medium {
188 | didSet {
189 | downloadedButton.nonhighlightedTitleColor = downloadedButtonNonhighlightedTitleColor
190 | }
191 | }
192 |
193 | /// State transformation
194 |
195 | public var state: State = .startDownload {
196 | didSet {
197 | delegate?.downloadButton(self, stateChanged: state)
198 | downloadButtonStateChangedAction?(self, state)
199 | animationQueue.async { [currentState = state] in
200 | self.animationDispatchGroup.enter()
201 |
202 | var delay: TimeInterval = 0
203 | if oldValue == .downloading && currentState == .downloaded && self.downloadingButton.progress == 1 {
204 | delay = self.downloadingButton.progressCircleView.progressAnimationDuration
205 | }
206 |
207 | DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
208 | self.animateTransition(from: oldValue, to: currentState)
209 | }
210 | self.animationDispatchGroup.wait()
211 | }
212 | }
213 | }
214 |
215 | public var transitionAnimationDuration: TimeInterval = 0.1
216 |
217 | /// Callbacks
218 |
219 | public weak var delegate: AHDownloadButtonDelegate?
220 |
221 | public var didTapDownloadButtonAction: ((AHDownloadButton, State) -> Void)?
222 |
223 | public var downloadButtonStateChangedAction: ((AHDownloadButton, State) -> Void)?
224 |
225 | // MARK: Private properties
226 |
227 | let startDownloadButton: HighlightableRoundedButton = {
228 | let button = HighlightableRoundedButton()
229 | button.addTarget(self, action: #selector(currentButtonTapped), for: .touchUpInside)
230 | return button
231 | }()
232 |
233 | let pendingCircleView: CircleView = {
234 | let view = CircleView()
235 | view.endAngleRadians = view.startAngleRadians + 12 * .pi / 7
236 | return view
237 | }()
238 |
239 | let downloadingButton: ProgressButton = {
240 | let button = ProgressButton()
241 | button.addTarget(self, action: #selector(currentButtonTapped), for: .touchUpInside)
242 | return button
243 | }()
244 |
245 | let downloadedButton: HighlightableRoundedButton = {
246 | let button = HighlightableRoundedButton()
247 | button.addTarget(self, action: #selector(currentButtonTapped), for: .touchUpInside)
248 | return button
249 | }()
250 |
251 | let contentHorizontalAlignment: HorizontalAlignment
252 |
253 | // MARK: Animation
254 |
255 | let animationDispatchGroup = DispatchGroup()
256 | let animationQueue = DispatchQueue(label: "com.amerhukic.animation")
257 |
258 | // MARK: Constraints
259 |
260 | var startDownloadButtonWidthConstraint: NSLayoutConstraint!
261 | var pendingViewWidthConstraint: NSLayoutConstraint!
262 | var downloadingButtonWidthConstraint: NSLayoutConstraint!
263 | var downloadedButtonWidthConstraint: NSLayoutConstraint!
264 | var horizontalAlignmentAttribute: NSLayoutConstraint.Attribute {
265 | return contentHorizontalAlignment.relativeLayoutAttribute
266 | }
267 |
268 | var startDownloadButtonTitleWidth: CGFloat = 0 {
269 | didSet {
270 | startDownloadButtonWidthConstraint.constant = startDownloadButtonFullWidth
271 | }
272 | }
273 |
274 | var downloadedButtonTitleWidth: CGFloat = 0 {
275 | didSet {
276 | downloadedButtonWidthConstraint.constant = downloadedButtonFullWidth
277 | }
278 | }
279 |
280 | var startDownloadButtonFullWidth: CGFloat {
281 | return startDownloadButtonTitleWidth + 2 * startDownloadButtonTitleSidePadding
282 | }
283 |
284 | var downloadedButtonFullWidth: CGFloat {
285 | return downloadedButtonTitleWidth + 2 * downloadedButtonTitleSidePadding
286 | }
287 |
288 | // MARK: Initializers
289 |
290 | public init(alignment: HorizontalAlignment) {
291 | contentHorizontalAlignment = alignment
292 | super.init(frame: .zero)
293 | commonInit()
294 | }
295 |
296 | public override init(frame: CGRect) {
297 | contentHorizontalAlignment = .center
298 | super.init(frame: frame)
299 | commonInit()
300 | }
301 |
302 | public required init?(coder aDecoder: NSCoder) {
303 | contentHorizontalAlignment = .center
304 | super.init(coder: aDecoder)
305 | commonInit()
306 | }
307 |
308 | private func commonInit() {
309 | addSubview(startDownloadButton)
310 | setUpStartDownloadButtonProperties()
311 | setUpStartDownloadButtonConstraints()
312 |
313 | addSubview(pendingCircleView)
314 | setUpPendingCircleViewProperties()
315 | setUpPendingButtonConstraints()
316 |
317 | addSubview(downloadingButton)
318 | setUpDownloadingButtonProperties()
319 | setUpDownloadingButtonConstraints()
320 |
321 | addSubview(downloadedButton)
322 | setUpDownloadedButtonProperties()
323 | setUpDownloadedButtonConstraints()
324 | }
325 |
326 | // MARK: Style customisation
327 |
328 | private func setUpStartDownloadButtonProperties() {
329 | startDownloadButton.setTitle(startDownloadButtonTitle, for: .normal)
330 | startDownloadButton.titleLabel?.font = startDownloadButtonTitleFont
331 | startDownloadButton.highlightedBackgroundColor = startDownloadButtonHighlightedBackgroundColor
332 | startDownloadButton.nonhighlightedBackgroundColor = startDownloadButtonNonhighlightedBackgroundColor
333 | startDownloadButton.highlightedTitleColor = startDownloadButtonHighlightedTitleColor
334 | startDownloadButton.nonhighlightedTitleColor = startDownloadButtonNonhighlightedTitleColor
335 | }
336 |
337 | private func setUpPendingCircleViewProperties() {
338 | pendingCircleView.circleColor = pendingCircleColor
339 | pendingCircleView.lineWidth = pendingCircleLineWidth
340 | pendingCircleView.alpha = 0
341 |
342 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(currentButtonTapped))
343 | pendingCircleView.addGestureRecognizer(tapGesture)
344 | }
345 |
346 | private func setUpDownloadingButtonProperties() {
347 | downloadingButton.highlightedTrackCircleColor = downloadingButtonHighlightedTrackCircleColor
348 | downloadingButton.nonhighlightedTrackCircleColor = downloadingButtonNonhighlightedTrackCircleColor
349 | downloadingButton.highlightedProgressCircleColor = downloadingButtonHighlightedProgressCircleColor
350 | downloadingButton.nonhighlightedProgressCircleColor = downloadingButtonNonhighlightedProgressCircleColor
351 | downloadingButton.highlightedStopViewColor = downloadingButtonHighlightedStopViewColor
352 | downloadingButton.nonhighlightedStopViewColor = downloadingButtonNonhighlightedStopViewColor
353 | downloadingButton.alpha = 0
354 | }
355 |
356 | private func setUpDownloadedButtonProperties() {
357 | downloadedButton.setTitle(downloadedButtonTitle, for: .normal)
358 | downloadedButton.titleLabel?.font = downloadedButtonTitleFont
359 | downloadedButton.highlightedBackgroundColor = downloadedButtonHighlightedBackgroundColor
360 | downloadedButton.nonhighlightedBackgroundColor = downloadedButtonNonhighlightedBackgroundColor
361 | downloadedButton.highlightedTitleColor = downloadedButtonHighlightedTitleColor
362 | downloadedButton.nonhighlightedTitleColor = downloadedButtonNonhighlightedTitleColor
363 | downloadedButton.alpha = 0
364 | }
365 |
366 | // MARK: Constraints setup
367 |
368 | private func setUpStartDownloadButtonConstraints() {
369 | let topConstraint = startDownloadButton.constraint(attribute: .top, toItem: self, toAttribute: .top)
370 |
371 | let bottomConstraint = startDownloadButton.constraint(attribute: .bottom, toItem: self, toAttribute: .bottom)
372 |
373 | let horizontalPositionConstraint = startDownloadButton.constraint(attribute: horizontalAlignmentAttribute, toItem: self, toAttribute: horizontalAlignmentAttribute)
374 |
375 | startDownloadButtonWidthConstraint = startDownloadButton.constraint(attribute: .width, constant: 50)
376 |
377 | NSLayoutConstraint.activate([topConstraint, bottomConstraint, horizontalPositionConstraint, startDownloadButtonWidthConstraint])
378 | }
379 |
380 | private func setUpPendingButtonConstraints() {
381 | let horizontalPositionConstraint = pendingCircleView.constraint(attribute: horizontalAlignmentAttribute, toItem: self, toAttribute: horizontalAlignmentAttribute)
382 | let heightConstraint = pendingCircleView.constraint(attribute: .height, relation: .equal, toItem: pendingCircleView, toAttribute: .width)
383 | let verticalPositionConstraint = pendingCircleView.constraint(attribute: .centerY, toItem: self, toAttribute: .centerY)
384 |
385 | pendingViewWidthConstraint = pendingCircleView.constraint(attribute: .width, constant: 30)
386 | NSLayoutConstraint.activate([horizontalPositionConstraint, verticalPositionConstraint, heightConstraint, pendingViewWidthConstraint])
387 | }
388 |
389 | private func setUpDownloadingButtonConstraints() {
390 | let horizontalPositionConstraint = downloadingButton.constraint(attribute: horizontalAlignmentAttribute, toItem: self, toAttribute: horizontalAlignmentAttribute)
391 | let verticalPositionConstraint = downloadingButton.constraint(attribute: .centerY, toItem: self, toAttribute: .centerY)
392 |
393 | let heightConstraint = downloadingButton.constraint(attribute: .height, toItem: downloadingButton, toAttribute: .width)
394 |
395 | downloadingButtonWidthConstraint = downloadingButton.constraint(attribute: .width, constant: 30)
396 |
397 | NSLayoutConstraint.activate([horizontalPositionConstraint, verticalPositionConstraint, heightConstraint, downloadingButtonWidthConstraint])
398 | }
399 |
400 | private func setUpDownloadedButtonConstraints() {
401 | let topConstraint = downloadedButton.constraint(attribute: .top, toItem: self, toAttribute: .top)
402 | let bottomConstraint = downloadedButton.constraint(attribute: .bottom, toItem: self, toAttribute: .bottom)
403 | let horizontalPositionConstraint = downloadedButton.constraint(attribute: horizontalAlignmentAttribute, toItem: self, toAttribute: horizontalAlignmentAttribute)
404 |
405 | // This constraint will be changed later on (in layoutSubviews), here we're just creating it
406 | downloadedButtonWidthConstraint = downloadedButton.constraint(attribute: .width, constant: 50)
407 |
408 | NSLayoutConstraint.activate([topConstraint, bottomConstraint, horizontalPositionConstraint, downloadedButtonWidthConstraint])
409 | }
410 |
411 | // MARK: Method overrides
412 |
413 | public override func layoutSubviews() {
414 | super.layoutSubviews()
415 | let width = min(frame.width, frame.height)
416 | pendingViewWidthConstraint.constant = width
417 | downloadingButtonWidthConstraint.constant = width
418 |
419 | if startDownloadButtonTitleWidth == 0 {
420 | startDownloadButtonTitleWidth = startDownloadButton.titleWidth
421 | }
422 |
423 | if downloadedButtonTitleWidth == 0 {
424 | downloadedButtonTitleWidth = downloadedButton.titleWidth
425 | }
426 | }
427 |
428 | // MARK: Action methods
429 |
430 | @objc private func currentButtonTapped() {
431 | delegate?.downloadButton(self, tappedWithState: state)
432 | didTapDownloadButtonAction?(self, state)
433 | }
434 |
435 | }
436 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/CircleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleView.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 03/09/2018.
6 | // Copyright © 2018 Amer Hukic. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class CircleView: UIView {
12 |
13 | // MARK: Properties
14 |
15 | var startAngleRadians: CGFloat = -CGFloat.pi / 2
16 |
17 | var endAngleRadians: CGFloat = 3 * CGFloat.pi / 2
18 |
19 | var lineWidth: CGFloat = 1 {
20 | didSet {
21 | circleLayer.lineWidth = lineWidth
22 | }
23 | }
24 |
25 | var circleColor: UIColor = Color.Blue.medium {
26 | didSet {
27 | circleLayer.strokeColor = circleColor.cgColor
28 | }
29 | }
30 |
31 | let circleLayer: CAShapeLayer = {
32 | let layer = CAShapeLayer()
33 | layer.fillColor = UIColor.clear.cgColor
34 | layer.lineCap = .round
35 | return layer
36 | }()
37 |
38 | // MARK: Initializers
39 |
40 | override init(frame: CGRect) {
41 | super.init(frame: frame)
42 | commonInit()
43 | }
44 |
45 | required init?(coder aDecoder: NSCoder) {
46 | super.init(coder: aDecoder)
47 | commonInit()
48 | }
49 |
50 | private func commonInit() {
51 | backgroundColor = .clear
52 | circleLayer.strokeColor = circleColor.cgColor
53 | circleLayer.lineWidth = lineWidth
54 | layer.addSublayer(circleLayer)
55 | }
56 |
57 | override func layoutSubviews() {
58 | super.layoutSubviews()
59 | let radius = min(frame.width / 2, frame.height / 2) - lineWidth / 2
60 | let center = CGPoint(x: frame.width / 2, y: frame.height / 2)
61 | circleLayer.path = UIBezierPath(arcCenter: center,
62 | radius: radius,
63 | startAngle: startAngleRadians,
64 | endAngle: endAngleRadians,
65 | clockwise: true).cgPath
66 | }
67 |
68 | func startSpinning() {
69 | let animationKey = "rotation"
70 | layer.removeAnimation(forKey: animationKey)
71 | let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
72 | rotationAnimation.fromValue = 0.0
73 | rotationAnimation.toValue = CGFloat.pi * 2
74 | rotationAnimation.duration = 2
75 | rotationAnimation.repeatCount = .greatestFiniteMagnitude;
76 | layer.add(rotationAnimation, forKey: animationKey)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/Color.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 07/09/2018.
6 | // Copyright © 2018 Amer Hukic. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | enum Color {
12 |
13 | enum Gray {
14 | static let light = UIColor(red: 245.0 / 255.0, green: 244.0 / 255.0, blue: 249.0 / 255.0, alpha: 1)
15 | static let medium = UIColor(red: 238.0 / 255.0, green: 239.0 / 255.0, blue: 245.0 / 255.0, alpha: 1)
16 | static let dark = UIColor(red: 229.0 / 255.0, green: 229.0 / 255.0, blue: 233.0 / 255.0, alpha: 1)
17 | }
18 |
19 | enum Blue {
20 | static let light = UIColor(red: 199.0 / 255.0, green: 222 / 255.0, blue: 243 / 255.0, alpha: 1)
21 | static let medium = UIColor(red: 9.0 / 255.0, green: 111.0 / 255.0, blue: 227.0 / 255.0, alpha: 1)
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/HighlightableRoundedButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HighlightableRoundedButton.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 03/09/2018.
6 | // Copyright © 2018 Amer Hukic. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class HighlightableRoundedButton: UIButton {
12 |
13 | // MARK: Properties
14 |
15 | var highlightedBackgroundColor = Color.Gray.light {
16 | didSet {
17 | updateColors()
18 | }
19 | }
20 |
21 | var nonhighlightedBackgroundColor = Color.Gray.medium {
22 | didSet {
23 | updateColors()
24 | }
25 | }
26 |
27 | var highlightedTitleColor = Color.Blue.light {
28 | didSet {
29 | updateColors()
30 | }
31 | }
32 |
33 | var nonhighlightedTitleColor = Color.Blue.medium {
34 | didSet {
35 | updateColors()
36 | }
37 | }
38 |
39 | override var isHighlighted: Bool {
40 | didSet {
41 | updateColors()
42 | }
43 | }
44 |
45 | // MARK: Initializers
46 |
47 | override init(frame: CGRect) {
48 | super.init(frame: frame)
49 | updateColors()
50 | }
51 |
52 | required init?(coder aDecoder: NSCoder) {
53 | super.init(coder: aDecoder)
54 | updateColors()
55 | }
56 |
57 | // MARK: Helper methods
58 |
59 | private func updateColors() {
60 | backgroundColor = isHighlighted ? highlightedBackgroundColor : nonhighlightedBackgroundColor
61 | let titleColor = isHighlighted ? highlightedTitleColor : nonhighlightedTitleColor
62 | setTitleColor(titleColor, for: .normal)
63 | }
64 |
65 | override func layoutSubviews() {
66 | super.layoutSubviews()
67 | layer.cornerRadius = frame.height / 2
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/ProgressButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressButton.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 03/09/2018.
6 | // Copyright © 2018 Amer Hukic. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class ProgressButton: UIControl {
12 |
13 | // MARK: Properties
14 |
15 | var circleViewLineWidth: CGFloat = 6 {
16 | didSet {
17 | progressCircleView.lineWidth = circleViewLineWidth
18 | trackCircleView.lineWidth = circleViewLineWidth
19 | }
20 | }
21 |
22 | let stopView: UIView = {
23 | let view = UIView()
24 | view.isUserInteractionEnabled = false
25 | view.layer.cornerRadius = 3
26 | return view
27 | }()
28 |
29 | lazy var trackCircleView: CircleView = {
30 | let circleView = CircleView()
31 | circleView.lineWidth = circleViewLineWidth
32 | circleView.isUserInteractionEnabled = false
33 | return circleView
34 | }()
35 |
36 | lazy var progressCircleView: ProgressCircleView = {
37 | let view = ProgressCircleView()
38 | view.lineWidth = circleViewLineWidth
39 | view.isUserInteractionEnabled = false
40 | return view
41 | }()
42 |
43 | var nonhighlightedTrackCircleColor: UIColor = Color.Gray.medium {
44 | didSet {
45 | updateColors()
46 | }
47 | }
48 |
49 | var highlightedTrackCircleColor: UIColor = Color.Gray.light {
50 | didSet {
51 | updateColors()
52 | }
53 | }
54 |
55 | var nonhighlightedProgressCircleColor: UIColor = Color.Blue.medium {
56 | didSet {
57 | updateColors()
58 | }
59 | }
60 |
61 | var highlightedProgressCircleColor: UIColor = Color.Blue.light {
62 | didSet {
63 | updateColors()
64 | }
65 | }
66 |
67 | var nonhighlightedStopViewColor: UIColor = Color.Blue.medium {
68 | didSet {
69 | updateColors()
70 | }
71 | }
72 |
73 | var highlightedStopViewColor: UIColor = Color.Blue.light {
74 | didSet {
75 | updateColors()
76 | }
77 | }
78 |
79 | var progress: CGFloat = 0 {
80 | didSet {
81 | if progress < 0 {
82 | progress = 0
83 | } else if progress > 1 {
84 | progress = 1
85 | }
86 | progressCircleView.progress = progress
87 | }
88 | }
89 |
90 | var stopButtonCornerRadius: CGFloat = 3 {
91 | didSet {
92 | stopView.layer.cornerRadius = stopButtonCornerRadius
93 | }
94 | }
95 |
96 | override var isHighlighted: Bool {
97 | didSet {
98 | updateColors()
99 | }
100 | }
101 |
102 | // MARK: Initializers
103 |
104 | override init(frame: CGRect) {
105 | super.init(frame: frame)
106 | commonInit()
107 | }
108 |
109 | required init?(coder aDecoder: NSCoder) {
110 | super.init(coder: aDecoder)
111 | commonInit()
112 | }
113 |
114 | // MARK: Helper methods
115 |
116 | private func commonInit() {
117 | backgroundColor = .clear
118 |
119 | addSubview(trackCircleView)
120 | trackCircleView.pinToSuperview()
121 |
122 | addSubview(progressCircleView)
123 | progressCircleView.pinToSuperview()
124 |
125 | addSubview(stopView)
126 | stopView.centerToSuperview()
127 | let heightConstraint = stopView.constraint(attribute: .height, toItem: stopView, toAttribute: .width)
128 | let widthConstraint = stopView.constraint(attribute: .width, toItem: self, toAttribute: .width, multiplier: 0.3, constant: 0)
129 |
130 | NSLayoutConstraint.activate([heightConstraint, widthConstraint])
131 | updateColors()
132 | }
133 |
134 | private func updateColors() {
135 | trackCircleView.circleColor = isHighlighted ? highlightedTrackCircleColor : nonhighlightedTrackCircleColor
136 | progressCircleView.circleColor = isHighlighted ? highlightedProgressCircleColor : nonhighlightedProgressCircleColor
137 | stopView.backgroundColor = isHighlighted ? highlightedStopViewColor : nonhighlightedStopViewColor
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/ProgressCircleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressCircleView.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 17/09/2018.
6 | //
7 |
8 | import UIKit
9 |
10 | final class ProgressCircleView: UIView {
11 |
12 | // MARK: Properties
13 |
14 | private let circleView: CircleView = {
15 | let view = CircleView()
16 | view.circleColor = .red
17 | view.startAngleRadians = -CGFloat.pi / 2
18 | view.endAngleRadians = view.startAngleRadians + 2 * .pi
19 | return view
20 | }()
21 |
22 | private var isAnimating = false
23 |
24 | var progressAnimationDuration: TimeInterval = 0.3
25 |
26 | var progress: CGFloat = 0 {
27 | didSet {
28 | if progress == 1 && isAnimating {
29 | if let currentAnimatedProgress = circleView.circleLayer.presentation()?.strokeEnd {
30 | circleView.circleLayer.strokeEnd = currentAnimatedProgress
31 | animateProgress(from: currentAnimatedProgress, to: progress)
32 | }
33 | }
34 |
35 | guard !isAnimating else { return }
36 | animateProgress(from: circleView.circleLayer.strokeEnd, to: progress)
37 | }
38 | }
39 |
40 | var lineWidth: CGFloat = 1 {
41 | didSet {
42 | circleView.lineWidth = lineWidth
43 | }
44 | }
45 |
46 | var circleColor: UIColor = Color.Blue.medium {
47 | didSet {
48 | circleView.circleLayer.strokeColor = circleColor.cgColor
49 | }
50 | }
51 |
52 | // MARK: Initializers
53 |
54 | override init(frame: CGRect) {
55 | super.init(frame: frame)
56 | commonInit()
57 | }
58 |
59 | required init?(coder aDecoder: NSCoder) {
60 | super.init(coder: aDecoder)
61 | commonInit()
62 | }
63 |
64 | private func commonInit() {
65 | addSubview(circleView)
66 | circleView.pinToSuperview()
67 | }
68 |
69 | private func animateProgress(from startValue: CGFloat, to endValue: CGFloat) {
70 | isAnimating = true
71 | circleView.circleLayer.strokeEnd = endValue
72 | let animation = CABasicAnimation(keyPath: "strokeEnd")
73 | animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
74 | animation.fromValue = startValue
75 | animation.duration = progressAnimationDuration
76 | animation.delegate = self
77 | circleView.circleLayer.add(animation, forKey: nil)
78 | }
79 | }
80 |
81 | extension ProgressCircleView: CAAnimationDelegate {
82 | func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
83 | isAnimating = false
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/UIButton+TitleWidth.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIButton+TitleWidth.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 03/09/2018.
6 | // Copyright © 2018 Amer Hukic. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIButton {
12 |
13 | var titleWidth: CGFloat {
14 | guard let text = titleLabel?.text, let font = titleLabel?.font else { return 0 }
15 | return text.size(withAttributes: [.font: font]).width
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/AHDownloadButton/Classes/UIView+Constraint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+Constraint.swift
3 | // AHDownloadButton
4 | //
5 | // Created by Amer Hukic on 03/09/2018.
6 | // Copyright © 2018 Amer Hukic. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIView {
12 |
13 | @discardableResult
14 | func constraint(attribute: NSLayoutConstraint.Attribute, relation: NSLayoutConstraint.Relation = .equal, toItem: Any? = nil, toAttribute: NSLayoutConstraint.Attribute = .notAnAttribute, multiplier: CGFloat = 1, constant: CGFloat = 0) -> NSLayoutConstraint {
15 | translatesAutoresizingMaskIntoConstraints = false
16 | let constraint = NSLayoutConstraint(item: self,
17 | attribute: attribute,
18 | relatedBy: relation,
19 | toItem: toItem,
20 | attribute: toAttribute,
21 | multiplier: multiplier,
22 | constant: constant)
23 | return constraint
24 | }
25 |
26 | func pinToSuperview() {
27 | translatesAutoresizingMaskIntoConstraints = false
28 | let topConstraint = self.constraint(attribute: .top, toItem: superview, toAttribute: .top)
29 | let bottomConstraint = self.constraint(attribute: .bottom, toItem: superview, toAttribute: .bottom)
30 | let leadingConstraint = constraint(attribute: .leading, toItem: superview, toAttribute: .leading)
31 | let trailingConstraint = self.constraint(attribute: .trailing, toItem: superview, toAttribute: .trailing)
32 | NSLayoutConstraint.activate([trailingConstraint, topConstraint, leadingConstraint, bottomConstraint])
33 | }
34 |
35 | func centerToSuperview() {
36 | translatesAutoresizingMaskIntoConstraints = false
37 | let centerXConstraint = constraint(attribute: .centerX, toItem: superview, toAttribute: .centerX)
38 |
39 | let centerYConstraint = constraint(attribute: .centerY, toItem: superview, toAttribute: .centerY)
40 | NSLayoutConstraint.activate([centerXConstraint, centerYConstraint])
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/carthage.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # carthage.sh
4 | # Usage example: ./carthage.sh build --platform iOS
5 |
6 | set -euo pipefail
7 |
8 | xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX)
9 | trap 'rm -f "$xcconfig"' INT TERM HUP EXIT
10 |
11 | # For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise
12 | # the build will fail on lipo due to duplicate architectures.
13 |
14 | CURRENT_XCODE_VERSION=$(xcodebuild -version | grep "Build version" | cut -d' ' -f3)
15 | echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$CURRENT_XCODE_VERSION = arm64 arm64e armv7 armv7s armv6 armv8" >> $xcconfig
16 |
17 | echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >> $xcconfig
18 | echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig
19 |
20 | export XCODE_XCCONFIG_FILE="$xcconfig"
21 | carthage "$@"
22 |
--------------------------------------------------------------------------------