diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..02e39f6
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @FulcrumOne
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..916f081
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+team@mijick.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..684e27f
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1 @@
+Coming soon...
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..d06b778
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: ❓ Help and Support Discord Channel
+ url: https://discord.com/invite/dT5V7nm5SC
+ about: Please ask and answer questions here
diff --git "a/.github/ISSUE_TEMPLATE/\360\237\232\200-feature-request.md" "b/.github/ISSUE_TEMPLATE/\360\237\232\200-feature-request.md"
new file mode 100644
index 0000000..37c27b9
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/\360\237\232\200-feature-request.md"
@@ -0,0 +1,18 @@
+---
+name: "\U0001F680 Feature Request"
+about: If you have a feature request
+title: "[FREQ]"
+labels: 'feature'
+projects: "Mijick/17"
+assignees: FulcrumOne
+
+---
+
+## Context
+What are you trying to do and how would you want to do it differently? Is it something you currently you cannot do? Is this related to an issue/problem?
+
+## Alternatives
+Can you achieve the same result doing it in an alternative way? Is the alternative considerable?
+
+## If the feature request is approved, would you be willing to submit a PR?
+Yes / No _(Help can be provided if you need assistance submitting a PR)_
diff --git "a/.github/ISSUE_TEMPLATE/\360\237\246\237-bug-report.md" "b/.github/ISSUE_TEMPLATE/\360\237\246\237-bug-report.md"
new file mode 100644
index 0000000..0ccee61
--- /dev/null
+++ "b/.github/ISSUE_TEMPLATE/\360\237\246\237-bug-report.md"
@@ -0,0 +1,42 @@
+---
+name: "\U0001F99F Bug Report"
+about: If something isn't working
+title: "[BUG]"
+labels: 'bug'
+projects: "Mijick/17"
+assignees: FulcrumOne, jay-jay-lama
+
+---
+
+## Prerequisites
+- [ ] I checked the [documentation](https://github.com/Mijick/Timer/wiki) and found no answer
+- [ ] I checked to make sure that this issue has not already been filed
+
+## Expected Behavior
+Please describe the behavior you are expecting
+
+## Current Behavior
+What is the current behavior?
+
+## Steps to Reproduce
+Please provide detailed steps for reproducing the issue.
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+## Code Sample
+If you can, please include a code sample that we can use to debug the bug.
+
+## Screenshots
+If applicable, add screenshots to help explain your problem.
+
+## Context
+Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions.
+
+| Name | Version |
+| ------| ---------|
+| SDK | e.g. 3.0.0 |
+| Xcode | e.g. 14.0 |
+| Operating System | e.g. iOS 18.0 |
+| Device | e.g. iPhone 14 Pro |
diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d9810..0000000
--- a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- IDEDidComputeMac32BitWarning
-
-
-
diff --git a/LICENSE b/LICENSE
index ec86490..7aaaeb7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,201 @@
-MIT License
-
-Copyright (c) 2024 Mijick
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright ©2023 Mijick
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/MijickTimer.podspec b/MijickTimer.podspec
index 439a687..f4c23d4 100644
--- a/MijickTimer.podspec
+++ b/MijickTimer.podspec
@@ -1,18 +1,19 @@
Pod::Spec.new do |s|
s.name = 'MijickTimer'
s.summary = 'Modern API for Timer'
- s.description = 'MijickTimer is a free, open-source library for the Swift language that makes the process of managing timers much easier and clearer.'
+ s.description = 'Timers made simple: The Ultimate Swift Framework for Modern Apps on iOS, macOS, and visionOS.'
- s.version = '1.0.2'
+ s.version = '2.0.0'
s.ios.deployment_target = '13.0'
s.osx.deployment_target = '10.15'
- s.swift_version = '5.0'
+ s.visionos.deployment_target = '1.0'
+ s.swift_version = '6.0'
s.source_files = 'Sources/**/*'
s.frameworks = 'SwiftUI', 'Foundation', 'Combine'
s.homepage = 'https://github.com/Mijick/Timer.git'
s.license = { :type => 'MIT', :file => 'LICENSE' }
- s.author = { 'Mijick' => 'team@mijick.com' }
+ s.author = { 'Alina P. from Mijick' => 'alina.petrovska@mijick.com' }
s.source = { :git => 'https://github.com/Mijick/Timer.git', :tag => s.version.to_s }
end
diff --git a/Package.swift b/Package.swift
index 85206b0..b434338 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -7,7 +7,8 @@ let package = Package(
name: "MijickTimer",
platforms: [
.iOS(.v13),
- .macOS(.v10_15)
+ .macOS(.v10_15),
+ .visionOS(.v1)
],
products: [
.library(name: "MijickTimer", targets: ["MijickTimer"]),
@@ -15,5 +16,6 @@ let package = Package(
targets: [
.target(name: "MijickTimer", dependencies: [], path: "Sources"),
.testTarget(name: "MijickTimerTests", dependencies: ["MijickTimer"], path: "Tests")
- ]
+ ],
+ swiftLanguageModes: [.v6]
)
diff --git a/README.md b/README.md
index 73f49ad..c89cac5 100644
--- a/README.md
+++ b/README.md
@@ -1,208 +1,208 @@
-
-
+
-
-
-
-
-
+
+
+
-
- Modern API for Timer
-
-
- Easy to use yet powerful Timer library. Keep your code clean
+
+
+
Timers made simple
+ Easy to use yet powerful Timer library. Keep your code clean.
+
- Try demo we prepared
+ Try demo we prepared
|
- Roadmap
+ Framework documentation
|
- Propose a new feature
+ Roadmap
+
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-Timer is a free and open-source library dedicated for Swift that makes the process of handling timers easier and much cleaner.
-* **Improves code quality.** Start timer using the `publish().start()` method. Stop the timer with `stop()`. Simple as never.
-* **Run your timer in both directions.** Our Timer can operate in both modes (increasing or decreasing).
-* **Supports background mode.** Don't worry about the timer when the app goes into the background. We handled it!
-* **And much more.** Our library allows you to convert the current time to a string or to display the timer progress in no time.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+# ✨ Features
+
+
+
+
+
+ ⏳
+
+
+ Countdown Timer (Down-Going)
+
+
+
+
+ ⏱️
+
+
+ Count-Up Timer (Elapsed Time)
+
+
+
+
+ ⏸️
+
+
+ Pause Timer
+
+
+
+
+ ▶️
+
+
+ Resume Timer
+
+
+
+
+ ⏭️
+
+
+ Skip Timer
+
+
+
+
+ ⏮️
+
+
+ Cancel Timer
+
+
+
+
+ ⚡
+
+
+ Reactive programming friendly
+
+
+
+
+
+
+
+# ☀️ What Is MijickTimer?
+MijickTimer library is Swift-based library that offers powerful and flexible timer features for iOS and macOS and visionOS apps. It allows to create both countdown and count-up timers with enhanced state management and observation options.
+
+# 💡 Feature Insights
+
+
+
+
Count-Up Timer
+ Track elapsed time seamlessly with a count-up timer. Ideal for productivity, logging or workout apps.
+ Take a look at the implementation details here .
+
+
-# Getting Started
-### ✋ Requirements
+
+
Countdown Timer
+ Easily create countdown timers to track remaining time. Perfect for games, events or task apps.
+ Take a look at the implementation details here .
+
+
-| **Platforms** | **Minimum Swift Version** |
-|:----------|:----------|
-| iOS 13+ | 5.0 |
-| iPadOS 13+ | 5.0 |
-| macOS 10.15+ | 5.0 |
+
+
Control Timer state
+ Pause timers and resume them later without losing progress. It also allows to skip and cancel the progress.
+ Take a look at the implementation details here .
+
+
+
-### ⏳ Installation
-
-#### [Swift package manager][spm]
-Swift package manager is a tool for automating the distribution of Swift code and is integrated into the Swift compiler.
-
-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`.
-
-```Swift
-dependencies: [
- .package(url: "https://github.com/Mijick/Timer", branch(“main”))
-]
-```
-
-#### [Cocoapods][cocoapods]
-Cocoapods is a dependency manager for Swift and Objective-C Cocoa projects that helps to scale them elegantly.
-
-Installation steps:
-- Install CocoaPods 1.10.0 (or later)
-- [Generate CocoaPods][generate_cocoapods] for your project
-```Swift
- pod init
-```
-- Add CocoaPods dependency into your `Podfile`
-```Swift
- pod 'MijickTimer'
-```
-- Install dependency and generate `.xcworkspace` file
-```Swift
- pod install
-```
-- Use new XCode project file `.xcworkspace`
-
+Observe Timer State
+
+
Monitor timer state with a variety of different approaches: binding, callbacks, combine, state value updates.
+ Take a look at the implementation details here .
+
+
-# Usage
-
-### 1. Initialise the timer
-Call the `publish()` method that has three parameters:
-* **time** - The number of seconds between firings of the timer.
-* **tolerance** - The number of seconds after the update date that the timer may fire.
-* **currentTime** - The current timer time.
-```Swift
- try! MTimer.publish(every: 1, currentTime: $currentTime)
-```
-
-### 2. Start the timer
-Start the timer using the `start()` method. You can customise the start and end time using the parameters of this method.
-```Swift
- try! MTimer
- .publish(every: 1, currentTime: $currentTime)
- .start(from: .init(minutes: 21, seconds: 37), to: .zero)
-```
-
-### 3. *(Optional)* Observe TimerStatus and TimerProgress
-You can observe changes in both values by calling either of the methods
-```Swift
- try! MTimer
- .publish(every: 1, currentTime: $currentTime)
- .bindTimerStatus(isTimerRunning: $isTimerRunning)
- .bindTimerProgress(progress: $timerProgress)
- .start(from: .init(minutes: 21, seconds: 37), to: .zero)
-```
-
-### 4. Stop the timer
-Timer can be stopped with `stop()` method.
-```Swift
- MTimer.stop()
-```
-
-### 5. Additional timer controls
-- Once stopped, the timer can be resumed - simply use the `resume()` method.
-```Swift
- try! MTimer.resume()
-```
-- To stop and reset the timer to its initial values, use the `reset()` method.
-```Swift
- MTimer.reset()
-```
-
-### 6. Displaying the current time as String
-You can convert the current MTime to String by calling the `toString()` method. Use the `formatter` parameter to customise the output.
-```Swift
- currentTime.toString {
- $0.unitsStyle = .full
- $0.allowedUnits = [.hour, .minute]
- return $0
- }
-```
-
-### 7. Creating more timer instances
-Create a new instance of the timer and assign it to a new variable. Use the above functions directly with it
-```Swift
- let newTimer = MTimer.createNewInstance()
-
- try! newTimer
- .publish(every: 1, currentTime: $currentTime)
- .start()
-
- newTimer.stop()
-```
-
+
-# Try our demo
-See for yourself how does it work by cloning [project][Demo] we created
-
-# License
-Timer is released under the MIT license. See [LICENSE][License] for details.
-
-
-
-# Our other open source SwiftUI libraries
-[PopupView] - The most powerful popup library that allows you to present any popup
-
-[NavigationView] - Easier and cleaner way of navigating through your app
-
-[CalendarView] - Create your own calendar object in no time
-
-[GridView] - Lay out your data with no effort
-
-[CameraView] - The most powerful CameraController. Designed for SwiftUI
+# ✅ Why MijickTimer?
+Multiple Apple Platform Support:
+
+* iPhone, iPad. Requires iOS 13.0+ .
+* Mac. Requires macOS 10.15+.
+* Apple Vision Pro. Requires visionOS 1.0+.
+
+Built for Swift 6:
+
+* Modern, efficient, and designed for performance.
+
+All-in-One Timer Solution:
+
+* Handles countdowns, count-ups, pausing, resuming and state management seamlessly.
+
+Versatile Observation:
+
+* Choose callbacks, bindings or Combine for the implementation that works best for you.
+* Provides the ability to access the state of a specific timer from any part of the code base.
+
+It's just a cool library 😎
+
+
+# 🚀 How to use it?
+Visit the framework's [documentation](https://link.mijick.com/timer-wiki) to learn how to integrate your project with **MijickTimer**.
+See for yourself how does it work by cloning [project](https://link.mijick.com/timer-demo) we created
+
+# 🍀 Community
+Join the welcoming community of developers on [Discord](https://link.mijick.com/discord).
+
+# 🌼 Contribute
+To contribute a feature or idea to **MijickTimer**, create an [issue](https://github.com/Mijick/Timer/issues/new?assignees=FulcrumOne&labels=state%3A+inactive%2C+type%3A+feature&projects=&template=🚀-feature-request.md&title=%5BFREQ%5D) explaining your idea or bring it up on [Discord](https://discord.com/invite/dT5V7nm5SC).
+If you find a bug, please create an [issue](https://github.com/Mijick/Timer/issues/new?assignees=FulcrumOne%2C+jay-jay-lama&labels=state%3A+inactive%2C+type%3A+bug&projects=&template=🦟-bug-report.md&title=%5BBUG%5D).
+If you would like to contribute, please refer to the [Contribution Guidelines](https://link.mijick.com/contribution-guidelines).
+
+# 💜 Sponsor our work
+Support our work by [becoming a backer](https://link.mijick.com/buymeacoffee).
-[MIT]: https://en.wikipedia.org/wiki/MIT_License
-[SPM]: https://www.swift.org/package-manager
-[cocoapods]: https://cocoapods.org/
-[generate_cocoapods]: https://github.com/square/cocoapods-generate
-
-[Demo]: https://github.com/Mijick/Timer-Demo
-[License]: https://github.com/Mijick/Timer/blob/main/LICENSE
-
-[PopupView]: https://github.com/Mijick/PopupView
-[NavigationView]: https://github.com/Mijick/NavigationView
-[CalendarView]: https://github.com/Mijick/CalendarView
-[GridView]: https://github.com/Mijick/GridView
-[CameraView]: https://github.com/Mijick/CameraView
diff --git a/Sources/Internal/Extensions/NotificationCenter++.swift b/Sources/Internal/Extensions/NotificationCenter++.swift
index 0fc4c6f..974bb78 100644
--- a/Sources/Internal/Extensions/NotificationCenter++.swift
+++ b/Sources/Internal/Extensions/NotificationCenter++.swift
@@ -1,12 +1,12 @@
//
-// NotificationCenter++.swift of Timer
+// NotificationCenter++.swift
+// MijickTimer
//
-// Created by Tomasz Kurylik
-// - Twitter: https://twitter.com/tkurylik
-// - Mail: tomasz.kurylik@mijick.com
-// - GitHub: https://github.com/FulcrumOne
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
//
-// Copyright ©2023 Mijick. Licensed under MIT License.
+// Copyright ©2024 Mijick. All rights reserved.
import SwiftUI
diff --git a/Sources/Internal/Helpers/MTimerCallbacks.swift b/Sources/Internal/Helpers/MTimerCallbacks.swift
new file mode 100644
index 0000000..70a741e
--- /dev/null
+++ b/Sources/Internal/Helpers/MTimerCallbacks.swift
@@ -0,0 +1,17 @@
+//
+// MTimerCallbacks.swift
+// MijickTimer
+//
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
+//
+// Copyright ©2024 Mijick. All rights reserved.
+
+import SwiftUI
+
+class MTimerCallbacks {
+ var onRunningTimeChange: ((MTime) -> ())?
+ var onTimerStatusChange: ((MTimerStatus) -> ())?
+ var onTimerProgressChange: ((Double) -> ())?
+}
diff --git a/Sources/Internal/Helpers/MTimerConfigurationManager.swift b/Sources/Internal/Helpers/MTimerConfigurationManager.swift
new file mode 100644
index 0000000..449524b
--- /dev/null
+++ b/Sources/Internal/Helpers/MTimerConfigurationManager.swift
@@ -0,0 +1,69 @@
+//
+// MTimerConfigurationManager.swift
+// MijickTimer
+//
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
+//
+// Copyright ©2024 Mijick. All rights reserved.
+
+import SwiftUI
+
+class MTimerConfigurationManager {
+ private(set) var time: (start: TimeInterval, end: TimeInterval) = (0, 1)
+ private(set) var publisherTime: TimeInterval = 0
+ private(set) var publisherTimeTolerance: TimeInterval = 0.4
+ private(set) var currentTime: TimeInterval = 0
+}
+
+// MARK: Getters
+extension MTimerConfigurationManager {
+ func getPublisherTime() -> TimeInterval {
+ publisherTime == 0 ? max(time.start, time.end) : publisherTime
+ }
+ func getTimerProgress() -> Double {
+ let timerTotalTime = max(time.start, time.end) - min(time.start, time.end)
+ let timerRunningTime = abs(currentTime - time.start)
+ return timerRunningTime / timerTotalTime
+ }
+}
+
+// MARK: Setters
+extension MTimerConfigurationManager {
+ func setInitialTime(startTime: TimeInterval, endTime: TimeInterval) {
+ time = (startTime, endTime)
+ currentTime = startTime
+ }
+ func setPublishers(time: TimeInterval, tolerance: TimeInterval) {
+ publisherTime = time
+ publisherTimeTolerance = tolerance
+ }
+ func setCurrentTimeToStart() {
+ currentTime = time.start
+ }
+ func setCurrentTimeToEnd() {
+ currentTime = time.end
+ }
+ func setNewCurrentTime(_ timeChange: Any?) {
+ let timeChange = timeChange as? TimeInterval ?? getPublisherTime()
+ let newCurrentTime = currentTime + timeChange * timeIncrementMultiplier
+ currentTime = timeIncrementMultiplier == -1
+ ? max(newCurrentTime, time.end)
+ : min(newCurrentTime, time.end)
+ }
+ func reset() {
+ time = (0, 1)
+ publisherTime = 0
+ publisherTimeTolerance = 0.4
+ currentTime = 0
+ }
+}
+private extension MTimerConfigurationManager {
+ var timeIncrementMultiplier: Double { time.start > time.end ? -1 : 1 }
+}
+
+// MARK: Helpers
+extension MTimerConfigurationManager {
+ var canTimerBeStarted: Bool { currentTime != time.end }
+}
diff --git a/Sources/Internal/Helpers/MTimerContainer.swift b/Sources/Internal/Helpers/MTimerContainer.swift
new file mode 100644
index 0000000..c25fcab
--- /dev/null
+++ b/Sources/Internal/Helpers/MTimerContainer.swift
@@ -0,0 +1,30 @@
+//
+// MTimerContainer.swift
+// MijickTimer
+//
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
+//
+// Copyright ©2024 Mijick. All rights reserved.
+
+@MainActor class MTimerContainer {
+ private static var timers: [MTimer] = []
+}
+
+extension MTimerContainer {
+ static func register(_ timer: MTimer) -> MTimer {
+ if let timer = getTimer(timer.id) { return timer }
+ timers.append(timer)
+ return timer
+ }
+}
+private extension MTimerContainer {
+ static func getTimer(_ id: MTimerID) -> MTimer? {
+ timers.first(where: { $0.id == id })
+ }
+}
+
+extension MTimerContainer {
+ static func resetAll() { timers.forEach { $0.reset() }}
+}
diff --git a/Sources/Internal/Helpers/MTimerStateManager.swift b/Sources/Internal/Helpers/MTimerStateManager.swift
new file mode 100644
index 0000000..fbfb135
--- /dev/null
+++ b/Sources/Internal/Helpers/MTimerStateManager.swift
@@ -0,0 +1,64 @@
+//
+// MTimerStateManager.swift
+// MijickTimer
+//
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
+//
+// Copyright ©2024 Mijick. All rights reserved.
+
+import SwiftUI
+
+class MTimerStateManager {
+ private var internalTimer: Timer?
+ var backgroundTransitionDate: Date? = nil
+}
+
+// MARK: Run Timer
+extension MTimerStateManager {
+ func runTimer(_ configuration: MTimerConfigurationManager, _ target: Any, _ completion: Selector) {
+ stopTimer()
+ runTimer(target, configuration.getPublisherTime(), completion)
+ setTolerance(configuration.publisherTimeTolerance)
+ updateInternalTimerStartAddToRunLoop()
+ }
+}
+private extension MTimerStateManager {
+ func runTimer(_ target: Any, _ timeInterval: TimeInterval, _ completion: Selector) {
+ internalTimer = .scheduledTimer(
+ timeInterval: timeInterval,
+ target: target,
+ selector: completion,
+ userInfo: nil,
+ repeats: true
+ )
+ }
+ func setTolerance(_ value: TimeInterval) {
+ internalTimer?.tolerance = value
+ }
+ func updateInternalTimerStartAddToRunLoop() {
+ #if os(macOS)
+ guard let internalTimer = internalTimer else { return }
+ RunLoop.main.add(internalTimer, forMode: .common)
+ #endif
+ }
+}
+
+// MARK: Stop Timer
+extension MTimerStateManager {
+ func stopTimer() {
+ internalTimer?.invalidate()
+ }
+}
+
+// MARK: App State Handle
+extension MTimerStateManager {
+ func didEnterBackground() {
+ internalTimer?.invalidate()
+ backgroundTransitionDate = .init()
+ }
+ func willEnterForeground() {
+ backgroundTransitionDate = nil
+ }
+}
diff --git a/Sources/Internal/Helpers/MTimerValidator.swift b/Sources/Internal/Helpers/MTimerValidator.swift
new file mode 100644
index 0000000..697fbdd
--- /dev/null
+++ b/Sources/Internal/Helpers/MTimerValidator.swift
@@ -0,0 +1,29 @@
+//
+// MTimerValidator.swift
+// MijickTimer
+//
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
+//
+// Copyright ©2024 Mijick. All rights reserved.
+
+import Foundation
+
+class MTimerValidator {
+ static func checkRequirementsForInitializingTimer(_ publisherTime: TimeInterval) throws {
+ if publisherTime < 0 { throw MTimerError.publisherTimeCannotBeLessThanZero }
+ }
+ static func checkRequirementsForStartingTimer(_ startTime: TimeInterval, _ endTime: TimeInterval, _ state: MTimerStateManager, _ status: MTimerStatus) throws {
+ if startTime < 0 || endTime < 0 { throw MTimerError.timeCannotBeLessThanZero }
+ if startTime == endTime { throw MTimerError.startTimeCannotBeTheSameAsEndTime }
+ if status == .running && state.backgroundTransitionDate == nil { throw MTimerError.timerIsAlreadyRunning }
+ }
+ static func checkRequirementsForResumingTimer(_ callbacks: MTimerCallbacks) throws {
+ if callbacks.onRunningTimeChange == nil { throw MTimerError.cannotResumeNotInitialisedTimer }
+ }
+ static func isCanBeSkipped(_ timerStatus: MTimerStatus) throws {
+ if timerStatus == .running || timerStatus == .paused { return }
+ throw MTimerError.timerIsNotStarted
+ }
+}
diff --git a/Sources/Internal/MTime.swift b/Sources/Internal/MTime.swift
index 2feb36f..6548956 100644
--- a/Sources/Internal/MTime.swift
+++ b/Sources/Internal/MTime.swift
@@ -1,12 +1,12 @@
//
-// MTime.swift of Timer
+// MTime.swift
//
// Created by Tomasz Kurylik
// - Twitter: https://twitter.com/tkurylik
// - Mail: tomasz.kurylik@mijick.com
// - GitHub: https://github.com/FulcrumOne
//
-// Copyright ©2023 Mijick. Licensed under MIT License.
+// Copyright ©2023 Mijick. All rights reserved.
import Foundation
diff --git a/Sources/Internal/MTimer.swift b/Sources/Internal/MTimer.swift
index ae43ad9..3b9efdc 100644
--- a/Sources/Internal/MTimer.swift
+++ b/Sources/Internal/MTimer.swift
@@ -1,166 +1,158 @@
//
-// MTimer.swift of Timer
+// MTimer.swift
+// MijickTimer
//
-// Created by Tomasz Kurylik
-// - Twitter: https://twitter.com/tkurylik
-// - Mail: tomasz.kurylik@mijick.com
-// - GitHub: https://github.com/FulcrumOne
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
//
-// Copyright ©2023 Mijick. Licensed under MIT License.
-
+// Copyright ©2024 Mijick. All rights reserved.
import SwiftUI
-public final class MTimer {
- static let shared: MTimer = .init()
-
- // Current State
- var internalTimer: Timer?
- var isTimerRunning: Bool = false
- var runningTime: TimeInterval = 0
- var backgroundTransitionDate: Date? = nil
-
- // Configuration
- var initialTime: (start: TimeInterval, end: TimeInterval) = (0, 1)
- var publisherTime: TimeInterval = 0
- var publisherTimeTolerance: TimeInterval = 0.4
- var onRunningTimeChange: ((MTime) -> ())!
- var onTimerActivityChange: ((Bool) -> ())?
- var onTimerProgressChange: ((Double) -> ())?
-
- deinit { internalTimer?.invalidate() }
+public final class MTimer: ObservableObject, FactoryInitializable {
+ /// Timer time publisher.
+ /// - important: The frequency for updating this property can be configured with function ``MTimer/publish(every:tolerance:currentTime:)``
+ /// - NOTE: By default, updates are triggered each time the timer status is marked as **finished**
+ @Published public private(set) var timerTime: MTime = .init()
+
+ /// Timer status publisher.
+ @Published public private(set) var timerStatus: MTimerStatus = .notStarted
+
+ /// Timer progress publisher.
+ /// - important: The frequency for updating this property can be configured with function ``MTimer/publish(every:tolerance:currentTime:)``
+ /// - NOTE: By default, updates are triggered each time the timer status is marked as **finished**
+ @Published public private(set) var timerProgress: Double = 0
+
+ /// Unique id that enables an access to the registered timer from any location.
+ public let id: MTimerID
+
+ let callbacks = MTimerCallbacks()
+ let state = MTimerStateManager()
+ let configuration = MTimerConfigurationManager()
+
+ init(identifier: MTimerID) { self.id = identifier }
}
-
// MARK: - Initialising Timer
extension MTimer {
- func checkRequirementsForInitialisingTimer(_ publisherTime: TimeInterval) throws {
- if publisherTime < 0.001 { throw Error.publisherTimeCannotBeLessThanOneMillisecond }
- }
- func assignInitialPublisherValues(_ time: TimeInterval, _ tolerance: TimeInterval, _ completion: @escaping (MTime) -> ()) {
- publisherTime = time
- publisherTimeTolerance = tolerance
- onRunningTimeChange = completion
+ func setupPublishers(_ time: TimeInterval, _ tolerance: TimeInterval, _ completion: @escaping (MTime) -> ()) {
+ configuration.setPublishers(time: time, tolerance: tolerance)
+ callbacks.onRunningTimeChange = completion
+ resetTimerPublishers()
}
}
// MARK: - Starting Timer
extension MTimer {
- func checkRequirementsForStartingTimer(_ startTime: TimeInterval, _ endTime: TimeInterval) throws {
- if startTime < 0 || endTime < 0 { throw Error.timeCannotBeLessThanZero }
- if startTime == endTime { throw Error.startTimeCannotBeTheSameAsEndTime }
-
- if isTimerRunning && backgroundTransitionDate == nil { throw Error.timerIsAlreadyRunning }
- }
func assignInitialStartValues(_ startTime: TimeInterval, _ endTime: TimeInterval) {
- initialTime = (startTime, endTime)
- runningTime = startTime
+ configuration.setInitialTime(startTime: startTime, endTime: endTime)
+ resetRunningTime()
+ resetTimerPublishers()
+ }
+ func startTimer() {
+ handleTimer(status: .running)
}
- func startTimer() { handleTimer(start: true) }
}
-// MARK: - Resuming Timer
+// MARK: - Timer State Control
extension MTimer {
- func checkRequirementsForResumingTimer() throws {
- if onRunningTimeChange == nil { throw Error.cannotResumeNotInitialisedTimer }
- }
+ func pauseTimer() { handleTimer(status: .paused) }
+ func cancelTimer() { handleTimer(status: .notStarted) }
+ func finishTimer() { handleTimer(status: .finished) }
}
-// MARK: - Stopping Timer
+// MARK: - Reset Timer
extension MTimer {
- func stopTimer() { handleTimer(start: false) }
+ func resetTimer() {
+ configuration.reset()
+ updateInternalTimer(false)
+ timerStatus = .notStarted
+ updateObservers(false)
+ resetTimerPublishers()
+ publishTimerStatus()
+ }
}
-// MARK: - Resetting Timer
+// MARK: - Running Time Updates
extension MTimer {
- func resetRunningTime() { runningTime = initialTime.start }
+ func resetRunningTime() { configuration.setCurrentTimeToStart() }
+ func skipRunningTime() { configuration.setCurrentTimeToEnd() }
}
-
// MARK: - Handling Timer
private extension MTimer {
- func handleTimer(start: Bool) { if !start || canTimerBeStarted {
- isTimerRunning = start
- updateInternalTimer(start)
- updateObservers(start)
+ func handleTimer(status: MTimerStatus) { if status != .running || configuration.canTimerBeStarted {
+ timerStatus = status
+ updateInternalTimer(isTimerRunning)
+ updateObservers(isTimerRunning)
publishTimerStatus()
}}
}
private extension MTimer {
- func updateInternalTimer(_ start: Bool) { DispatchQueue.main.async { [self] in switch start {
- case true: updateInternalTimerStart()
- case false: updateInternalTimerStop()
- }}}
- func updateObservers(_ start: Bool) { switch start {
- case true: addObservers()
- case false: removeObservers()
+ func updateInternalTimer(_ start: Bool) {
+ switch start {
+ case true: updateInternalTimerStart()
+ case false: updateInternalTimerStop()
}}
-}
-private extension MTimer {
- func updateInternalTimerStart() {
- internalTimer = .scheduledTimer(withTimeInterval: publisherTime, repeats: true, block: handleTimeChange)
- internalTimer?.tolerance = publisherTimeTolerance
- updateInternalTimerStartAddToRunLoop()
+ func updateObservers(_ start: Bool) {
+ switch start {
+ case true: addObservers()
+ case false: removeObservers()
+ }
}
- func updateInternalTimerStop() { internalTimer?.invalidate() }
}
private extension MTimer {
- /// **CONTEXT**: On macOS, when the mouse is down in a menu item or other tracking loop, the timer will not start.
- /// **DECISION**: Adding a timer the RunLoop seems to fix the issue issue.
- func updateInternalTimerStartAddToRunLoop() {
- #if os(macOS)
- if let internalTimer { RunLoop.main.add(internalTimer, forMode: .common) }
- #endif
- }
+ func updateInternalTimerStart() { state.runTimer(configuration, self, #selector(handleTimeChange)) }
+ func updateInternalTimerStop() { state.stopTimer() }
}
// MARK: - Handling Time Change
private extension MTimer {
- func handleTimeChange(_ timeChange: Any? = nil) {
- runningTime = calculateNewRunningTime(timeChange as? TimeInterval ?? publisherTime)
+ @objc func handleTimeChange(_ timeChange: Any) {
+ configuration.setNewCurrentTime(timeChange)
stopTimerIfNecessary()
publishRunningTimeChange()
}
}
private extension MTimer {
- func calculateNewRunningTime(_ timeChange: TimeInterval) -> TimeInterval {
- let newRunningTime = runningTime + timeChange * timeIncrementMultiplier
- return timeIncrementMultiplier == -1 ? max(newRunningTime, initialTime.end) : min(newRunningTime, initialTime.end)
- }
- func stopTimerIfNecessary() { if !canTimerBeStarted {
- stopTimer()
+ func stopTimerIfNecessary() { if !configuration.canTimerBeStarted {
+ finishTimer()
}}
}
// MARK: - Handling Background Mode
private extension MTimer {
func addObservers() {
- NotificationCenter.addAppStateNotifications(self, onDidEnterBackground: #selector(didEnterBackgroundNotification), onWillEnterForeground: #selector(willEnterForegroundNotification))
+ NotificationCenter
+ .addAppStateNotifications(self,
+ onDidEnterBackground: #selector(didEnterBackgroundNotification),
+ onWillEnterForeground: #selector(willEnterForegroundNotification))
}
func removeObservers() {
NotificationCenter.removeAppStateChangedNotifications(self)
}
}
private extension MTimer {
- @objc func didEnterBackgroundNotification() {
- internalTimer?.invalidate()
- backgroundTransitionDate = .init()
- }
@objc func willEnterForegroundNotification() {
handleReturnFromBackgroundWhenTimerIsRunning()
- backgroundTransitionDate = nil
+ state.willEnterForeground()
+ }
+ @objc func didEnterBackgroundNotification() {
+ state.didEnterBackground()
}
}
private extension MTimer {
- func handleReturnFromBackgroundWhenTimerIsRunning() { if let backgroundTransitionDate, isTimerRunning {
+ func handleReturnFromBackgroundWhenTimerIsRunning() {
+ guard let backgroundTransitionDate = state.backgroundTransitionDate, isTimerRunning else { return }
let timeChange = Date().timeIntervalSince(backgroundTransitionDate)
-
+
handleTimeChange(timeChange)
resumeTimerAfterReturningFromBackground()
- }}
+ }
}
private extension MTimer {
- func resumeTimerAfterReturningFromBackground() { if canTimerBeStarted {
+ func resumeTimerAfterReturningFromBackground() { if configuration.canTimerBeStarted {
updateInternalTimer(true)
}}
}
@@ -171,26 +163,30 @@ private extension MTimer {
publishTimerStatusChange()
publishRunningTimeChange()
}
+ func resetTimerPublishers() {
+ guard isNeededReset else { return }
+ timerStatus = .notStarted
+ timerProgress = 0
+ timerTime = .init(timeInterval: configuration.time.start)
+ }
}
+
private extension MTimer {
- func publishTimerStatusChange() { DispatchQueue.main.async { [self] in
- onTimerActivityChange?(isTimerRunning)
+ func publishTimerStatusChange() { DispatchQueue.main.async(qos: .userInteractive) { [weak self] in
+ guard let self else { return }
+ callbacks.onTimerStatusChange?(timerStatus)
}}
- func publishRunningTimeChange() { DispatchQueue.main.async { [self] in
- onRunningTimeChange?(.init(timeInterval: runningTime))
- onTimerProgressChange?(calculateTimerProgress())
+ func publishRunningTimeChange() { DispatchQueue.main.async(qos: .userInteractive) { [weak self] in
+ guard let self else { return }
+ callbacks.onRunningTimeChange?(.init(timeInterval: configuration.currentTime))
+ callbacks.onTimerProgressChange?(configuration.getTimerProgress())
+ timerTime = .init(timeInterval: configuration.currentTime)
+ timerProgress = configuration.getTimerProgress()
}}
}
-private extension MTimer {
- func calculateTimerProgress() -> Double {
- let timerTotalTime = max(initialTime.start, initialTime.end) - min(initialTime.start, initialTime.end)
- let timerRunningTime = abs(runningTime - initialTime.start)
- return timerRunningTime / timerTotalTime
- }
-}
-// MARK: - Others
+// MARK: - Helpers
private extension MTimer {
- var canTimerBeStarted: Bool { runningTime != initialTime.end }
- var timeIncrementMultiplier: Double { initialTime.start > initialTime.end ? -1 : 1 }
+ var isTimerRunning: Bool { timerStatus.isTimerRunning }
+ var isNeededReset: Bool { timerStatus.isNeededReset }
}
diff --git a/Sources/Internal/Protocols/FactoryInitializable.swift b/Sources/Internal/Protocols/FactoryInitializable.swift
new file mode 100644
index 0000000..78a1220
--- /dev/null
+++ b/Sources/Internal/Protocols/FactoryInitializable.swift
@@ -0,0 +1,22 @@
+//
+// FactoryInitializable.swift
+// MijickTimer
+//
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
+//
+// Copyright ©2024 Mijick. All rights reserved.
+
+import SwiftUI
+
+@MainActor public protocol FactoryInitializable { }
+
+extension FactoryInitializable where Self: MTimer {
+ /// Registers or returns registered Timer
+ public init(_ id: MTimerID) {
+ let timer = MTimer(identifier: id)
+ let registeredTimer = MTimerContainer.register(timer)
+ self = registeredTimer as! Self
+ }
+}
diff --git a/Sources/Public/Public+MTimer.Error.swift b/Sources/Public/Enumerations/Public+MTimerError.swift
similarity index 56%
rename from Sources/Public/Public+MTimer.Error.swift
rename to Sources/Public/Enumerations/Public+MTimerError.swift
index 751e6b2..72357a9 100644
--- a/Sources/Public/Public+MTimer.Error.swift
+++ b/Sources/Public/Enumerations/Public+MTimerError.swift
@@ -1,19 +1,19 @@
//
-// Public+MTimer.Error.swift of Timer
+// Public+MTimerError.swift
//
// Created by Tomasz Kurylik
// - Twitter: https://twitter.com/tkurylik
// - Mail: tomasz.kurylik@mijick.com
// - GitHub: https://github.com/FulcrumOne
//
-// Copyright ©2023 Mijick. Licensed under MIT License.
+// Copyright ©2023 Mijick. All rights reserved.
import Foundation
-extension MTimer { public enum Error: Swift.Error {
- case publisherTimeCannotBeLessThanOneMillisecond
+public enum MTimerError: Error {
+ case publisherTimeCannotBeLessThanZero
case startTimeCannotBeTheSameAsEndTime, timeCannotBeLessThanZero
case cannotResumeNotInitialisedTimer
- case timerIsAlreadyRunning
-}}
+ case timerIsAlreadyRunning, timerIsNotStarted
+}
diff --git a/Sources/Public/Enumerations/Public+MTimerStatus.swift b/Sources/Public/Enumerations/Public+MTimerStatus.swift
new file mode 100644
index 0000000..956012b
--- /dev/null
+++ b/Sources/Public/Enumerations/Public+MTimerStatus.swift
@@ -0,0 +1,42 @@
+//
+// Public+MTimerStatus.swift
+// MijickTimer
+//
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
+//
+// Copyright ©2024 Mijick. All rights reserved.
+
+public enum MTimerStatus {
+ /// Initial timer status
+ /// ## Triggered by methods
+ /// - ``MTimer/reset()``
+ case notStarted
+
+ /// Timer is in a progress
+ ///
+ /// ## Triggered by methods
+ /// - ``MTimer/start()``
+ /// - ``MTimer/start(from:to:)-1mvp1``
+ /// - ``MTimer/resume()``
+ case running
+
+ /// Timer is in a paused state
+ ///
+ /// ## Triggered by methods
+ /// - ``MTimer/pause()``
+ case paused
+
+ /// The timer was terminated by running out of time or calling the function
+ ///
+ /// ## Triggered by methods
+ /// - ``MTimer/skip()``
+ case finished
+}
+
+extension MTimerStatus {
+ var isTimerRunning: Bool { self == .running }
+ var isNeededReset: Bool { self == .notStarted || self == .finished }
+ var isSkippable: Bool { self == .running || self == .paused }
+}
diff --git a/Sources/Public/Models/Public+MTimerID.swift b/Sources/Public/Models/Public+MTimerID.swift
new file mode 100644
index 0000000..b48820e
--- /dev/null
+++ b/Sources/Public/Models/Public+MTimerID.swift
@@ -0,0 +1,16 @@
+//
+// Public+MTimerID.swift
+// MijickTimer
+//
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
+//
+// Copyright ©2024 Mijick. All rights reserved.
+
+/// Unique id that enables an access to the registered timer from any location.
+public struct MTimerID: Equatable, Sendable {
+ public let rawValue: String
+
+ public init(rawValue: String) { self.rawValue = rawValue }
+}
diff --git a/Sources/Public/Public+MTime.swift b/Sources/Public/Public+MTime.swift
index 17056ec..4f1d203 100644
--- a/Sources/Public/Public+MTime.swift
+++ b/Sources/Public/Public+MTime.swift
@@ -6,7 +6,7 @@
// - Mail: tomasz.kurylik@mijick.com
// - GitHub: https://github.com/FulcrumOne
//
-// Copyright ©2023 Mijick. Licensed under MIT License.
+// Copyright ©2023 Mijick. All rights reserved.
import Foundation
@@ -23,7 +23,7 @@ extension MTime {
self.init(timeInterval: timeInterval)
}
public init(timeInterval: TimeInterval) {
- let millisecondsInt = Int(timeInterval * 1000)
+ let millisecondsInt = timeInterval == .infinity ? Self.maxMilliseconds : Int(timeInterval * 1000)
let hoursDiv = 1000 * 60 * 60
let minutesDiv = 1000 * 60
@@ -38,9 +38,13 @@ extension MTime {
public static var zero: MTime { .init() }
public static var max: MTime { .init(hours: 60 * 60 * 24 * 365 * 100) }
}
+private extension MTime {
+ static var maxMilliseconds: Int { Int(max.toTimeInterval() * 1000) }
+}
// MARK: - Converting to TimeInterval
extension MTime {
+ /// Converts MTime values to TimeInterval
public func toTimeInterval() -> TimeInterval {
let hoursAsTimeInterval = 60 * 60 * TimeInterval(hours)
let minutesAsTimeInterval = 60 * TimeInterval(minutes)
@@ -53,7 +57,9 @@ extension MTime {
// MARK: - Converting To String
extension MTime {
- /// Converts the object to a string representation. Output can be customised by modifying the formatter block.
+ /// Converts the object to a string representation. Output can be customized by modifying the formatter block.
+ /// - Parameters:
+ /// - formatter: A formatter that creates string representations of quantities of time
public func toString(_ formatter: (DateComponentsFormatter) -> DateComponentsFormatter = { $0 }) -> String {
formatter(defaultTimeFormatter).string(from: toTimeInterval()) ?? ""
}
diff --git a/Sources/Public/Public+MTimer.swift b/Sources/Public/Public+MTimer.swift
index 8afea43..a7e661e 100644
--- a/Sources/Public/Public+MTimer.swift
+++ b/Sources/Public/Public+MTimer.swift
@@ -1,126 +1,169 @@
//
// Public+MTimer.swift of Timer
+// MijickTimer
//
-// Created by Tomasz Kurylik
-// - Twitter: https://twitter.com/tkurylik
-// - Mail: tomasz.kurylik@mijick.com
-// - GitHub: https://github.com/FulcrumOne
+// Created by Alina Petrovska
+// - Mail: alina.petrovska@mijick.com
+// - GitHub: https://github.com/Mijick
//
-// Copyright ©2023 Mijick. Licensed under MIT License.
+// Copyright ©2024 Mijick. All rights reserved.
import SwiftUI
-// MARK: - Creating New Instance Of Timer
-extension MTimer {
- /// Allows to create multiple instances of a timer.
- public static func createNewInstance() -> MTimer { .init() }
-}
-
// MARK: - Initialising Timer
-extension MTimer {
- /// Prepares the timer to start.
- /// WARNING: Use the start() method to start the timer.
- public static func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, _ completion: @escaping (_ currentTime: MTime) -> ()) throws -> MTimer {
- try shared.publish(every: time, tolerance: tolerance, completion)
- }
- /// Prepares the timer to start.
- /// WARNING: Use the start() method to start the timer.
- public static func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, currentTime: Binding) throws -> MTimer {
- try shared.publish(every: time, tolerance: tolerance) { currentTime.wrappedValue = $0 }
- }
- /// Prepares the timer to start.
- /// WARNING: Use the start() method to start the timer.
- public func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, currentTime: Binding) throws -> MTimer {
+public extension MTimer {
+ /// Configure the interval for publishing the timer status.
+ ///
+ /// - Parameters:
+ /// - time: timer status publishing interval
+ /// - tolerance: The amount of time after the scheduled fire date that the timer may fire.
+ /// - currentTime: A binding value that will be updated every **time** interval.
+ ///
+ /// - WARNING: Use the ``start()`` or ``start(from:to:)-1mvp1`` methods to start the timer.
+ func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, currentTime: Binding) throws -> MTimer {
try publish(every: time, tolerance: tolerance) { currentTime.wrappedValue = $0 }
}
- /// Prepares the timer to start.
- /// WARNING: Use the start() method to start the timer.
- public func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, _ completion: @escaping (_ currentTime: MTime) -> ()) throws -> MTimer {
- try checkRequirementsForInitialisingTimer(time)
- assignInitialPublisherValues(time, tolerance, completion)
+
+ /// Configure the interval for publishing the timer status.
+ ///
+ /// - Parameters:
+ /// - time: timer status publishing interval
+ /// - tolerance: The amount of time after the scheduled fire date that the timer may fire.
+ /// - completion: A completion block that will be executed every **time** interval
+ ///
+ /// - WARNING: Use the ``start()`` or ``start(from:to:)-1mvp1`` method to start the timer.
+ func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, _ completion: @escaping (_ currentTime: MTime) -> () = { _ in }) throws -> MTimer {
+ try MTimerValidator.checkRequirementsForInitializingTimer(time)
+ setupPublishers(time, tolerance, completion)
return self
}
}
// MARK: - Starting Timer
-extension MTimer {
- /// Starts the timer using the specified initial values. Can be run backwards - use any "to" value that is greater than "from".
- public func start(from startTime: MTime = .zero, to endTime: MTime = .max) throws {
+public extension MTimer {
+ /**
+ Starts the timer using the specified initial values.
+
+ - Note: The timer can be run backwards - use any value **to** that is greater than **from**.
+
+ ### Up-going timer
+ ```swift
+ MTimer(.exampleId)
+ .start(from: .zero, to: MTime(seconds: 10))
+ ```
+
+ ### Down-going timer
+ ```swift
+ MTimer(.exampleId)
+ .start(from: MTime(seconds: 10), to: .zero)
+ ```
+ */
+ func start(from startTime: MTime = .zero, to endTime: MTime = .max) throws {
try start(from: startTime.toTimeInterval(), to: endTime.toTimeInterval())
}
- /// Starts the timer using the specified initial values. Can be run backwards - use any "to" value that is greater than "from".
- public func start(from startTime: TimeInterval = 0, to endTime: TimeInterval = .infinity) throws {
- try checkRequirementsForStartingTimer(startTime, endTime)
+
+ /**
+ Starts the timer using the specified initial values.
+
+ - Note: The timer can be run backwards - use any value **to** that is greater than **from**.
+
+ ### Up-going timer
+ ```swift
+ MTimer(.exampleId)
+ .start(from: .zero, to: 10)
+ ```
+
+ ### Down-going timer
+ ```swift
+ MTimer(.exampleId)
+ .start(from: 10, to: .zero)
+ ```
+ */
+ func start(from startTime: TimeInterval = 0, to endTime: TimeInterval = .infinity) throws {
+ try MTimerValidator.checkRequirementsForStartingTimer(startTime, endTime, state, timerStatus)
assignInitialStartValues(startTime, endTime)
startTimer()
}
- /// Starts the timer.
- public func start() throws {
+
+ /// Starts the up-going infinity timer
+ func start() throws {
try start(from: .zero, to: .infinity)
}
}
// MARK: - Stopping Timer
-extension MTimer {
- /// Stops the timer.
- public static func stop() {
- shared.stop()
- }
- /// Stops the timer.
- public func stop() {
- stopTimer()
+public extension MTimer {
+ /// Pause the timer.
+ func pause() {
+ guard timerStatus == .running else { return }
+ pauseTimer()
}
}
// MARK: - Resuming Timer
-extension MTimer {
- /// Resumes the stopped timer.
- public static func resume() throws {
- try shared.resume()
- }
- /// Resumes the stopped timer.
- public func resume() throws {
- try checkRequirementsForResumingTimer()
+public extension MTimer {
+ /// Resumes the paused timer.
+ func resume() throws {
+ try MTimerValidator.checkRequirementsForResumingTimer(callbacks)
startTimer()
}
}
-// MARK: - Resetting Timer
-extension MTimer {
- /// Stops the timer and resets its current time to the initial value.
- public static func reset() {
- shared.reset()
- }
+// MARK: - Aborting Timer
+public extension MTimer {
/// Stops the timer and resets its current time to the initial value.
- public func reset() {
+ func cancel() {
resetRunningTime()
- stopTimer()
+ cancelTimer()
+ }
+}
+
+// MARK: - Aborting Timer
+public extension MTimer {
+ /// Stops the timer and resets all timer states to default values.
+ func reset() {
+ resetTimer()
+ }
+}
+
+// MARK: - Skip Timer
+public extension MTimer {
+ /// Stops the timer and updates its status to the final state.
+ func skip() throws {
+ guard timerStatus.isSkippable else { return }
+ try MTimerValidator.isCanBeSkipped(timerStatus)
+ skipRunningTime()
+ finishTimer()
}
}
// MARK: - Publishing Timer Activity Status
-extension MTimer {
- /// Publishes the timer activity changes.
- public func onTimerActivityChange(_ action: @escaping (_ isRunning: Bool) -> ()) -> MTimer {
- onTimerActivityChange = action
+public extension MTimer {
+ /// Publishes timer status changes.
+ /// - Note: To configure the interval at which the state of the timer will be published, use method ``publish(every:tolerance:currentTime:)``
+ func onTimerStatusChange(_ action: @escaping (_ timerStatus: MTimerStatus) -> ()) -> MTimer {
+ callbacks.onTimerStatusChange = action
return self
}
- /// Publishes the timer activity changes.
- public func bindTimerStatus(isTimerRunning: Binding) -> MTimer {
- onTimerActivityChange { isTimerRunning.wrappedValue = $0 }
+ /// Publishes timer status changes.
+ /// - Note: To configure the interval at which the state of the timer will be published, use method ``publish(every:tolerance:currentTime:)``
+ func bindTimerStatus(timerStatus: Binding) -> MTimer {
+ onTimerStatusChange { timerStatus.wrappedValue = $0 }
}
}
// MARK: - Publishing Timer Progress
-extension MTimer {
- /// Publishes the timer progress changes.
- public func onTimerProgressChange(_ action: @escaping (_ progress: Double) -> ()) -> MTimer {
- onTimerProgressChange = action
+public extension MTimer {
+ /// Publishes timer progress changes.
+ /// - Note: To configure the interval at which the timer's progress will be published, use method ``publish(every:tolerance:currentTime:)``
+ func onTimerProgressChange(_ action: @escaping (_ progress: Double) -> ()) -> MTimer {
+ callbacks.onTimerProgressChange = action
return self
}
- /// Publishes the timer progress changes.
- public func bindTimerProgress(progress: Binding) -> MTimer {
+ /// Publishes timer progress changes.
+ /// - Note: To configure the interval at which the timer's progress will be published, use method ``publish(every:tolerance:currentTime:)``
+ func bindTimerProgress(progress: Binding) -> MTimer {
onTimerProgressChange { progress.wrappedValue = $0 }
}
}
diff --git a/Tests/MTimeTests.swift b/Tests/MTimeTests.swift
index 1640fa9..cf1673d 100644
--- a/Tests/MTimeTests.swift
+++ b/Tests/MTimeTests.swift
@@ -17,7 +17,7 @@ final class MTimeTests: XCTestCase {}
// MARK: - Initialisation from TimeInterval
extension MTimeTests {
func testTimeInitialisesCorrectly_1second() {
- let time = MTime(1)
+ let time = MTime(timeInterval: 1)
XCTAssertEqual(time.hours, 0)
XCTAssertEqual(time.minutes, 0)
@@ -25,7 +25,7 @@ extension MTimeTests {
XCTAssertEqual(time.milliseconds, 0)
}
func testTimeInitialisesCorrectly_59seconds120milliseconds() {
- let time = MTime(59.12)
+ let time = MTime(timeInterval: 59.12)
XCTAssertEqual(time.hours, 0)
XCTAssertEqual(time.minutes, 0)
@@ -33,7 +33,7 @@ extension MTimeTests {
XCTAssertEqual(time.milliseconds, 120)
}
func testTimeInitialisesCorrectly_21minutes37seconds() {
- let time = MTime(1297)
+ let time = MTime(timeInterval: 1297)
XCTAssertEqual(time.hours, 0)
XCTAssertEqual(time.minutes, 21)
@@ -41,7 +41,7 @@ extension MTimeTests {
XCTAssertEqual(time.milliseconds, 0)
}
func testTimeInitialisesCorrectly_1hour39minutes17seconds140milliseconds() {
- let time = MTime(5957.14)
+ let time = MTime(timeInterval: 5957.14)
XCTAssertEqual(time.hours, 1)
XCTAssertEqual(time.minutes, 39)
diff --git a/Tests/MTimerTests.swift b/Tests/MTimerTests.swift
index 7910fef..00f5bef 100644
--- a/Tests/MTimerTests.swift
+++ b/Tests/MTimerTests.swift
@@ -12,10 +12,12 @@
import XCTest
@testable import MijickTimer
-final class MTimerTests: XCTestCase {
+@MainActor final class MTimerTests: XCTestCase {
var currentTime: TimeInterval = 0
- override func setUp() { MTimer.stop() }
+ override func setUp() async throws {
+ MTimerContainer.resetAll()
+ }
}
// MARK: - Basics
@@ -23,55 +25,71 @@ extension MTimerTests {
func testTimerStarts() {
try! defaultTimer.start()
wait(for: defaultWaitingTime)
-
+
XCTAssertGreaterThan(currentTime, 0)
+ XCTAssertEqual(.running, timer.timerStatus)
}
func testTimerIsCancellable() {
try! defaultTimer.start()
wait(for: defaultWaitingTime)
- MTimer.stop()
+ timer.cancel()
wait(for: defaultWaitingTime)
let timeAfterStop = currentTime
wait(for: defaultWaitingTime)
XCTAssertEqual(timeAfterStop, currentTime)
+ XCTAssertEqual(.notStarted, timer.timerStatus)
}
func testTimerIsResetable() {
let startTime: TimeInterval = 3
-
try! defaultTimer.start(from: startTime)
wait(for: defaultWaitingTime)
-
+
XCTAssertNotEqual(currentTime, startTime)
-
+
+ wait(for: defaultWaitingTime)
+ timer.reset()
+ wait(for: defaultWaitingTime)
+
+ XCTAssertEqual(0, currentTime)
+ XCTAssertEqual(0, timer.timerProgress)
+ XCTAssertEqual(.notStarted, timer.timerStatus)
+ }
+ func testTimerIsSkippable() {
+ let endTime: TimeInterval = 3
+
+ try! defaultTimer.start(to: endTime)
wait(for: defaultWaitingTime)
- MTimer.reset()
+ try! timer.skip()
wait(for: defaultWaitingTime)
- XCTAssertEqual(startTime, currentTime)
+ XCTAssertEqual(endTime, currentTime)
+ XCTAssertEqual(1, timer.timerProgress)
+ XCTAssertEqual(.finished, timer.timerStatus)
}
func testTimerCanBeResumed() {
try! defaultTimer.start()
wait(for: defaultWaitingTime)
- MTimer.stop()
+ timer.pause()
let timeAfterStop = currentTime
wait(for: defaultWaitingTime)
- try! MTimer.resume()
+ try! timer.resume()
wait(for: defaultWaitingTime)
XCTAssertNotEqual(timeAfterStop, currentTime)
+ XCTAssertEqual(.running, timer.timerStatus)
}
}
// MARK: - Additional Basics
extension MTimerTests {
func testTimerShouldPublishAccurateValuesWithZeroTolerance() {
- try! MTimer
- .publish(every: 0.1, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
+ try! timer
+ .publish(every: 0.1, tolerance: 0.0) { self.currentTime = $0.toTimeInterval() }
.start()
wait(for: 0.6)
@@ -80,8 +98,8 @@ extension MTimerTests {
func testTimerShouldPublishInaccurateValuesWithNonZeroTolerance() {
try! defaultTimer.start()
wait(for: 1)
-
- XCTAssertNotEqual(currentTime, 1)
+
+ XCTAssertEqual(currentTime, 1)
}
func testTimerCanRunBackwards() {
try! defaultTimer.start(from: 3, to: 1)
@@ -90,14 +108,14 @@ extension MTimerTests {
XCTAssertLessThan(currentTime, 3)
}
func testTimerPublishesStatuses() {
- var statuses: [Bool: Bool] = [true: false, false: false]
+ var statuses: [MTimerStatus: Bool] = [.running: false, .notStarted: false]
try! defaultTimer
- .onTimerActivityChange { statuses[$0] = true }
+ .onTimerStatusChange { statuses[$0] = true }
.start()
wait(for: defaultWaitingTime)
- MTimer.stop()
+ timer.cancel()
wait(for: defaultWaitingTime)
XCTAssertTrue(statuses.values.filter { !$0 }.isEmpty)
@@ -137,7 +155,7 @@ extension MTimerTests {
func testTimerCanHaveMultipleInstances() {
var newTime: TimeInterval = 0
- let newTimer = MTimer.createNewInstance()
+ let newTimer = MTimer(.multipleInstancesTimer)
try! newTimer
.publish(every: 0.3) { newTime = $0.toTimeInterval() }
.start(from: 10, to: 100)
@@ -150,20 +168,20 @@ extension MTimerTests {
XCTAssertNotEqual(newTime, currentTime)
}
func testNewInstanceTimerCanBeStopped() {
- let newTimer = MTimer.createNewInstance()
+ let newTimer = MTimer(.stoppableTimer)
try! newTimer
- .publish(every: 0.1) { self.currentTime = $0.toTimeInterval() }
+ .publish(every: 0.1) { print($0); self.currentTime = $0.toTimeInterval() }
.start()
wait(for: defaultWaitingTime)
- newTimer.stop()
+ newTimer.cancel()
wait(for: defaultWaitingTime)
let timeAfterStop = currentTime
wait(for: defaultWaitingTime)
- XCTAssertGreaterThan(currentTime, 0)
+ XCTAssertEqual(currentTime, 0)
XCTAssertEqual(timeAfterStop, currentTime)
}
}
@@ -173,7 +191,7 @@ extension MTimerTests {
func testTimerProgressCountsCorrectly_From0To10() {
var progress: Double = 0
- try! MTimer
+ try! timer
.publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
.onTimerProgressChange { progress = $0 }
.start(from: 0, to: 10)
@@ -184,7 +202,7 @@ extension MTimerTests {
func testTimerProgressCountsCorrectly_From10To29() {
var progress: Double = 0
- try! MTimer
+ try! timer
.publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
.onTimerProgressChange { progress = $0 }
.start(from: 10, to: 29)
@@ -195,7 +213,7 @@ extension MTimerTests {
func testTimerProgressCountsCorrectly_From31To100() {
var progress: Double = 0
- try! MTimer
+ try! timer
.publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
.onTimerProgressChange { progress = $0 }
.start(from: 31, to: 100)
@@ -206,7 +224,7 @@ extension MTimerTests {
func testTimerProgressCountsCorrectly_From100To0() {
var progress: Double = 0
- try! MTimer
+ try! timer
.publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
.onTimerProgressChange { progress = $0 }
.start(from: 100, to: 0)
@@ -217,45 +235,53 @@ extension MTimerTests {
func testTimerProgressCountsCorrectly_From31To14() {
var progress: Double = 0
- try! MTimer
+ try! timer
.publish(every: 0.25, tolerance: 0) { self.currentTime = $0.toTimeInterval() }
.onTimerProgressChange { progress = $0 }
.start(from: 31, to: 14)
wait(for: 1)
XCTAssertEqual(progress, 1/17)
+ XCTAssertEqual(timer.timerProgress, 1/17)
+ }
+ func timerShouldPublishStatusUpdateAtTheEndIfPublishersNotSetUpped() {
+ let timer = MTimer(.timerWithoutPublishers)
+ try! timer.start(to: 1)
+ wait(for: 1)
+
+ XCTAssertEqual(1.0, timer.timerTime.toTimeInterval())
}
}
// MARK: - Errors
extension MTimerTests {
func testTimerCannotBeInitialised_PublishTimeIsTooLess() {
- XCTAssertThrowsError(try MTimer.publish(every: 0.0001, { _ in })) { error in
- let error = error as! MTimer.Error
- XCTAssertEqual(error, .publisherTimeCannotBeLessThanOneMillisecond)
+ XCTAssertThrowsError(try timer.publish(every: -1, { _ in })) { error in
+ let error = error as! MTimerError
+ XCTAssertEqual(error, .publisherTimeCannotBeLessThanZero)
}
}
func testTimerDoesNotStart_StartTimeEqualsEndTime() {
XCTAssertThrowsError(try defaultTimer.start(from: 0, to: 0)) { error in
- let error = error as! MTimer.Error
+ let error = error as! MTimerError
XCTAssertEqual(error, .startTimeCannotBeTheSameAsEndTime)
}
}
func testTimerDoesNotStart_StartTimeIsLessThanZero() {
XCTAssertThrowsError(try defaultTimer.start(from: -10, to: 5)) { error in
- let error = error as! MTimer.Error
+ let error = error as! MTimerError
XCTAssertEqual(error, .timeCannotBeLessThanZero)
}
}
func testTimerDoesNotStart_EndTimeIsLessThanZero() {
XCTAssertThrowsError(try defaultTimer.start(from: 10, to: -15)) { error in
- let error = error as! MTimer.Error
+ let error = error as! MTimerError
XCTAssertEqual(error, .timeCannotBeLessThanZero)
}
}
func testCannotResumeTimer_WhenTimerIsNotInitialised() {
- XCTAssertThrowsError(try MTimer.resume()) { error in
- let error = error as! MTimer.Error
+ XCTAssertThrowsError(try timer.resume()) { error in
+ let error = error as! MTimerError
XCTAssertEqual(error, .cannotResumeNotInitialisedTimer)
}
}
@@ -263,7 +289,7 @@ extension MTimerTests {
try! defaultTimer.start()
XCTAssertThrowsError(try defaultTimer.start()) { error in
- let error = error as! MTimer.Error
+ let error = error as! MTimerError
XCTAssertEqual(error, .timerIsAlreadyRunning)
}
}
@@ -284,5 +310,12 @@ private extension MTimerTests {
}
private extension MTimerTests {
var defaultWaitingTime: TimeInterval { 0.15 }
- var defaultTimer: MTimer { try! .publish(every: 0.05, tolerance: 0.5) { self.currentTime = $0.toTimeInterval() } }
+ var defaultTimer: MTimer { try! timer.publish(every: 0.05, tolerance: 20) { self.currentTime = $0.toTimeInterval() } }
+ var timer: MTimer { .init(.testTimer) }
+}
+fileprivate extension MTimerID {
+ static let testTimer: MTimerID = .init(rawValue: "Test timer")
+ static let timerWithoutPublishers: MTimerID = .init(rawValue: "Timer Without Publishers")
+ static let stoppableTimer: MTimerID = .init(rawValue: "Stoppable Timer")
+ static let multipleInstancesTimer: MTimerID = .init(rawValue: "Multiple Instances")
}