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 @@ -
- +

- - - - Timer Logo - + + MijickTimer Hero +

-

- 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


+ +

- SwiftUI logo - Platforms: iOS, iPadOS, macOS, tvOS - Current Version - License: MIT + Labels

-

- Made in Kraków - - Follow us on X - - - Let's work together - - - Stargazers - -

+
+

Timer Examples


-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. + +

+ + + Join us on Discord + + + + Follow us on LinkedIn + + + + See our other frameworks + + + + Read us on Medium + + + + Buy us a coffee + +

-
+# ✨ 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.

+

+A demonstration of the code used to compare the implementation of the native iOS timer framework with the custom MijickTimer. -# 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.

+

+An illustration of how to create a countdown timer using only a few lines of code with the MijickTimer library. -| **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.

+

+

+Demonstrates code for controlling the Timer state via the MijickTimer library: stop, pause, resume, skip, and cancel or stop the Timer. -### ⏳ 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.

+

+The code illustrates various methods for monitoring the current timer state, including binding, callbacks, combining, and state observation -# 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") }