Skip to content

Commit fecd1af

Browse files
committedMar 10, 2024
Initial Commit
0 parents  commit fecd1af

14 files changed

+1132
-0
lines changed
 

‎.github/FUNDING.yml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
custom: [https://www.buymeacoffee.com/mijickteam]

‎.github/release-cocoapods.yml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Release CocoaPods
2+
on:
3+
push:
4+
branches:
5+
- master
6+
workflow_dispatch:
7+
8+
jobs:
9+
build:
10+
runs-on: macOS-latest
11+
steps:
12+
- uses: actions/checkout@v1
13+
- name: Publish to CocoaPod register
14+
env:
15+
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
16+
run: |
17+
pod trunk push MijickTimer.podspec

‎.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

‎Package.swift

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "MijickTimer",
8+
platforms: [
9+
.iOS(.v13)
10+
],
11+
products: [
12+
.library(name: "MijickTimer", targets: ["MijickTimer"]),
13+
],
14+
targets: [
15+
.target(name: "MijickTimer", dependencies: [], path: "Sources"),
16+
.testTarget(name: "MijickTimerTests", dependencies: ["MijickTimer"], path: "Tests")
17+
]
18+
)

‎README.md

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<br>
2+
3+
<p align="center">
4+
<picture>
5+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/Mijick/Assets/blob/main/Timer/Logotype/On%20Dark.svg">
6+
<source media="(prefers-color-scheme: light)" srcset="https://github.com/Mijick/Assets/blob/main/Timer/Logotype/On%20Light.svg">
7+
<img alt="Timer Logo" src="https://github.com/Mijick/Assets/blob/main/Timer/Logotype/On%20Dark.svg" width="76%"">
8+
</picture>
9+
</p>
10+
11+
<h3 style="font-size: 5em" align="center">
12+
Modern API for Timer
13+
</h3>
14+
15+
<p align="center">
16+
Easy to use yet powerful Timer library. Keep your code clean
17+
</p>
18+
19+
<p align="center">
20+
<a href="https://github.com/Mijick/Timer-Demo" rel="nofollow">Try demo we prepared</a>
21+
</p>
22+
23+
<br>
24+
25+
<p align="center">
26+
<img alt="SwiftUI logo" src="https://github.com/Mijick/Assets/blob/main/Timer/Labels/Language.svg"/>
27+
<img alt="Platforms: iOS, iPadOS, macOS, tvOS" src="https://github.com/Mijick/Assets/blob/main/Timer/Labels/Platforms.svg"/>
28+
<img alt="Current Version" src="https://github.com/Mijick/Assets/blob/main/Timer/Labels/Version.svg"/>
29+
<img alt="License: MIT" src="https://github.com/Mijick/Assets/blob/main/Timer/Labels/License.svg"/>
30+
</p>
31+
32+
<p align="center">
33+
<img alt="Made in Kraków" src="https://github.com/Mijick/Assets/blob/main/Timer/Labels/Origin.svg"/>
34+
<a href="https://twitter.com/MijickTeam">
35+
<img alt="Follow us on X" src="https://github.com/Mijick/Assets/blob/main/Timer/Labels/X.svg"/>
36+
</a>
37+
<a href=mailto:team@mijick.com?subject=Hello>
38+
<img alt="Let's work together" src="https://github.com/Mijick/Assets/blob/main/Timer/Labels/Work%20with%20us.svg"/>
39+
</a>
40+
<a href="https://github.com/Mijick/Timer/stargazers">
41+
<img alt="Stargazers" src="https://github.com/Mijick/Assets/blob/main/Timer/Labels/Stars.svg"/>
42+
</a>
43+
</p>
44+
45+
<p align="center">
46+
<img alt="Timer Examples" src="https://github.com/Mijick/Assets/blob/main/Timer/GIFs/Timer.gif"/>
47+
</p>
48+
49+
<br>
50+
51+
Timer is a free and open-source library dedicated for Swift that makes the process of handling timers easier and much cleaner.
52+
* **Improves code quality.** Start timer using the `publish().start()` method. Stop the timer with `stop()`. Simple as never.
53+
* **Run your timer in both directions.** Our Timer can operate in both modes (increasing or decreasing).
54+
* **Supports background mode.** Don't worry about the timer when the app goes into the background. We handled it!
55+
* **And much more.** Our library allows you to convert the current time to a string or to display the timer progress in no time.
56+
57+
<br>
58+
59+
# Getting Started
60+
### ✋ Requirements
61+
62+
| **Platforms** | **Minimum Swift Version** |
63+
|:----------|:----------|
64+
| iOS 13+ | 5.0 |
65+
66+
### ⏳ Installation
67+
68+
#### [Swift package manager][spm]
69+
Swift package manager is a tool for automating the distribution of Swift code and is integrated into the Swift compiler.
70+
71+
Once you have your Swift package set up, adding Timer as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
72+
73+
```Swift
74+
dependencies: [
75+
.package(url: "https://github.com/Mijick/Timer", branch(“main”))
76+
]
77+
```
78+
79+
<br>
80+
81+
# Usage
82+
83+
### 1. Initialise the timer
84+
Call the `publish()` method that has three parameters:
85+
* **time** - The number of seconds between firings of the timer.
86+
* **tolerance** - The number of seconds after the update date that the timer may fire.
87+
* **currentTime** - The current timer time.
88+
```Swift
89+
try! MTimer.publish(every: 1, currentTime: $currentTime)
90+
```
91+
92+
### 2. Start the timer
93+
Start the timer using the `start()` method. You can customise the start and end time using the parameters of this method.
94+
```Swift
95+
try! MTimer
96+
.publish(every: 1, currentTime: $currentTime)
97+
.start(from: .init(minutes: 21, seconds: 37), to: .zero)
98+
```
99+
100+
### 3. *(Optional)* Observe TimerStatus and TimerProgress
101+
You can observe changes in both values by calling either of the methods
102+
```Swift
103+
try! MTimer
104+
.publish(every: 1, currentTime: $currentTime)
105+
.bindTimerStatus(isTimerRunning: $isTimerRunning)
106+
.bindTimerProgress(progress: $timerProgress)
107+
.start(from: .init(minutes: 21, seconds: 37), to: .zero)
108+
```
109+
110+
### 4. Stop the timer
111+
Timer can be stopped with `stop()` method.
112+
```Swift
113+
MTimer.stop()
114+
```
115+
116+
### 5. Additional timer controls
117+
- Once stopped, the timer can be resumed - simply use the `resume()` method.
118+
```Swift
119+
try! MTimer.resume()
120+
```
121+
- To stop and reset the timer to its initial values, use the `reset()` method.
122+
```Swift
123+
MTimer.reset()
124+
```
125+
126+
### 6. Displaying the current time as String
127+
You can convert the current MTime to String by calling the `toString()` method. Use the `formatter` parameter to customise the output.
128+
```Swift
129+
currentTime.toString {
130+
$0.unitsStyle = .full
131+
$0.allowedUnits = [.hour, .minute]
132+
return $0
133+
}
134+
```
135+
136+
### 7. Creating more timer instances
137+
Create a new instance of the timer and assign it to a new variable. Use the above functions directly with it
138+
```Swift
139+
let newTimer = MTimer.createNewInstance()
140+
141+
try! newTimer
142+
.publish(every: 1, currentTime: $currentTime)
143+
.start()
144+
145+
newTimer.stop()
146+
```
147+
148+
<br>
149+
150+
# Try our demo
151+
See for yourself how does it work by cloning [project][Demo] we created
152+
153+
# License
154+
Timer is released under the MIT license. See [LICENSE][License] for details.
155+
156+
<br><br>
157+
158+
# Our other open source SwiftUI libraries
159+
[PopupView] - The most powerful popup library that allows you to present any popup
160+
<br>
161+
[Navigattie] - Easier and cleaner way of navigating through your app
162+
<br>
163+
[GridView] - Lay out your data with no effort
164+
165+
166+
167+
168+
[MIT]: https://en.wikipedia.org/wiki/MIT_License
169+
[SPM]: https://www.swift.org/package-manager
170+
171+
[Demo]: https://github.com/Mijick/Timer-Demo
172+
[License]: https://github.com/Mijick/Timer/blob/main/LICENSE
173+
174+
[PopupView]: https://github.com/Mijick/PopupView
175+
[Navigattie]: https://github.com/Mijick/Navigattie
176+
[GridView]: https://github.com/Mijick/GridView
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// NotificationCenter++.swift of Timer
3+
//
4+
// Created by Tomasz Kurylik
5+
// - Twitter: https://twitter.com/tkurylik
6+
// - Mail: tomasz.kurylik@mijick.com
7+
// - GitHub: https://github.com/FulcrumOne
8+
//
9+
// Copyright ©2023 Mijick. Licensed under MIT License.
10+
11+
12+
import SwiftUI
13+
14+
extension NotificationCenter {
15+
static func addAppStateNotifications(_ observer: Any, onDidEnterBackground backgroundNotification: Selector, onWillEnterForeground foregroundNotification: Selector) {
16+
Self.default.addObserver(observer, selector: (backgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil)
17+
Self.default.addObserver(observer, selector: (foregroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
18+
}
19+
static func removeAppStateChangedNotifications(_ observer: Any) {
20+
Self.default.removeObserver(observer, name: UIApplication.didEnterBackgroundNotification, object: nil)
21+
Self.default.removeObserver(observer, name: UIApplication.willEnterForegroundNotification, object: nil)
22+
}
23+
}

‎Sources/Internal/MTime.swift

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// MTime.swift of Timer
3+
//
4+
// Created by Tomasz Kurylik
5+
// - Twitter: https://twitter.com/tkurylik
6+
// - Mail: tomasz.kurylik@mijick.com
7+
// - GitHub: https://github.com/FulcrumOne
8+
//
9+
// Copyright ©2023 Mijick. Licensed under MIT License.
10+
11+
12+
import Foundation
13+
14+
public struct MTime: Equatable {
15+
public let hours: Int
16+
public let minutes: Int
17+
public let seconds: Int
18+
public let milliseconds: Int
19+
}
20+
extension MTime {
21+
init(_ timeInterval: TimeInterval) {
22+
let millisecondsInt = Int(timeInterval * 1000)
23+
24+
let hoursDiv = 1000 * 60 * 60
25+
let minutesDiv = 1000 * 60
26+
let secondsDiv = 1000
27+
let millisecondsDiv = 1
28+
29+
hours = millisecondsInt / hoursDiv
30+
minutes = (millisecondsInt % hoursDiv) / minutesDiv
31+
seconds = (millisecondsInt % hoursDiv % minutesDiv) / secondsDiv
32+
milliseconds = (millisecondsInt % hoursDiv % minutesDiv % secondsDiv) / millisecondsDiv
33+
}
34+
}
35+
36+
// MARK: - Helpers
37+
extension MTime {
38+
var defaultTimeFormatter: DateComponentsFormatter {
39+
let formatter = DateComponentsFormatter()
40+
41+
formatter.allowedUnits = [.hour, .minute, .second]
42+
formatter.unitsStyle = .positional
43+
formatter.zeroFormattingBehavior = .pad
44+
formatter.maximumUnitCount = 0
45+
formatter.allowsFractionalUnits = false
46+
formatter.collapsesLargestUnit = false
47+
48+
return formatter
49+
}
50+
}

‎Sources/Internal/MTimer.swift

+179
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//
2+
// MTimer.swift of Timer
3+
//
4+
// Created by Tomasz Kurylik
5+
// - Twitter: https://twitter.com/tkurylik
6+
// - Mail: tomasz.kurylik@mijick.com
7+
// - GitHub: https://github.com/FulcrumOne
8+
//
9+
// Copyright ©2023 Mijick. Licensed under MIT License.
10+
11+
12+
import SwiftUI
13+
14+
public final class MTimer {
15+
static let shared: MTimer = .init()
16+
17+
// Current State
18+
var internalTimer: Timer?
19+
var isTimerRunning: Bool = false
20+
var runningTime: TimeInterval = 0
21+
var backgroundTransitionDate: Date? = nil
22+
23+
// Configuration
24+
var initialTime: (start: TimeInterval, end: TimeInterval) = (0, 1)
25+
var publisherTime: TimeInterval = 0
26+
var publisherTimeTolerance: TimeInterval = 0.4
27+
var onRunningTimeChange: ((MTime) -> ())!
28+
var onTimerActivityChange: ((Bool) -> ())?
29+
var onTimerProgressChange: ((Double) -> ())?
30+
31+
deinit { internalTimer?.invalidate() }
32+
}
33+
34+
35+
// MARK: - Initialising Timer
36+
extension MTimer {
37+
func checkRequirementsForInitialisingTimer(_ publisherTime: TimeInterval) throws {
38+
if publisherTime < 0.001 { throw Error.publisherTimeCannotBeLessThanOneMillisecond }
39+
}
40+
func assignInitialPublisherValues(_ time: TimeInterval, _ tolerance: TimeInterval, _ completion: @escaping (MTime) -> ()) {
41+
publisherTime = time
42+
publisherTimeTolerance = tolerance
43+
onRunningTimeChange = completion
44+
}
45+
}
46+
47+
// MARK: - Starting Timer
48+
extension MTimer {
49+
func checkRequirementsForStartingTimer(_ startTime: TimeInterval, _ endTime: TimeInterval) throws {
50+
if startTime < 0 || endTime < 0 { throw Error.timeCannotBeLessThanZero }
51+
if startTime == endTime { throw Error.startTimeCannotBeTheSameAsEndTime }
52+
53+
if isTimerRunning && backgroundTransitionDate == nil { throw Error.timerIsAlreadyRunning }
54+
}
55+
func assignInitialStartValues(_ startTime: TimeInterval, _ endTime: TimeInterval) {
56+
initialTime = (startTime, endTime)
57+
runningTime = startTime
58+
}
59+
func startTimer() { handleTimer(start: true) }
60+
}
61+
62+
// MARK: - Resuming Timer
63+
extension MTimer {
64+
func checkRequirementsForResumingTimer() throws {
65+
if onRunningTimeChange == nil { throw Error.cannotResumeNotInitialisedTimer }
66+
}
67+
}
68+
69+
// MARK: - Stopping Timer
70+
extension MTimer {
71+
func stopTimer() { handleTimer(start: false) }
72+
}
73+
74+
// MARK: - Resetting Timer
75+
extension MTimer {
76+
func resetRunningTime() { runningTime = initialTime.start }
77+
}
78+
79+
80+
// MARK: - Handling Timer
81+
private extension MTimer {
82+
func handleTimer(start: Bool) { if !start || canTimerBeStarted {
83+
isTimerRunning = start
84+
updateInternalTimer(start)
85+
updateObservers(start)
86+
publishTimerStatus()
87+
}}
88+
}
89+
private extension MTimer {
90+
func updateInternalTimer(_ start: Bool) { DispatchQueue.main.async { [self] in switch start {
91+
case true: internalTimer = .scheduledTimer(withTimeInterval: publisherTime, repeats: true, block: handleTimeChange); internalTimer?.tolerance = publisherTimeTolerance
92+
case false: internalTimer?.invalidate()
93+
}}}
94+
func updateObservers(_ start: Bool) { switch start {
95+
case true: addObservers()
96+
case false: removeObservers()
97+
}}
98+
}
99+
100+
// MARK: - Handling Time Change
101+
private extension MTimer {
102+
func handleTimeChange(_ timeChange: Any? = nil) {
103+
runningTime = calculateNewRunningTime(timeChange as? TimeInterval ?? publisherTime)
104+
stopTimerIfNecessary()
105+
publishRunningTimeChange()
106+
}
107+
}
108+
private extension MTimer {
109+
func calculateNewRunningTime(_ timeChange: TimeInterval) -> TimeInterval {
110+
let newRunningTime = runningTime + timeChange * timeIncrementMultiplier
111+
return timeIncrementMultiplier == -1 ? max(newRunningTime, initialTime.end) : min(newRunningTime, initialTime.end)
112+
}
113+
func stopTimerIfNecessary() { if !canTimerBeStarted {
114+
stopTimer()
115+
}}
116+
}
117+
118+
// MARK: - Handling Background Mode
119+
private extension MTimer {
120+
func addObservers() {
121+
NotificationCenter.addAppStateNotifications(self, onDidEnterBackground: #selector(didEnterBackgroundNotification), onWillEnterForeground: #selector(willEnterForegroundNotification))
122+
}
123+
func removeObservers() {
124+
NotificationCenter.removeAppStateChangedNotifications(self)
125+
}
126+
}
127+
private extension MTimer {
128+
@objc func didEnterBackgroundNotification() {
129+
internalTimer?.invalidate()
130+
backgroundTransitionDate = .init()
131+
}
132+
@objc func willEnterForegroundNotification() {
133+
handleReturnFromBackgroundWhenTimerIsRunning()
134+
backgroundTransitionDate = nil
135+
}
136+
}
137+
private extension MTimer {
138+
func handleReturnFromBackgroundWhenTimerIsRunning() { if let backgroundTransitionDate, isTimerRunning {
139+
let timeChange = Date().timeIntervalSince(backgroundTransitionDate)
140+
141+
handleTimeChange(timeChange)
142+
resumeTimerAfterReturningFromBackground()
143+
}}
144+
}
145+
private extension MTimer {
146+
func resumeTimerAfterReturningFromBackground() { if canTimerBeStarted {
147+
updateInternalTimer(true)
148+
}}
149+
}
150+
151+
// MARK: - Publishers
152+
private extension MTimer {
153+
func publishTimerStatus() {
154+
publishTimerStatusChange()
155+
publishRunningTimeChange()
156+
}
157+
}
158+
private extension MTimer {
159+
func publishTimerStatusChange() { DispatchQueue.main.async { [self] in
160+
onTimerActivityChange?(isTimerRunning)
161+
}}
162+
func publishRunningTimeChange() { DispatchQueue.main.async { [self] in
163+
onRunningTimeChange?(.init(runningTime))
164+
onTimerProgressChange?(calculateTimerProgress())
165+
}}
166+
}
167+
private extension MTimer {
168+
func calculateTimerProgress() -> Double {
169+
let timerTotalTime = max(initialTime.start, initialTime.end) - min(initialTime.start, initialTime.end)
170+
let timerRunningTime = abs(runningTime - initialTime.start)
171+
return timerRunningTime / timerTotalTime
172+
}
173+
}
174+
175+
// MARK: - Others
176+
private extension MTimer {
177+
var canTimerBeStarted: Bool { runningTime != initialTime.end }
178+
var timeIncrementMultiplier: Double { initialTime.start > initialTime.end ? -1 : 1 }
179+
}

‎Sources/Public/Public+MTime.swift

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// Public+MTime.swift of Timer
3+
//
4+
// Created by Tomasz Kurylik
5+
// - Twitter: https://twitter.com/tkurylik
6+
// - Mail: tomasz.kurylik@mijick.com
7+
// - GitHub: https://github.com/FulcrumOne
8+
//
9+
// Copyright ©2023 Mijick. Licensed under MIT License.
10+
11+
12+
import Foundation
13+
14+
// MARK: - Initialisation
15+
extension MTime {
16+
public init(hours: Double = 0, minutes: Double = 0, seconds: Double = 0, milliseconds: Int = 0) {
17+
let hoursInterval = hours * 60 * 60
18+
let minutesInterval = minutes * 60
19+
let secondsInterval = seconds
20+
let millisecondsInterval = Double(milliseconds) / 1000
21+
22+
let timeInterval = hoursInterval + minutesInterval + secondsInterval + millisecondsInterval
23+
self.init(timeInterval)
24+
}
25+
public static var zero: MTime { .init() }
26+
public static var max: MTime { .init(hours: 60 * 60 * 24 * 365 * 100) }
27+
}
28+
29+
// MARK: - Converting to TimeInterval
30+
extension MTime {
31+
public func toTimeInterval() -> TimeInterval {
32+
let hoursAsTimeInterval = 60 * 60 * TimeInterval(hours)
33+
let minutesAsTimeInterval = 60 * TimeInterval(minutes)
34+
let secondsAsTimeInterval = 1 * TimeInterval(seconds)
35+
let millisecondsAsTimeInterval = 0.001 * TimeInterval(milliseconds)
36+
37+
return hoursAsTimeInterval + minutesAsTimeInterval + secondsAsTimeInterval + millisecondsAsTimeInterval
38+
}
39+
}
40+
41+
// MARK: - Converting To String
42+
extension MTime {
43+
/// Converts the object to a string representation. Output can be customised by modifying the formatter block.
44+
public func toString(_ formatter: (DateComponentsFormatter) -> DateComponentsFormatter = { $0 }) -> String {
45+
formatter(defaultTimeFormatter).string(from: toTimeInterval()) ?? ""
46+
}
47+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// Public+MTimer.Error.swift of Timer
3+
//
4+
// Created by Tomasz Kurylik
5+
// - Twitter: https://twitter.com/tkurylik
6+
// - Mail: tomasz.kurylik@mijick.com
7+
// - GitHub: https://github.com/FulcrumOne
8+
//
9+
// Copyright ©2023 Mijick. Licensed under MIT License.
10+
11+
12+
import Foundation
13+
14+
extension MTimer { public enum Error: Swift.Error {
15+
case publisherTimeCannotBeLessThanOneMillisecond
16+
case startTimeCannotBeTheSameAsEndTime, timeCannotBeLessThanZero
17+
case cannotResumeNotInitialisedTimer
18+
case timerIsAlreadyRunning
19+
}}

‎Sources/Public/Public+MTimer.swift

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//
2+
// Public+MTimer.swift of Timer
3+
//
4+
// Created by Tomasz Kurylik
5+
// - Twitter: https://twitter.com/tkurylik
6+
// - Mail: tomasz.kurylik@mijick.com
7+
// - GitHub: https://github.com/FulcrumOne
8+
//
9+
// Copyright ©2023 Mijick. Licensed under MIT License.
10+
11+
12+
import SwiftUI
13+
14+
// MARK: - Creating New Instance Of Timer
15+
extension MTimer {
16+
/// Allows to create multiple instances of a timer.
17+
public static func createNewInstance() -> MTimer { .init() }
18+
}
19+
20+
// MARK: - Initialising Timer
21+
extension MTimer {
22+
/// Prepares the timer to start.
23+
/// WARNING: Use the start() method to start the timer.
24+
public static func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, _ completion: @escaping (_ currentTime: MTime) -> ()) throws -> MTimer {
25+
try shared.publish(every: time, tolerance: tolerance, completion)
26+
}
27+
/// Prepares the timer to start.
28+
/// WARNING: Use the start() method to start the timer.
29+
public static func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, currentTime: Binding<MTime>) throws -> MTimer {
30+
try shared.publish(every: time, tolerance: tolerance) { currentTime.wrappedValue = $0 }
31+
}
32+
/// Prepares the timer to start.
33+
/// WARNING: Use the start() method to start the timer.
34+
public func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, currentTime: Binding<MTime>) throws -> MTimer {
35+
try publish(every: time, tolerance: tolerance) { currentTime.wrappedValue = $0 }
36+
}
37+
/// Prepares the timer to start.
38+
/// WARNING: Use the start() method to start the timer.
39+
public func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, _ completion: @escaping (_ currentTime: MTime) -> ()) throws -> MTimer {
40+
try checkRequirementsForInitialisingTimer(time)
41+
assignInitialPublisherValues(time, tolerance, completion)
42+
return self
43+
}
44+
}
45+
46+
// MARK: - Starting Timer
47+
extension MTimer {
48+
/// Starts the timer using the specified initial values. Can be run backwards - use any "to" value that is greater than "from".
49+
public func start(from startTime: MTime = .zero, to endTime: MTime = .max) throws {
50+
try start(from: startTime.toTimeInterval(), to: endTime.toTimeInterval())
51+
}
52+
/// Starts the timer using the specified initial values. Can be run backwards - use any "to" value that is greater than "from".
53+
public func start(from startTime: TimeInterval = 0, to endTime: TimeInterval = .infinity) throws {
54+
try checkRequirementsForStartingTimer(startTime, endTime)
55+
assignInitialStartValues(startTime, endTime)
56+
startTimer()
57+
}
58+
/// Starts the timer.
59+
public func start() throws {
60+
try start(from: .zero, to: .infinity)
61+
}
62+
}
63+
64+
// MARK: - Stopping Timer
65+
extension MTimer {
66+
/// Stops the timer.
67+
public static func stop() {
68+
shared.stop()
69+
}
70+
/// Stops the timer.
71+
public func stop() {
72+
stopTimer()
73+
}
74+
}
75+
76+
// MARK: - Resuming Timer
77+
extension MTimer {
78+
/// Resumes the stopped timer.
79+
public static func resume() throws {
80+
try shared.resume()
81+
}
82+
/// Resumes the stopped timer.
83+
public func resume() throws {
84+
try checkRequirementsForResumingTimer()
85+
startTimer()
86+
}
87+
}
88+
89+
// MARK: - Resetting Timer
90+
extension MTimer {
91+
/// Stops the timer and resets its current time to the initial value.
92+
public static func reset() {
93+
shared.reset()
94+
}
95+
/// Stops the timer and resets its current time to the initial value.
96+
public func reset() {
97+
resetRunningTime()
98+
stopTimer()
99+
}
100+
}
101+
102+
// MARK: - Publishing Timer Activity Status
103+
extension MTimer {
104+
/// Publishes the timer activity changes.
105+
public func onTimerActivityChange(_ action: @escaping (_ isRunning: Bool) -> ()) -> MTimer {
106+
onTimerActivityChange = action
107+
return self
108+
}
109+
/// Publishes the timer activity changes.
110+
public func bindTimerStatus(isTimerRunning: Binding<Bool>) -> MTimer {
111+
onTimerActivityChange { isTimerRunning.wrappedValue = $0 }
112+
}
113+
}
114+
115+
// MARK: - Publishing Timer Progress
116+
extension MTimer {
117+
/// Publishes the timer progress changes.
118+
public func onTimerProgressChange(_ action: @escaping (_ progress: Double) -> ()) -> MTimer {
119+
onTimerProgressChange = action
120+
return self
121+
}
122+
/// Publishes the timer progress changes.
123+
public func bindTimerProgress(progress: Binding<Double>) -> MTimer {
124+
onTimerProgressChange { progress.wrappedValue = $0 }
125+
}
126+
}

‎Tests/MTimeTests.swift

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
//
2+
// MTimeTests.swift of Timer
3+
//
4+
// Created by Tomasz Kurylik
5+
// - Twitter: https://twitter.com/tkurylik
6+
// - Mail: tomasz.kurylik@mijick.com
7+
// - GitHub: https://github.com/FulcrumOne
8+
//
9+
// Copyright ©2023 Mijick. Licensed under MIT License.
10+
11+
12+
import XCTest
13+
@testable import MijickTimer
14+
15+
final class MTimeTests: XCTestCase {}
16+
17+
// MARK: - Initialisation from TimeInterval
18+
extension MTimeTests {
19+
func testTimeInitialisesCorrectly_1second() {
20+
let time = MTime(1)
21+
22+
XCTAssertEqual(time.hours, 0)
23+
XCTAssertEqual(time.minutes, 0)
24+
XCTAssertEqual(time.seconds, 1)
25+
XCTAssertEqual(time.milliseconds, 0)
26+
}
27+
func testTimeInitialisesCorrectly_59seconds120milliseconds() {
28+
let time = MTime(59.12)
29+
30+
XCTAssertEqual(time.hours, 0)
31+
XCTAssertEqual(time.minutes, 0)
32+
XCTAssertEqual(time.seconds, 59)
33+
XCTAssertEqual(time.milliseconds, 120)
34+
}
35+
func testTimeInitialisesCorrectly_21minutes37seconds() {
36+
let time = MTime(1297)
37+
38+
XCTAssertEqual(time.hours, 0)
39+
XCTAssertEqual(time.minutes, 21)
40+
XCTAssertEqual(time.seconds, 37)
41+
XCTAssertEqual(time.milliseconds, 0)
42+
}
43+
func testTimeInitialisesCorrectly_1hour39minutes17seconds140milliseconds() {
44+
let time = MTime(5957.14)
45+
46+
XCTAssertEqual(time.hours, 1)
47+
XCTAssertEqual(time.minutes, 39)
48+
XCTAssertEqual(time.seconds, 17)
49+
XCTAssertEqual(time.milliseconds, 140)
50+
}
51+
}
52+
53+
// MARK: - Initialisation from Values
54+
extension MTimeTests {
55+
func testTimeInitialisesCorrectly_140milliseconds() {
56+
let time = MTime(milliseconds: 140)
57+
58+
XCTAssertEqual(time.hours, 0)
59+
XCTAssertEqual(time.minutes, 0)
60+
XCTAssertEqual(time.seconds, 0)
61+
XCTAssertEqual(time.milliseconds, 140)
62+
}
63+
func testTimeInitialisesCorrectly_0point3seconds() {
64+
let time = MTime(seconds: 0.3)
65+
66+
XCTAssertEqual(time.hours, 0)
67+
XCTAssertEqual(time.minutes, 0)
68+
XCTAssertEqual(time.seconds, 0)
69+
XCTAssertEqual(time.milliseconds, 300)
70+
}
71+
func testTimeInitialisesCorrectly_31seconds() {
72+
let time = MTime(seconds: 31.0)
73+
74+
XCTAssertEqual(time.hours, 0)
75+
XCTAssertEqual(time.minutes, 0)
76+
XCTAssertEqual(time.seconds, 31)
77+
XCTAssertEqual(time.milliseconds, 0)
78+
}
79+
func testTimeInitialisesCorrectly_31point5seconds() {
80+
let time = MTime(seconds: 31.5)
81+
82+
XCTAssertEqual(time.hours, 0)
83+
XCTAssertEqual(time.minutes, 0)
84+
XCTAssertEqual(time.seconds, 31)
85+
XCTAssertEqual(time.milliseconds, 500)
86+
}
87+
func testTimeInitialisesCorrectly_107seconds() {
88+
let time = MTime(seconds: 107.0)
89+
90+
XCTAssertEqual(time.hours, 0)
91+
XCTAssertEqual(time.minutes, 1)
92+
XCTAssertEqual(time.seconds, 47)
93+
XCTAssertEqual(time.milliseconds, 0)
94+
}
95+
func testTimeInitialisesCorrectly_1point5minutes() {
96+
let time = MTime(minutes: 1.5)
97+
98+
XCTAssertEqual(time.hours, 0)
99+
XCTAssertEqual(time.minutes, 1)
100+
XCTAssertEqual(time.seconds, 30)
101+
XCTAssertEqual(time.milliseconds, 0)
102+
}
103+
func testTimeInitialisesCorrectly_69minutes() {
104+
let time = MTime(minutes: 69.0)
105+
106+
XCTAssertEqual(time.hours, 1)
107+
XCTAssertEqual(time.minutes, 9)
108+
XCTAssertEqual(time.seconds, 0)
109+
XCTAssertEqual(time.milliseconds, 0)
110+
}
111+
func testTimeInitialisesCorrectly_3hours72minutes21seconds14milliseconds() {
112+
let time = MTime(hours: 3.0, minutes: 72.0, seconds: 21.0, milliseconds: 14)
113+
114+
XCTAssertEqual(time.hours, 4)
115+
XCTAssertEqual(time.minutes, 12)
116+
XCTAssertEqual(time.seconds, 21)
117+
XCTAssertEqual(time.milliseconds, 14)
118+
}
119+
}
120+
121+
// MARK: - Converting to TimeInterval
122+
extension MTimeTests {
123+
func testTimeConvertsCorrectly_ToTimeInterval_13milliseconds() {
124+
let time = MTime(hours: 0, minutes: 0, seconds: 0, milliseconds: 13)
125+
126+
XCTAssertEqual(time.toTimeInterval(), 0.013, accuracy: 0.001)
127+
}
128+
func testTimeConvertsCorrectly_ToTimeInterval_33seconds() {
129+
let time = MTime(hours: 0, minutes: 0, seconds: 33, milliseconds: 0)
130+
131+
XCTAssertEqual(time.toTimeInterval(), 33, accuracy: 0.001)
132+
}
133+
func testTimeConvertsCorrectly_ToTimeInterval_1minute9seconds() {
134+
let time = MTime(hours: 0, minutes: 0, seconds: 69, milliseconds: 0)
135+
136+
XCTAssertEqual(time.toTimeInterval(), 69, accuracy: 0.001)
137+
}
138+
func testTimeConvertsCorrectly_ToTimeInterval_1hour13minutes14seconds() {
139+
let time = MTime(hours: 1, minutes: 13, seconds: 14, milliseconds: 0)
140+
141+
XCTAssertEqual(time.toTimeInterval(), 4394, accuracy: 0.001)
142+
}
143+
func testTimeConvertsCorrectly_ToTimeInterval_33hours58minutes32seconds141milliseconds() {
144+
let time = MTime(hours: 33, minutes: 58, seconds: 32, milliseconds: 141)
145+
146+
XCTAssertEqual(time.toTimeInterval(), 122312.141, accuracy: 0.001)
147+
}
148+
}
149+
150+
// MARK: - Converting to String
151+
extension MTimeTests {
152+
func testTimeConvertsCorrectly_ToString_3seconds() {
153+
let time = MTime(hours: 0, minutes: 0, seconds: 3, milliseconds: 0)
154+
155+
XCTAssertEqual(time.toString(), "00:00:03")
156+
}
157+
func testTimeConvertsCorrectly_ToString_12minutes_33seconds() {
158+
let time = MTime(hours: 0, minutes: 12, seconds: 33, milliseconds: 0)
159+
160+
XCTAssertEqual(time.toString(), "00:12:33")
161+
}
162+
func testTimeConvertsCorrectly_ToString_1hour_3minutes_17seconds() {
163+
let time = MTime(hours: 1, minutes: 3, seconds: 17, milliseconds: 0)
164+
165+
XCTAssertEqual(time.toString(), "01:03:17")
166+
}
167+
func testTimeConvertsCorrectly_ToString_31hours_1minute_21seconds() {
168+
let time = MTime(hours: 31, minutes: 1, seconds: 21, milliseconds: 0)
169+
170+
XCTAssertEqual(time.toString(), "31:01:21")
171+
}
172+
}

‎Tests/MTimerTests.swift

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
//
2+
// MTimerTests.swift of Timer
3+
//
4+
// Created by Tomasz Kurylik
5+
// - Twitter: https://twitter.com/tkurylik
6+
// - Mail: tomasz.kurylik@mijick.com
7+
// - GitHub: https://github.com/FulcrumOne
8+
//
9+
// Copyright ©2023 Mijick. Licensed under MIT License.
10+
11+
12+
import XCTest
13+
@testable import MijickTimer
14+
15+
final class MTimerTests: XCTestCase {
16+
var currentTime: TimeInterval = 0
17+
18+
override func setUp() { MTimer.stop() }
19+
}
20+
21+
// MARK: - Basics
22+
extension MTimerTests {
23+
func testTimerStarts() {
24+
try! defaultTimer.start()
25+
wait(for: defaultWaitingTime)
26+
27+
XCTAssertGreaterThan(currentTime, 0)
28+
}
29+
func testTimerIsCancellable() {
30+
try! defaultTimer.start()
31+
wait(for: defaultWaitingTime)
32+
33+
MTimer.stop()
34+
wait(for: defaultWaitingTime)
35+
36+
let timeAfterStop = currentTime
37+
wait(for: defaultWaitingTime)
38+
39+
XCTAssertEqual(timeAfterStop, currentTime)
40+
}
41+
func testTimerIsResetable() {
42+
let startTime: TimeInterval = 3
43+
44+
try! defaultTimer.start(from: startTime)
45+
wait(for: defaultWaitingTime)
46+
47+
XCTAssertNotEqual(currentTime, startTime)
48+
49+
wait(for: defaultWaitingTime)
50+
MTimer.reset()
51+
wait(for: defaultWaitingTime)
52+
53+
XCTAssertEqual(startTime, currentTime)
54+
}
55+
func testTimerCanBeResumed() {
56+
try! defaultTimer.start()
57+
wait(for: defaultWaitingTime)
58+
59+
MTimer.stop()
60+
let timeAfterStop = currentTime
61+
wait(for: defaultWaitingTime)
62+
63+
try! MTimer.resume()
64+
wait(for: defaultWaitingTime)
65+
66+
XCTAssertNotEqual(timeAfterStop, currentTime)
67+
}
68+
}
69+
70+
// MARK: - Additional Basics
71+
extension MTimerTests {
72+
func testTimerShouldPublishAccurateValuesWithZeroTolerance() {
73+
try! MTimer
74+
.publish(every: 0.1, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
75+
.start()
76+
wait(for: 0.6)
77+
78+
XCTAssertEqual(currentTime, 0.6)
79+
}
80+
func testTimerShouldPublishInaccurateValuesWithNonZeroTolerance() {
81+
try! defaultTimer.start()
82+
wait(for: 1)
83+
84+
XCTAssertNotEqual(currentTime, 1)
85+
}
86+
func testTimerCanRunBackwards() {
87+
try! defaultTimer.start(from: 3, to: 1)
88+
wait(for: defaultWaitingTime)
89+
90+
XCTAssertLessThan(currentTime, 3)
91+
}
92+
func testTimerPublishesStatuses() {
93+
var statuses: [Bool: Bool] = [true: false, false: false]
94+
95+
try! defaultTimer
96+
.onTimerActivityChange { statuses[$0] = true }
97+
.start()
98+
wait(for: defaultWaitingTime)
99+
100+
MTimer.stop()
101+
wait(for: defaultWaitingTime)
102+
103+
XCTAssertTrue(statuses.values.filter { !$0 }.isEmpty)
104+
}
105+
func testTimerIncreasesTimeCorrectly_WhenGoesForward() {
106+
try! defaultTimer.start(from: 0, to: 10)
107+
wait(for: 0.8)
108+
109+
XCTAssertGreaterThan(currentTime, 0)
110+
XCTAssertLessThan(currentTime, 10)
111+
}
112+
func testTimerIncreasesTimeCorrectly_WhenGoesBackward() {
113+
try! defaultTimer.start(from: 10, to: 0)
114+
wait(for: 0.8)
115+
116+
XCTAssertGreaterThan(currentTime, 0)
117+
XCTAssertLessThan(currentTime, 10)
118+
}
119+
func testTimerStopsAutomatically_WhenGoesForward() {
120+
try! defaultTimer.start(from: 0, to: 0.25)
121+
wait(for: 0.8)
122+
123+
XCTAssertEqual(currentTime, 0.25)
124+
}
125+
func testTimerStopsAutomatically_WhenGoesBackward() {
126+
try! defaultTimer.start(from: 3, to: 2.75)
127+
wait(for: 0.8)
128+
129+
XCTAssertEqual(currentTime, 2.75)
130+
}
131+
func testTimerStopsAutomatically_WhenGoesBackward_DoesNotExceedZero() {
132+
try! defaultTimer.start(from: 0.25, to: 0)
133+
wait(for: 1.2)
134+
135+
XCTAssertEqual(currentTime, 0)
136+
}
137+
func testTimerCanHaveMultipleInstances() {
138+
var newTime: TimeInterval = 0
139+
140+
let newTimer = MTimer.createNewInstance()
141+
try! newTimer
142+
.publish(every: 0.3) { newTime = $0.toTimeInterval() }
143+
.start(from: 10, to: 100)
144+
try! defaultTimer.start(from: 0, to: 100)
145+
146+
wait(for: 1)
147+
148+
XCTAssertGreaterThan(newTime, 10)
149+
XCTAssertGreaterThan(currentTime, 0)
150+
XCTAssertNotEqual(newTime, currentTime)
151+
}
152+
func testNewInstanceTimerCanBeStopped() {
153+
let newTimer = MTimer.createNewInstance()
154+
155+
try! newTimer
156+
.publish(every: 0.1) { self.currentTime = $0.toTimeInterval() }
157+
.start()
158+
wait(for: defaultWaitingTime)
159+
160+
newTimer.stop()
161+
wait(for: defaultWaitingTime)
162+
163+
let timeAfterStop = currentTime
164+
wait(for: defaultWaitingTime)
165+
166+
XCTAssertGreaterThan(currentTime, 0)
167+
XCTAssertEqual(timeAfterStop, currentTime)
168+
}
169+
}
170+
171+
// MARK: - Progress
172+
extension MTimerTests {
173+
func testTimerProgressCountsCorrectly_From0To10() {
174+
var progress: Double = 0
175+
176+
try! MTimer
177+
.publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
178+
.onTimerProgressChange { progress = $0 }
179+
.start(from: 0, to: 10)
180+
wait(for: 1)
181+
182+
XCTAssertEqual(progress, 0.1)
183+
}
184+
func testTimerProgressCountsCorrectly_From10To29() {
185+
var progress: Double = 0
186+
187+
try! MTimer
188+
.publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
189+
.onTimerProgressChange { progress = $0 }
190+
.start(from: 10, to: 29)
191+
wait(for: 1)
192+
193+
XCTAssertEqual(progress, 1/19)
194+
}
195+
func testTimerProgressCountsCorrectly_From31To100() {
196+
var progress: Double = 0
197+
198+
try! MTimer
199+
.publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
200+
.onTimerProgressChange { progress = $0 }
201+
.start(from: 31, to: 100)
202+
wait(for: 1)
203+
204+
XCTAssertEqual(progress, 1/69)
205+
}
206+
func testTimerProgressCountsCorrectly_From100To0() {
207+
var progress: Double = 0
208+
209+
try! MTimer
210+
.publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
211+
.onTimerProgressChange { progress = $0 }
212+
.start(from: 100, to: 0)
213+
wait(for: 1.5)
214+
215+
XCTAssertEqual(progress, 1.5/100)
216+
}
217+
func testTimerProgressCountsCorrectly_From31To14() {
218+
var progress: Double = 0
219+
220+
try! MTimer
221+
.publish(every: 0.25, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
222+
.onTimerProgressChange { progress = $0 }
223+
.start(from: 31, to: 14)
224+
wait(for: 1)
225+
226+
XCTAssertEqual(progress, 1/17)
227+
}
228+
}
229+
230+
// MARK: - Errors
231+
extension MTimerTests {
232+
func testTimerCannotBeInitialised_PublishTimeIsTooLess() {
233+
XCTAssertThrowsError(try MTimer.publish(every: 0.0001, { _ in })) { error in
234+
let error = error as! MTimer.Error
235+
XCTAssertEqual(error, .publisherTimeCannotBeLessThanOneMillisecond)
236+
}
237+
}
238+
func testTimerDoesNotStart_StartTimeEqualsEndTime() {
239+
XCTAssertThrowsError(try defaultTimer.start(from: 0, to: 0)) { error in
240+
let error = error as! MTimer.Error
241+
XCTAssertEqual(error, .startTimeCannotBeTheSameAsEndTime)
242+
}
243+
}
244+
func testTimerDoesNotStart_StartTimeIsLessThanZero() {
245+
XCTAssertThrowsError(try defaultTimer.start(from: -10, to: 5)) { error in
246+
let error = error as! MTimer.Error
247+
XCTAssertEqual(error, .timeCannotBeLessThanZero)
248+
}
249+
}
250+
func testTimerDoesNotStart_EndTimeIsLessThanZero() {
251+
XCTAssertThrowsError(try defaultTimer.start(from: 10, to: -15)) { error in
252+
let error = error as! MTimer.Error
253+
XCTAssertEqual(error, .timeCannotBeLessThanZero)
254+
}
255+
}
256+
func testCannotResumeTimer_WhenTimerIsNotInitialised() {
257+
XCTAssertThrowsError(try MTimer.resume()) { error in
258+
let error = error as! MTimer.Error
259+
XCTAssertEqual(error, .cannotResumeNotInitialisedTimer)
260+
}
261+
}
262+
func testCannotStartTimer_WhenTimerIsRunning() {
263+
try! defaultTimer.start()
264+
265+
XCTAssertThrowsError(try defaultTimer.start()) { error in
266+
let error = error as! MTimer.Error
267+
XCTAssertEqual(error, .timerIsAlreadyRunning)
268+
}
269+
}
270+
}
271+
272+
273+
// MARK: - Helpers
274+
private extension MTimerTests {
275+
func wait(for duration: TimeInterval) {
276+
let waitExpectation = expectation(description: "Waiting")
277+
278+
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
279+
waitExpectation.fulfill()
280+
}
281+
282+
waitForExpectations(timeout: duration + 0.5)
283+
}
284+
}
285+
private extension MTimerTests {
286+
var defaultWaitingTime: TimeInterval { 0.15 }
287+
var defaultTimer: MTimer { try! .publish(every: 0.05, tolerance: 0.5) { self.currentTime = $0.toTimeInterval() } }
288+
}

0 commit comments

Comments
 (0)
Please sign in to comment.