├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── 🚀-feature-request.md │ └── 🦟-bug-report.md ├── workflows │ └── release-cocoapods.yml ├── FUNDING.yml └── CODE_OF_CONDUCT.md ├── .gitignore ├── Sources ├── Internal │ ├── Helpers │ │ ├── MTimerCallbacks.swift │ │ ├── MTimerContainer.swift │ │ ├── MTimerValidator.swift │ │ ├── MTimerStateManager.swift │ │ └── MTimerConfigurationManager.swift │ ├── Protocols │ │ └── FactoryInitializable.swift │ ├── MTime.swift │ ├── Extensions │ │ └── NotificationCenter++.swift │ └── MTimer.swift └── Public │ ├── Models │ └── Public+MTimerID.swift │ ├── Enumerations │ ├── Public+MTimerError.swift │ └── Public+MTimerStatus.swift │ ├── Public+MTime.swift │ └── Public+MTimer.swift ├── Package.swift ├── MijickTimer.podspec ├── Tests ├── MTimeTests.swift └── MTimerTests.swift ├── README.md └── LICENSE /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @FulcrumOne 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Coming soon... 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓ Help and Support Discord Channel 4 | url: https://discord.com/invite/dT5V7nm5SC 5 | about: Please ask and answer questions here 6 | -------------------------------------------------------------------------------- /.github/workflows/release-cocoapods.yml: -------------------------------------------------------------------------------- 1 | name: Release CocoaPods 2 | on: 3 | push: 4 | branches: 5 | - master 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: macOS-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Publish to CocoaPod register 14 | env: 15 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 16 | run: | 17 | pod trunk push MijickTimer.podspec 18 | -------------------------------------------------------------------------------- /Sources/Internal/Helpers/MTimerCallbacks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTimerCallbacks.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | import SwiftUI 12 | 13 | class MTimerCallbacks { 14 | var onRunningTimeChange: ((MTime) -> ())? 15 | var onTimerStatusChange: ((MTimerStatus) -> ())? 16 | var onTimerProgressChange: ((Double) -> ())? 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Public/Models/Public+MTimerID.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Public+MTimerID.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | /// Unique id that enables an access to the registered timer from any location. 12 | public struct MTimerID: Equatable, Sendable { 13 | public let rawValue: String 14 | 15 | public init(rawValue: String) { self.rawValue = rawValue } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Public/Enumerations/Public+MTimerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Public+MTimerError.swift 3 | // 4 | // Created by Tomasz Kurylik 5 | // - Twitter: https://twitter.com/tkurylik 6 | // - Mail: tomasz.kurylik@mijick.com 7 | // - GitHub: https://github.com/FulcrumOne 8 | // 9 | // Copyright ©2023 Mijick. All rights reserved. 10 | 11 | 12 | import Foundation 13 | 14 | public enum MTimerError: Error { 15 | case publisherTimeCannotBeLessThanZero 16 | case startTimeCannotBeTheSameAsEndTime, timeCannotBeLessThanZero 17 | case cannotResumeNotInitialisedTimer 18 | case timerIsAlreadyRunning, timerIsNotStarted 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🚀-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: If you have a feature request 4 | title: "[FREQ]" 5 | labels: 'feature' 6 | projects: "Mijick/17" 7 | assignees: FulcrumOne 8 | 9 | --- 10 | 11 | ## Context 12 | 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? 13 | 14 | ## Alternatives 15 | Can you achieve the same result doing it in an alternative way? Is the alternative considerable? 16 | 17 | ## If the feature request is approved, would you be willing to submit a PR? 18 | Yes / No _(Help can be provided if you need assistance submitting a PR)_ 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MijickTimer", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .visionOS(.v1) 12 | ], 13 | products: [ 14 | .library(name: "MijickTimer", targets: ["MijickTimer"]), 15 | ], 16 | targets: [ 17 | .target(name: "MijickTimer", dependencies: [], path: "Sources"), 18 | .testTarget(name: "MijickTimerTests", dependencies: ["MijickTimer"], path: "Tests") 19 | ], 20 | swiftLanguageModes: [.v6] 21 | ) 22 | -------------------------------------------------------------------------------- /Sources/Internal/Protocols/FactoryInitializable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FactoryInitializable.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | import SwiftUI 12 | 13 | @MainActor public protocol FactoryInitializable { } 14 | 15 | extension FactoryInitializable where Self: MTimer { 16 | /// Registers or returns registered Timer 17 | public init(_ id: MTimerID) { 18 | let timer = MTimer(identifier: id) 19 | let registeredTimer = MTimerContainer.register(timer) 20 | self = registeredTimer as! Self 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mijick 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: mijick 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /Sources/Internal/Helpers/MTimerContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTimerContainer.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | @MainActor class MTimerContainer { 12 | private static var timers: [MTimer] = [] 13 | } 14 | 15 | extension MTimerContainer { 16 | static func register(_ timer: MTimer) -> MTimer { 17 | if let timer = getTimer(timer.id) { return timer } 18 | timers.append(timer) 19 | return timer 20 | } 21 | } 22 | private extension MTimerContainer { 23 | static func getTimer(_ id: MTimerID) -> MTimer? { 24 | timers.first(where: { $0.id == id }) 25 | } 26 | } 27 | 28 | extension MTimerContainer { 29 | static func resetAll() { timers.forEach { $0.reset() }} 30 | } 31 | -------------------------------------------------------------------------------- /MijickTimer.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'MijickTimer' 3 | s.summary = 'Modern API for Timer' 4 | s.description = 'Timers made simple: The Ultimate Swift Framework for Modern Apps on iOS, macOS, and visionOS.' 5 | 6 | s.version = '2.0.0' 7 | s.ios.deployment_target = '13.0' 8 | s.osx.deployment_target = '10.15' 9 | s.visionos.deployment_target = '1.0' 10 | s.swift_version = '6.0' 11 | 12 | s.source_files = 'Sources/**/*' 13 | s.frameworks = 'SwiftUI', 'Foundation', 'Combine' 14 | 15 | s.homepage = 'https://github.com/Mijick/Timer.git' 16 | s.license = { :type => 'MIT', :file => 'LICENSE' } 17 | s.author = { 'Alina P. from Mijick' => 'alina.petrovska@mijick.com' } 18 | s.source = { :git => 'https://github.com/Mijick/Timer.git', :tag => s.version.to_s } 19 | end 20 | -------------------------------------------------------------------------------- /Sources/Internal/MTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTime.swift 3 | // 4 | // Created by Tomasz Kurylik 5 | // - Twitter: https://twitter.com/tkurylik 6 | // - Mail: tomasz.kurylik@mijick.com 7 | // - GitHub: https://github.com/FulcrumOne 8 | // 9 | // Copyright ©2023 Mijick. All rights reserved. 10 | 11 | 12 | import Foundation 13 | 14 | public struct MTime: Equatable { 15 | public let hours: Int 16 | public let minutes: Int 17 | public let seconds: Int 18 | public let milliseconds: Int 19 | } 20 | 21 | // MARK: - Helpers 22 | extension MTime { 23 | var defaultTimeFormatter: DateComponentsFormatter { 24 | let formatter = DateComponentsFormatter() 25 | 26 | formatter.allowedUnits = [.hour, .minute, .second] 27 | formatter.unitsStyle = .positional 28 | formatter.zeroFormattingBehavior = .pad 29 | formatter.maximumUnitCount = 0 30 | formatter.allowsFractionalUnits = false 31 | formatter.collapsesLargestUnit = false 32 | 33 | return formatter 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Public/Enumerations/Public+MTimerStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Public+MTimerStatus.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | public enum MTimerStatus { 12 | /// Initial timer status 13 | /// ## Triggered by methods 14 | /// - ``MTimer/reset()`` 15 | case notStarted 16 | 17 | /// Timer is in a progress 18 | /// 19 | /// ## Triggered by methods 20 | /// - ``MTimer/start()`` 21 | /// - ``MTimer/start(from:to:)-1mvp1`` 22 | /// - ``MTimer/resume()`` 23 | case running 24 | 25 | /// Timer is in a paused state 26 | /// 27 | /// ## Triggered by methods 28 | /// - ``MTimer/pause()`` 29 | case paused 30 | 31 | /// The timer was terminated by running out of time or calling the function 32 | /// 33 | /// ## Triggered by methods 34 | /// - ``MTimer/skip()`` 35 | case finished 36 | } 37 | 38 | extension MTimerStatus { 39 | var isTimerRunning: Bool { self == .running } 40 | var isNeededReset: Bool { self == .notStarted || self == .finished } 41 | var isSkippable: Bool { self == .running || self == .paused } 42 | } 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🦟-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F99F Bug Report" 3 | about: If something isn't working 4 | title: "[BUG]" 5 | labels: 'bug' 6 | projects: "Mijick/17" 7 | assignees: FulcrumOne, jay-jay-lama 8 | 9 | --- 10 | 11 | ## Prerequisites 12 | - [ ] I checked the [documentation](https://github.com/Mijick/Timer/wiki) and found no answer 13 | - [ ] I checked to make sure that this issue has not already been filed 14 | 15 | ## Expected Behavior 16 | Please describe the behavior you are expecting 17 | 18 | ## Current Behavior 19 | What is the current behavior? 20 | 21 | ## Steps to Reproduce 22 | Please provide detailed steps for reproducing the issue. 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | ## Code Sample 29 | If you can, please include a code sample that we can use to debug the bug. 30 | 31 | ## Screenshots 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | ## Context 35 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions. 36 | 37 | | Name | Version | 38 | | ------| ---------| 39 | | SDK | e.g. 3.0.0 | 40 | | Xcode | e.g. 14.0 | 41 | | Operating System | e.g. iOS 18.0 | 42 | | Device | e.g. iPhone 14 Pro | 43 | -------------------------------------------------------------------------------- /Sources/Internal/Helpers/MTimerValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTimerValidator.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | import Foundation 12 | 13 | class MTimerValidator { 14 | static func checkRequirementsForInitializingTimer(_ publisherTime: TimeInterval) throws { 15 | if publisherTime < 0 { throw MTimerError.publisherTimeCannotBeLessThanZero } 16 | } 17 | static func checkRequirementsForStartingTimer(_ startTime: TimeInterval, _ endTime: TimeInterval, _ state: MTimerStateManager, _ status: MTimerStatus) throws { 18 | if startTime < 0 || endTime < 0 { throw MTimerError.timeCannotBeLessThanZero } 19 | if startTime == endTime { throw MTimerError.startTimeCannotBeTheSameAsEndTime } 20 | if status == .running && state.backgroundTransitionDate == nil { throw MTimerError.timerIsAlreadyRunning } 21 | } 22 | static func checkRequirementsForResumingTimer(_ callbacks: MTimerCallbacks) throws { 23 | if callbacks.onRunningTimeChange == nil { throw MTimerError.cannotResumeNotInitialisedTimer } 24 | } 25 | static func isCanBeSkipped(_ timerStatus: MTimerStatus) throws { 26 | if timerStatus == .running || timerStatus == .paused { return } 27 | throw MTimerError.timerIsNotStarted 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Internal/Extensions/NotificationCenter++.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCenter++.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | 12 | import SwiftUI 13 | 14 | extension NotificationCenter { 15 | static func addAppStateNotifications(_ observer: Any, onDidEnterBackground backgroundNotification: Selector, onWillEnterForeground foregroundNotification: Selector) { 16 | #if os(iOS) 17 | Self.default.addObserver(observer, selector: (backgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil) 18 | Self.default.addObserver(observer, selector: (foregroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil) 19 | 20 | #elseif os(macOS) 21 | Self.default.addObserver(observer, selector: (backgroundNotification), name: NSApplication.didResignActiveNotification, object: nil) 22 | Self.default.addObserver(observer, selector: (foregroundNotification), name: NSApplication.willBecomeActiveNotification, object: nil) 23 | 24 | #endif 25 | } 26 | static func removeAppStateChangedNotifications(_ observer: Any) { 27 | #if os(iOS) 28 | Self.default.removeObserver(observer, name: UIApplication.didEnterBackgroundNotification, object: nil) 29 | Self.default.removeObserver(observer, name: UIApplication.willEnterForegroundNotification, object: nil) 30 | 31 | #elseif os(macOS) 32 | Self.default.removeObserver(observer, name: NSApplication.didResignActiveNotification, object: nil) 33 | Self.default.removeObserver(observer, name: NSApplication.willBecomeActiveNotification, object: nil) 34 | 35 | #endif 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Internal/Helpers/MTimerStateManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTimerStateManager.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | import SwiftUI 12 | 13 | class MTimerStateManager { 14 | private var internalTimer: Timer? 15 | var backgroundTransitionDate: Date? = nil 16 | } 17 | 18 | // MARK: Run Timer 19 | extension MTimerStateManager { 20 | func runTimer(_ configuration: MTimerConfigurationManager, _ target: Any, _ completion: Selector) { 21 | stopTimer() 22 | runTimer(target, configuration.getPublisherTime(), completion) 23 | setTolerance(configuration.publisherTimeTolerance) 24 | updateInternalTimerStartAddToRunLoop() 25 | } 26 | } 27 | private extension MTimerStateManager { 28 | func runTimer(_ target: Any, _ timeInterval: TimeInterval, _ completion: Selector) { 29 | internalTimer = .scheduledTimer( 30 | timeInterval: timeInterval, 31 | target: target, 32 | selector: completion, 33 | userInfo: nil, 34 | repeats: true 35 | ) 36 | } 37 | func setTolerance(_ value: TimeInterval) { 38 | internalTimer?.tolerance = value 39 | } 40 | func updateInternalTimerStartAddToRunLoop() { 41 | #if os(macOS) 42 | guard let internalTimer = internalTimer else { return } 43 | RunLoop.main.add(internalTimer, forMode: .common) 44 | #endif 45 | } 46 | } 47 | 48 | // MARK: Stop Timer 49 | extension MTimerStateManager { 50 | func stopTimer() { 51 | internalTimer?.invalidate() 52 | } 53 | } 54 | 55 | // MARK: App State Handle 56 | extension MTimerStateManager { 57 | func didEnterBackground() { 58 | internalTimer?.invalidate() 59 | backgroundTransitionDate = .init() 60 | } 61 | func willEnterForeground() { 62 | backgroundTransitionDate = nil 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Internal/Helpers/MTimerConfigurationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTimerConfigurationManager.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | import SwiftUI 12 | 13 | class MTimerConfigurationManager { 14 | private(set) var time: (start: TimeInterval, end: TimeInterval) = (0, 1) 15 | private(set) var publisherTime: TimeInterval = 0 16 | private(set) var publisherTimeTolerance: TimeInterval = 0.4 17 | private(set) var currentTime: TimeInterval = 0 18 | } 19 | 20 | // MARK: Getters 21 | extension MTimerConfigurationManager { 22 | func getPublisherTime() -> TimeInterval { 23 | publisherTime == 0 ? max(time.start, time.end) : publisherTime 24 | } 25 | func getTimerProgress() -> Double { 26 | let timerTotalTime = max(time.start, time.end) - min(time.start, time.end) 27 | let timerRunningTime = abs(currentTime - time.start) 28 | return timerRunningTime / timerTotalTime 29 | } 30 | } 31 | 32 | // MARK: Setters 33 | extension MTimerConfigurationManager { 34 | func setInitialTime(startTime: TimeInterval, endTime: TimeInterval) { 35 | time = (startTime, endTime) 36 | currentTime = startTime 37 | } 38 | func setPublishers(time: TimeInterval, tolerance: TimeInterval) { 39 | publisherTime = time 40 | publisherTimeTolerance = tolerance 41 | } 42 | func setCurrentTimeToStart() { 43 | currentTime = time.start 44 | } 45 | func setCurrentTimeToEnd() { 46 | currentTime = time.end 47 | } 48 | func setNewCurrentTime(_ timeChange: Any?) { 49 | let timeChange = timeChange as? TimeInterval ?? getPublisherTime() 50 | let newCurrentTime = currentTime + timeChange * timeIncrementMultiplier 51 | currentTime = timeIncrementMultiplier == -1 52 | ? max(newCurrentTime, time.end) 53 | : min(newCurrentTime, time.end) 54 | } 55 | func reset() { 56 | time = (0, 1) 57 | publisherTime = 0 58 | publisherTimeTolerance = 0.4 59 | currentTime = 0 60 | } 61 | } 62 | private extension MTimerConfigurationManager { 63 | var timeIncrementMultiplier: Double { time.start > time.end ? -1 : 1 } 64 | } 65 | 66 | // MARK: Helpers 67 | extension MTimerConfigurationManager { 68 | var canTimerBeStarted: Bool { currentTime != time.end } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Public/Public+MTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Public+MTime.swift of Timer 3 | // 4 | // Created by Tomasz Kurylik 5 | // - Twitter: https://twitter.com/tkurylik 6 | // - Mail: tomasz.kurylik@mijick.com 7 | // - GitHub: https://github.com/FulcrumOne 8 | // 9 | // Copyright ©2023 Mijick. All rights reserved. 10 | 11 | 12 | import Foundation 13 | 14 | // MARK: - Initialisation 15 | extension MTime { 16 | public init(hours: Double = 0, minutes: Double = 0, seconds: Double = 0, milliseconds: Int = 0) { 17 | let hoursInterval = hours * 60 * 60 18 | let minutesInterval = minutes * 60 19 | let secondsInterval = seconds 20 | let millisecondsInterval = Double(milliseconds) / 1000 21 | 22 | let timeInterval = hoursInterval + minutesInterval + secondsInterval + millisecondsInterval 23 | self.init(timeInterval: timeInterval) 24 | } 25 | public init(timeInterval: TimeInterval) { 26 | let millisecondsInt = timeInterval == .infinity ? Self.maxMilliseconds : Int(timeInterval * 1000) 27 | 28 | let hoursDiv = 1000 * 60 * 60 29 | let minutesDiv = 1000 * 60 30 | let secondsDiv = 1000 31 | let millisecondsDiv = 1 32 | 33 | hours = millisecondsInt / hoursDiv 34 | minutes = (millisecondsInt % hoursDiv) / minutesDiv 35 | seconds = (millisecondsInt % hoursDiv % minutesDiv) / secondsDiv 36 | milliseconds = (millisecondsInt % hoursDiv % minutesDiv % secondsDiv) / millisecondsDiv 37 | } 38 | public static var zero: MTime { .init() } 39 | public static var max: MTime { .init(hours: 60 * 60 * 24 * 365 * 100) } 40 | } 41 | private extension MTime { 42 | static var maxMilliseconds: Int { Int(max.toTimeInterval() * 1000) } 43 | } 44 | 45 | // MARK: - Converting to TimeInterval 46 | extension MTime { 47 | /// Converts MTime values to TimeInterval 48 | public func toTimeInterval() -> TimeInterval { 49 | let hoursAsTimeInterval = 60 * 60 * TimeInterval(hours) 50 | let minutesAsTimeInterval = 60 * TimeInterval(minutes) 51 | let secondsAsTimeInterval = 1 * TimeInterval(seconds) 52 | let millisecondsAsTimeInterval = 0.001 * TimeInterval(milliseconds) 53 | 54 | return hoursAsTimeInterval + minutesAsTimeInterval + secondsAsTimeInterval + millisecondsAsTimeInterval 55 | } 56 | } 57 | 58 | // MARK: - Converting To String 59 | extension MTime { 60 | /// Converts the object to a string representation. Output can be customized by modifying the formatter block. 61 | /// - Parameters: 62 | /// - formatter: A formatter that creates string representations of quantities of time 63 | public func toString(_ formatter: (DateComponentsFormatter) -> DateComponentsFormatter = { $0 }) -> String { 64 | formatter(defaultTimeFormatter).string(from: toTimeInterval()) ?? "" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | team@mijick.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Sources/Public/Public+MTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Public+MTimer.swift of Timer 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | 12 | import SwiftUI 13 | 14 | // MARK: - Initialising Timer 15 | public extension MTimer { 16 | /// Configure the interval for publishing the timer status. 17 | /// 18 | /// - Parameters: 19 | /// - time: timer status publishing interval 20 | /// - tolerance: The amount of time after the scheduled fire date that the timer may fire. 21 | /// - currentTime: A binding value that will be updated every **time** interval. 22 | /// 23 | /// - WARNING: Use the ``start()`` or ``start(from:to:)-1mvp1`` methods to start the timer. 24 | func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, currentTime: Binding) throws -> MTimer { 25 | try publish(every: time, tolerance: tolerance) { currentTime.wrappedValue = $0 } 26 | } 27 | 28 | /// Configure the interval for publishing the timer status. 29 | /// 30 | /// - Parameters: 31 | /// - time: timer status publishing interval 32 | /// - tolerance: The amount of time after the scheduled fire date that the timer may fire. 33 | /// - completion: A completion block that will be executed every **time** interval 34 | /// 35 | /// - WARNING: Use the ``start()`` or ``start(from:to:)-1mvp1`` method to start the timer. 36 | func publish(every time: TimeInterval, tolerance: TimeInterval = 0.4, _ completion: @escaping (_ currentTime: MTime) -> () = { _ in }) throws -> MTimer { 37 | try MTimerValidator.checkRequirementsForInitializingTimer(time) 38 | setupPublishers(time, tolerance, completion) 39 | return self 40 | } 41 | } 42 | 43 | // MARK: - Starting Timer 44 | public extension MTimer { 45 | /** 46 | Starts the timer using the specified initial values. 47 | 48 | - Note: The timer can be run backwards - use any value **to** that is greater than **from**. 49 | 50 | ### Up-going timer 51 | ```swift 52 | MTimer(.exampleId) 53 | .start(from: .zero, to: MTime(seconds: 10)) 54 | ``` 55 | 56 | ### Down-going timer 57 | ```swift 58 | MTimer(.exampleId) 59 | .start(from: MTime(seconds: 10), to: .zero) 60 | ``` 61 | */ 62 | func start(from startTime: MTime = .zero, to endTime: MTime = .max) throws { 63 | try start(from: startTime.toTimeInterval(), to: endTime.toTimeInterval()) 64 | } 65 | 66 | /** 67 | Starts the timer using the specified initial values. 68 | 69 | - Note: The timer can be run backwards - use any value **to** that is greater than **from**. 70 | 71 | ### Up-going timer 72 | ```swift 73 | MTimer(.exampleId) 74 | .start(from: .zero, to: 10) 75 | ``` 76 | 77 | ### Down-going timer 78 | ```swift 79 | MTimer(.exampleId) 80 | .start(from: 10, to: .zero) 81 | ``` 82 | */ 83 | func start(from startTime: TimeInterval = 0, to endTime: TimeInterval = .infinity) throws { 84 | try MTimerValidator.checkRequirementsForStartingTimer(startTime, endTime, state, timerStatus) 85 | assignInitialStartValues(startTime, endTime) 86 | startTimer() 87 | } 88 | 89 | /// Starts the up-going infinity timer 90 | func start() throws { 91 | try start(from: .zero, to: .infinity) 92 | } 93 | } 94 | 95 | // MARK: - Stopping Timer 96 | public extension MTimer { 97 | /// Pause the timer. 98 | func pause() { 99 | guard timerStatus == .running else { return } 100 | pauseTimer() 101 | } 102 | } 103 | 104 | // MARK: - Resuming Timer 105 | public extension MTimer { 106 | /// Resumes the paused timer. 107 | func resume() throws { 108 | try MTimerValidator.checkRequirementsForResumingTimer(callbacks) 109 | startTimer() 110 | } 111 | } 112 | 113 | // MARK: - Aborting Timer 114 | public extension MTimer { 115 | /// Stops the timer and resets its current time to the initial value. 116 | func cancel() { 117 | resetRunningTime() 118 | cancelTimer() 119 | } 120 | } 121 | 122 | // MARK: - Aborting Timer 123 | public extension MTimer { 124 | /// Stops the timer and resets all timer states to default values. 125 | func reset() { 126 | resetTimer() 127 | } 128 | } 129 | 130 | // MARK: - Skip Timer 131 | public extension MTimer { 132 | /// Stops the timer and updates its status to the final state. 133 | func skip() throws { 134 | guard timerStatus.isSkippable else { return } 135 | try MTimerValidator.isCanBeSkipped(timerStatus) 136 | skipRunningTime() 137 | finishTimer() 138 | } 139 | } 140 | 141 | // MARK: - Publishing Timer Activity Status 142 | public extension MTimer { 143 | /// Publishes timer status changes. 144 | /// - Note: To configure the interval at which the state of the timer will be published, use method ``publish(every:tolerance:currentTime:)`` 145 | func onTimerStatusChange(_ action: @escaping (_ timerStatus: MTimerStatus) -> ()) -> MTimer { 146 | callbacks.onTimerStatusChange = action 147 | return self 148 | } 149 | /// Publishes timer status changes. 150 | /// - Note: To configure the interval at which the state of the timer will be published, use method ``publish(every:tolerance:currentTime:)`` 151 | func bindTimerStatus(timerStatus: Binding) -> MTimer { 152 | onTimerStatusChange { timerStatus.wrappedValue = $0 } 153 | } 154 | } 155 | 156 | // MARK: - Publishing Timer Progress 157 | public extension MTimer { 158 | /// Publishes timer progress changes. 159 | /// - Note: To configure the interval at which the timer's progress will be published, use method ``publish(every:tolerance:currentTime:)`` 160 | func onTimerProgressChange(_ action: @escaping (_ progress: Double) -> ()) -> MTimer { 161 | callbacks.onTimerProgressChange = action 162 | return self 163 | } 164 | /// Publishes timer progress changes. 165 | /// - Note: To configure the interval at which the timer's progress will be published, use method ``publish(every:tolerance:currentTime:)`` 166 | func bindTimerProgress(progress: Binding) -> MTimer { 167 | onTimerProgressChange { progress.wrappedValue = $0 } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Tests/MTimeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTimeTests.swift of Timer 3 | // 4 | // Created by Tomasz Kurylik 5 | // - Twitter: https://twitter.com/tkurylik 6 | // - Mail: tomasz.kurylik@mijick.com 7 | // - GitHub: https://github.com/FulcrumOne 8 | // 9 | // Copyright ©2023 Mijick. Licensed under MIT License. 10 | 11 | 12 | import XCTest 13 | @testable import MijickTimer 14 | 15 | final class MTimeTests: XCTestCase {} 16 | 17 | // MARK: - Initialisation from TimeInterval 18 | extension MTimeTests { 19 | func testTimeInitialisesCorrectly_1second() { 20 | let time = MTime(timeInterval: 1) 21 | 22 | XCTAssertEqual(time.hours, 0) 23 | XCTAssertEqual(time.minutes, 0) 24 | XCTAssertEqual(time.seconds, 1) 25 | XCTAssertEqual(time.milliseconds, 0) 26 | } 27 | func testTimeInitialisesCorrectly_59seconds120milliseconds() { 28 | let time = MTime(timeInterval: 59.12) 29 | 30 | XCTAssertEqual(time.hours, 0) 31 | XCTAssertEqual(time.minutes, 0) 32 | XCTAssertEqual(time.seconds, 59) 33 | XCTAssertEqual(time.milliseconds, 120) 34 | } 35 | func testTimeInitialisesCorrectly_21minutes37seconds() { 36 | let time = MTime(timeInterval: 1297) 37 | 38 | XCTAssertEqual(time.hours, 0) 39 | XCTAssertEqual(time.minutes, 21) 40 | XCTAssertEqual(time.seconds, 37) 41 | XCTAssertEqual(time.milliseconds, 0) 42 | } 43 | func testTimeInitialisesCorrectly_1hour39minutes17seconds140milliseconds() { 44 | let time = MTime(timeInterval: 5957.14) 45 | 46 | XCTAssertEqual(time.hours, 1) 47 | XCTAssertEqual(time.minutes, 39) 48 | XCTAssertEqual(time.seconds, 17) 49 | XCTAssertEqual(time.milliseconds, 140) 50 | } 51 | } 52 | 53 | // MARK: - Initialisation from Values 54 | extension MTimeTests { 55 | func testTimeInitialisesCorrectly_140milliseconds() { 56 | let time = MTime(milliseconds: 140) 57 | 58 | XCTAssertEqual(time.hours, 0) 59 | XCTAssertEqual(time.minutes, 0) 60 | XCTAssertEqual(time.seconds, 0) 61 | XCTAssertEqual(time.milliseconds, 140) 62 | } 63 | func testTimeInitialisesCorrectly_0point3seconds() { 64 | let time = MTime(seconds: 0.3) 65 | 66 | XCTAssertEqual(time.hours, 0) 67 | XCTAssertEqual(time.minutes, 0) 68 | XCTAssertEqual(time.seconds, 0) 69 | XCTAssertEqual(time.milliseconds, 300) 70 | } 71 | func testTimeInitialisesCorrectly_31seconds() { 72 | let time = MTime(seconds: 31.0) 73 | 74 | XCTAssertEqual(time.hours, 0) 75 | XCTAssertEqual(time.minutes, 0) 76 | XCTAssertEqual(time.seconds, 31) 77 | XCTAssertEqual(time.milliseconds, 0) 78 | } 79 | func testTimeInitialisesCorrectly_31point5seconds() { 80 | let time = MTime(seconds: 31.5) 81 | 82 | XCTAssertEqual(time.hours, 0) 83 | XCTAssertEqual(time.minutes, 0) 84 | XCTAssertEqual(time.seconds, 31) 85 | XCTAssertEqual(time.milliseconds, 500) 86 | } 87 | func testTimeInitialisesCorrectly_107seconds() { 88 | let time = MTime(seconds: 107.0) 89 | 90 | XCTAssertEqual(time.hours, 0) 91 | XCTAssertEqual(time.minutes, 1) 92 | XCTAssertEqual(time.seconds, 47) 93 | XCTAssertEqual(time.milliseconds, 0) 94 | } 95 | func testTimeInitialisesCorrectly_1point5minutes() { 96 | let time = MTime(minutes: 1.5) 97 | 98 | XCTAssertEqual(time.hours, 0) 99 | XCTAssertEqual(time.minutes, 1) 100 | XCTAssertEqual(time.seconds, 30) 101 | XCTAssertEqual(time.milliseconds, 0) 102 | } 103 | func testTimeInitialisesCorrectly_69minutes() { 104 | let time = MTime(minutes: 69.0) 105 | 106 | XCTAssertEqual(time.hours, 1) 107 | XCTAssertEqual(time.minutes, 9) 108 | XCTAssertEqual(time.seconds, 0) 109 | XCTAssertEqual(time.milliseconds, 0) 110 | } 111 | func testTimeInitialisesCorrectly_3hours72minutes21seconds14milliseconds() { 112 | let time = MTime(hours: 3.0, minutes: 72.0, seconds: 21.0, milliseconds: 14) 113 | 114 | XCTAssertEqual(time.hours, 4) 115 | XCTAssertEqual(time.minutes, 12) 116 | XCTAssertEqual(time.seconds, 21) 117 | XCTAssertEqual(time.milliseconds, 14) 118 | } 119 | } 120 | 121 | // MARK: - Converting to TimeInterval 122 | extension MTimeTests { 123 | func testTimeConvertsCorrectly_ToTimeInterval_13milliseconds() { 124 | let time = MTime(hours: 0, minutes: 0, seconds: 0, milliseconds: 13) 125 | 126 | XCTAssertEqual(time.toTimeInterval(), 0.013, accuracy: 0.001) 127 | } 128 | func testTimeConvertsCorrectly_ToTimeInterval_33seconds() { 129 | let time = MTime(hours: 0, minutes: 0, seconds: 33, milliseconds: 0) 130 | 131 | XCTAssertEqual(time.toTimeInterval(), 33, accuracy: 0.001) 132 | } 133 | func testTimeConvertsCorrectly_ToTimeInterval_1minute9seconds() { 134 | let time = MTime(hours: 0, minutes: 0, seconds: 69, milliseconds: 0) 135 | 136 | XCTAssertEqual(time.toTimeInterval(), 69, accuracy: 0.001) 137 | } 138 | func testTimeConvertsCorrectly_ToTimeInterval_1hour13minutes14seconds() { 139 | let time = MTime(hours: 1, minutes: 13, seconds: 14, milliseconds: 0) 140 | 141 | XCTAssertEqual(time.toTimeInterval(), 4394, accuracy: 0.001) 142 | } 143 | func testTimeConvertsCorrectly_ToTimeInterval_33hours58minutes32seconds141milliseconds() { 144 | let time = MTime(hours: 33, minutes: 58, seconds: 32, milliseconds: 141) 145 | 146 | XCTAssertEqual(time.toTimeInterval(), 122312.141, accuracy: 0.001) 147 | } 148 | } 149 | 150 | // MARK: - Converting to String 151 | extension MTimeTests { 152 | func testTimeConvertsCorrectly_ToString_3seconds() { 153 | let time = MTime(hours: 0, minutes: 0, seconds: 3, milliseconds: 0) 154 | 155 | XCTAssertEqual(time.toString(), "00:00:03") 156 | } 157 | func testTimeConvertsCorrectly_ToString_12minutes_33seconds() { 158 | let time = MTime(hours: 0, minutes: 12, seconds: 33, milliseconds: 0) 159 | 160 | XCTAssertEqual(time.toString(), "00:12:33") 161 | } 162 | func testTimeConvertsCorrectly_ToString_1hour_3minutes_17seconds() { 163 | let time = MTime(hours: 1, minutes: 3, seconds: 17, milliseconds: 0) 164 | 165 | XCTAssertEqual(time.toString(), "01:03:17") 166 | } 167 | func testTimeConvertsCorrectly_ToString_31hours_1minute_21seconds() { 168 | let time = MTime(hours: 31, minutes: 1, seconds: 21, milliseconds: 0) 169 | 170 | XCTAssertEqual(time.toString(), "31:01:21") 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/Internal/MTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTimer.swift 3 | // MijickTimer 4 | // 5 | // Created by Alina Petrovska 6 | // - Mail: alina.petrovska@mijick.com 7 | // - GitHub: https://github.com/Mijick 8 | // 9 | // Copyright ©2024 Mijick. All rights reserved. 10 | 11 | import SwiftUI 12 | 13 | public final class MTimer: ObservableObject, FactoryInitializable { 14 | /// Timer time publisher. 15 | /// - important: The frequency for updating this property can be configured with function ``MTimer/publish(every:tolerance:currentTime:)`` 16 | /// - NOTE: By default, updates are triggered each time the timer status is marked as **finished** 17 | @Published public private(set) var timerTime: MTime = .init() 18 | 19 | /// Timer status publisher. 20 | @Published public private(set) var timerStatus: MTimerStatus = .notStarted 21 | 22 | /// Timer progress publisher. 23 | /// - important: The frequency for updating this property can be configured with function ``MTimer/publish(every:tolerance:currentTime:)`` 24 | /// - NOTE: By default, updates are triggered each time the timer status is marked as **finished** 25 | @Published public private(set) var timerProgress: Double = 0 26 | 27 | /// Unique id that enables an access to the registered timer from any location. 28 | public let id: MTimerID 29 | 30 | let callbacks = MTimerCallbacks() 31 | let state = MTimerStateManager() 32 | let configuration = MTimerConfigurationManager() 33 | 34 | init(identifier: MTimerID) { self.id = identifier } 35 | } 36 | 37 | // MARK: - Initialising Timer 38 | extension MTimer { 39 | func setupPublishers(_ time: TimeInterval, _ tolerance: TimeInterval, _ completion: @escaping (MTime) -> ()) { 40 | configuration.setPublishers(time: time, tolerance: tolerance) 41 | callbacks.onRunningTimeChange = completion 42 | resetTimerPublishers() 43 | } 44 | } 45 | 46 | // MARK: - Starting Timer 47 | extension MTimer { 48 | func assignInitialStartValues(_ startTime: TimeInterval, _ endTime: TimeInterval) { 49 | configuration.setInitialTime(startTime: startTime, endTime: endTime) 50 | resetRunningTime() 51 | resetTimerPublishers() 52 | } 53 | func startTimer() { 54 | handleTimer(status: .running) 55 | } 56 | } 57 | 58 | // MARK: - Timer State Control 59 | extension MTimer { 60 | func pauseTimer() { handleTimer(status: .paused) } 61 | func cancelTimer() { handleTimer(status: .notStarted) } 62 | func finishTimer() { handleTimer(status: .finished) } 63 | } 64 | 65 | // MARK: - Reset Timer 66 | extension MTimer { 67 | func resetTimer() { 68 | configuration.reset() 69 | updateInternalTimer(false) 70 | timerStatus = .notStarted 71 | updateObservers(false) 72 | resetTimerPublishers() 73 | publishTimerStatus() 74 | } 75 | } 76 | 77 | // MARK: - Running Time Updates 78 | extension MTimer { 79 | func resetRunningTime() { configuration.setCurrentTimeToStart() } 80 | func skipRunningTime() { configuration.setCurrentTimeToEnd() } 81 | } 82 | 83 | // MARK: - Handling Timer 84 | private extension MTimer { 85 | func handleTimer(status: MTimerStatus) { if status != .running || configuration.canTimerBeStarted { 86 | timerStatus = status 87 | updateInternalTimer(isTimerRunning) 88 | updateObservers(isTimerRunning) 89 | publishTimerStatus() 90 | }} 91 | } 92 | private extension MTimer { 93 | func updateInternalTimer(_ start: Bool) { 94 | switch start { 95 | case true: updateInternalTimerStart() 96 | case false: updateInternalTimerStop() 97 | }} 98 | func updateObservers(_ start: Bool) { 99 | switch start { 100 | case true: addObservers() 101 | case false: removeObservers() 102 | } 103 | } 104 | } 105 | private extension MTimer { 106 | func updateInternalTimerStart() { state.runTimer(configuration, self, #selector(handleTimeChange)) } 107 | func updateInternalTimerStop() { state.stopTimer() } 108 | } 109 | 110 | // MARK: - Handling Time Change 111 | private extension MTimer { 112 | @objc func handleTimeChange(_ timeChange: Any) { 113 | configuration.setNewCurrentTime(timeChange) 114 | stopTimerIfNecessary() 115 | publishRunningTimeChange() 116 | } 117 | } 118 | private extension MTimer { 119 | func stopTimerIfNecessary() { if !configuration.canTimerBeStarted { 120 | finishTimer() 121 | }} 122 | } 123 | 124 | // MARK: - Handling Background Mode 125 | private extension MTimer { 126 | func addObservers() { 127 | NotificationCenter 128 | .addAppStateNotifications(self, 129 | onDidEnterBackground: #selector(didEnterBackgroundNotification), 130 | onWillEnterForeground: #selector(willEnterForegroundNotification)) 131 | } 132 | func removeObservers() { 133 | NotificationCenter.removeAppStateChangedNotifications(self) 134 | } 135 | } 136 | private extension MTimer { 137 | @objc func willEnterForegroundNotification() { 138 | handleReturnFromBackgroundWhenTimerIsRunning() 139 | state.willEnterForeground() 140 | } 141 | @objc func didEnterBackgroundNotification() { 142 | state.didEnterBackground() 143 | } 144 | } 145 | private extension MTimer { 146 | func handleReturnFromBackgroundWhenTimerIsRunning() { 147 | guard let backgroundTransitionDate = state.backgroundTransitionDate, isTimerRunning else { return } 148 | let timeChange = Date().timeIntervalSince(backgroundTransitionDate) 149 | 150 | handleTimeChange(timeChange) 151 | resumeTimerAfterReturningFromBackground() 152 | } 153 | } 154 | private extension MTimer { 155 | func resumeTimerAfterReturningFromBackground() { if configuration.canTimerBeStarted { 156 | updateInternalTimer(true) 157 | }} 158 | } 159 | 160 | // MARK: - Publishers 161 | private extension MTimer { 162 | func publishTimerStatus() { 163 | publishTimerStatusChange() 164 | publishRunningTimeChange() 165 | } 166 | func resetTimerPublishers() { 167 | guard isNeededReset else { return } 168 | timerStatus = .notStarted 169 | timerProgress = 0 170 | timerTime = .init(timeInterval: configuration.time.start) 171 | } 172 | } 173 | 174 | private extension MTimer { 175 | func publishTimerStatusChange() { DispatchQueue.main.async(qos: .userInteractive) { [weak self] in 176 | guard let self else { return } 177 | callbacks.onTimerStatusChange?(timerStatus) 178 | }} 179 | func publishRunningTimeChange() { DispatchQueue.main.async(qos: .userInteractive) { [weak self] in 180 | guard let self else { return } 181 | callbacks.onRunningTimeChange?(.init(timeInterval: configuration.currentTime)) 182 | callbacks.onTimerProgressChange?(configuration.getTimerProgress()) 183 | timerTime = .init(timeInterval: configuration.currentTime) 184 | timerProgress = configuration.getTimerProgress() 185 | }} 186 | } 187 | 188 | // MARK: - Helpers 189 | private extension MTimer { 190 | var isTimerRunning: Bool { timerStatus.isTimerRunning } 191 | var isNeededReset: Bool { timerStatus.isNeededReset } 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | MijickTimer Hero 5 | 6 |

7 | 8 | 9 | 10 |

11 |

Timers made simple

12 |

Easy to use yet powerful Timer library. Keep your code clean.

13 |

14 | 15 | 16 | 17 |

18 | Try demo we prepared 19 | | 20 | Framework documentation 21 | | 22 | Roadmap 23 |

24 | 25 |
26 | 27 | 28 | 29 |

30 | Labels 31 |

32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 52 | 53 | 54 |
Multiple TimersState Control
47 | Timer Examples 48 | 50 | Timer Examples 51 |
55 | 56 |
57 |
58 | 59 | 60 | 61 |

62 | 63 | 64 | Visit our Website 65 | 66 | 67 | 68 | Join us on Discord 69 | 70 | 71 | 72 | Follow us on LinkedIn 73 | 74 | 75 | 76 | See our other frameworks 77 | 78 | 79 | 80 | Read us on Medium 81 | 82 | 83 | 84 | Buy us a coffee 85 | 86 |

87 | 88 | 89 | 90 | # ✨ Features 91 | 92 | 93 | 94 | 97 | 100 | 101 | 102 | 105 | 108 | 109 | 110 | 113 | 116 | 117 | 118 | 121 | 124 | 125 | 126 | 129 | 132 | 133 | 134 | 137 | 140 | 141 | 142 | 145 | 148 | 149 | 150 |
95 | ⏳ 96 | 98 | Countdown Timer (Down-Going) 99 |
103 | ⏱️ 104 | 106 | Count-Up Timer (Elapsed Time) 107 |
111 | ⏸️ 112 | 114 | Pause Timer 115 |
119 | ▶️ 120 | 122 | Resume Timer 123 |
127 | ⏭️ 128 | 130 | Skip Timer 131 |
135 | ⏮️ 136 | 138 | Cancel Timer 139 |
143 | ⚡ 144 | 146 | Reactive programming friendly 147 |
151 | 152 | 153 | 154 | # ☀️ What is MijickTimer? 155 | 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. 156 | 157 |

158 |

Count-Up Timer

159 |

Track elapsed time seamlessly with a count-up timer. Ideal for productivity, logging or workout apps.

160 |

Take a look at the implementation details here.

161 |

162 | A demonstration of the code used to compare the implementation of the native iOS timer framework with the custom MijickTimer. 163 | 164 |

165 |

Countdown Timer

166 |

Easily create countdown timers to track remaining time. Perfect for games, events or task apps.

167 |

Take a look at the implementation details here.

168 |

169 | An illustration of how to create a countdown timer using only a few lines of code with the MijickTimer library. 170 | 171 |

172 |

Control Timer state

173 |

Pause timers and resume them later without losing progress. It also allows to skip and cancel the progress.

174 |

Take a look at the implementation details here.

175 |

176 |

177 | Demonstrates code for controlling the Timer state via the MijickTimer library: stop, pause, resume, skip, and cancel or stop the Timer. 178 | 179 |

Observe Timer State

180 |

181 |

Monitor timer state with a variety of different approaches: binding, callbacks, combine, state value updates.

182 |

Take a look at the implementation details here.

183 |

184 | The code illustrates various methods for monitoring the current timer state, including binding, callbacks, combining, and state observation 185 | 186 | 187 | 188 | # ✅ Why MijickTimer? 189 |

Multiple Apple Platform Support:

190 | 191 | * iPhone, iPad. Requires iOS 13.0+ . 192 | * Mac. Requires macOS 10.15+. 193 | * Apple Vision Pro. Requires visionOS 1.0+. 194 | 195 |

Built for Swift 6:

196 | 197 | * Modern, efficient, and designed for performance. 198 | 199 |

All-in-One Timer Solution:

200 | 201 | * Handles countdowns, count-ups, pausing, resuming and state management seamlessly. 202 | 203 |

Versatile Observation:

204 | 205 | * Choose callbacks, bindings or Combine for the implementation that works best for you. 206 | * Provides the ability to access the state of a specific timer from any part of the code base. 207 | 208 |

It's just a cool library 😎

209 | 210 | 211 | 212 | # 🚀 How to use it? 213 | Visit the framework's [documentation](https://link.mijick.com/timer-wiki) to learn how to integrate your project with **MijickTimer**.
214 | See for yourself how does it work by cloning [project](https://link.mijick.com/timer-demo) we created 215 | 216 | 217 | 218 | # 🍀 Community 219 | Join the welcoming community of developers on [Discord](https://link.mijick.com/discord). 220 | 221 | 222 | 223 | # 🌼 Contribute 224 | 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).
225 | 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).
226 | If you would like to contribute, please refer to the [Contribution Guidelines](https://link.mijick.com/contribution-guidelines). 227 | 228 | 229 | 230 | # 💜 Sponsor our work 231 | Support our work by [becoming a backer](https://link.mijick.com/buymeacoffee). 232 | 233 | -------------------------------------------------------------------------------- /Tests/MTimerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTimerTests.swift of Timer 3 | // 4 | // Created by Tomasz Kurylik 5 | // - Twitter: https://twitter.com/tkurylik 6 | // - Mail: tomasz.kurylik@mijick.com 7 | // - GitHub: https://github.com/FulcrumOne 8 | // 9 | // Copyright ©2023 Mijick. Licensed under MIT License. 10 | 11 | 12 | import XCTest 13 | @testable import MijickTimer 14 | 15 | @MainActor final class MTimerTests: XCTestCase { 16 | var currentTime: TimeInterval = 0 17 | 18 | override func setUp() async throws { 19 | MTimerContainer.resetAll() 20 | } 21 | } 22 | 23 | // MARK: - Basics 24 | extension MTimerTests { 25 | func testTimerStarts() { 26 | try! defaultTimer.start() 27 | wait(for: defaultWaitingTime) 28 | 29 | XCTAssertGreaterThan(currentTime, 0) 30 | XCTAssertEqual(.running, timer.timerStatus) 31 | } 32 | func testTimerIsCancellable() { 33 | try! defaultTimer.start() 34 | wait(for: defaultWaitingTime) 35 | 36 | timer.cancel() 37 | wait(for: defaultWaitingTime) 38 | 39 | let timeAfterStop = currentTime 40 | wait(for: defaultWaitingTime) 41 | 42 | XCTAssertEqual(timeAfterStop, currentTime) 43 | XCTAssertEqual(.notStarted, timer.timerStatus) 44 | } 45 | func testTimerIsResetable() { 46 | let startTime: TimeInterval = 3 47 | try! defaultTimer.start(from: startTime) 48 | wait(for: defaultWaitingTime) 49 | 50 | XCTAssertNotEqual(currentTime, startTime) 51 | 52 | wait(for: defaultWaitingTime) 53 | timer.reset() 54 | wait(for: defaultWaitingTime) 55 | 56 | XCTAssertEqual(0, currentTime) 57 | XCTAssertEqual(0, timer.timerProgress) 58 | XCTAssertEqual(.notStarted, timer.timerStatus) 59 | } 60 | func testTimerIsSkippable() { 61 | let endTime: TimeInterval = 3 62 | 63 | try! defaultTimer.start(to: endTime) 64 | wait(for: defaultWaitingTime) 65 | try! timer.skip() 66 | wait(for: defaultWaitingTime) 67 | 68 | XCTAssertEqual(endTime, currentTime) 69 | XCTAssertEqual(1, timer.timerProgress) 70 | XCTAssertEqual(.finished, timer.timerStatus) 71 | } 72 | func testTimerCanBeResumed() { 73 | try! defaultTimer.start() 74 | wait(for: defaultWaitingTime) 75 | 76 | timer.pause() 77 | let timeAfterStop = currentTime 78 | wait(for: defaultWaitingTime) 79 | 80 | try! timer.resume() 81 | wait(for: defaultWaitingTime) 82 | 83 | XCTAssertNotEqual(timeAfterStop, currentTime) 84 | XCTAssertEqual(.running, timer.timerStatus) 85 | } 86 | } 87 | 88 | // MARK: - Additional Basics 89 | extension MTimerTests { 90 | func testTimerShouldPublishAccurateValuesWithZeroTolerance() { 91 | try! timer 92 | .publish(every: 0.1, tolerance: 0.0) { self.currentTime = $0.toTimeInterval() } 93 | .start() 94 | wait(for: 0.6) 95 | 96 | XCTAssertEqual(currentTime, 0.6) 97 | } 98 | func testTimerShouldPublishInaccurateValuesWithNonZeroTolerance() { 99 | try! defaultTimer.start() 100 | wait(for: 1) 101 | 102 | XCTAssertEqual(currentTime, 1) 103 | } 104 | func testTimerCanRunBackwards() { 105 | try! defaultTimer.start(from: 3, to: 1) 106 | wait(for: defaultWaitingTime) 107 | 108 | XCTAssertLessThan(currentTime, 3) 109 | } 110 | func testTimerPublishesStatuses() { 111 | var statuses: [MTimerStatus: Bool] = [.running: false, .notStarted: false] 112 | 113 | try! defaultTimer 114 | .onTimerStatusChange { statuses[$0] = true } 115 | .start() 116 | wait(for: defaultWaitingTime) 117 | 118 | timer.cancel() 119 | wait(for: defaultWaitingTime) 120 | 121 | XCTAssertTrue(statuses.values.filter { !$0 }.isEmpty) 122 | } 123 | func testTimerIncreasesTimeCorrectly_WhenGoesForward() { 124 | try! defaultTimer.start(from: 0, to: 10) 125 | wait(for: 0.8) 126 | 127 | XCTAssertGreaterThan(currentTime, 0) 128 | XCTAssertLessThan(currentTime, 10) 129 | } 130 | func testTimerIncreasesTimeCorrectly_WhenGoesBackward() { 131 | try! defaultTimer.start(from: 10, to: 0) 132 | wait(for: 0.8) 133 | 134 | XCTAssertGreaterThan(currentTime, 0) 135 | XCTAssertLessThan(currentTime, 10) 136 | } 137 | func testTimerStopsAutomatically_WhenGoesForward() { 138 | try! defaultTimer.start(from: 0, to: 0.25) 139 | wait(for: 0.8) 140 | 141 | XCTAssertEqual(currentTime, 0.25) 142 | } 143 | func testTimerStopsAutomatically_WhenGoesBackward() { 144 | try! defaultTimer.start(from: 3, to: 2.75) 145 | wait(for: 0.8) 146 | 147 | XCTAssertEqual(currentTime, 2.75) 148 | } 149 | func testTimerStopsAutomatically_WhenGoesBackward_DoesNotExceedZero() { 150 | try! defaultTimer.start(from: 0.25, to: 0) 151 | wait(for: 1.2) 152 | 153 | XCTAssertEqual(currentTime, 0) 154 | } 155 | func testTimerCanHaveMultipleInstances() { 156 | var newTime: TimeInterval = 0 157 | 158 | let newTimer = MTimer(.multipleInstancesTimer) 159 | try! newTimer 160 | .publish(every: 0.3) { newTime = $0.toTimeInterval() } 161 | .start(from: 10, to: 100) 162 | try! defaultTimer.start(from: 0, to: 100) 163 | 164 | wait(for: 1) 165 | 166 | XCTAssertGreaterThan(newTime, 10) 167 | XCTAssertGreaterThan(currentTime, 0) 168 | XCTAssertNotEqual(newTime, currentTime) 169 | } 170 | func testNewInstanceTimerCanBeStopped() { 171 | let newTimer = MTimer(.stoppableTimer) 172 | 173 | try! newTimer 174 | .publish(every: 0.1) { print($0); self.currentTime = $0.toTimeInterval() } 175 | .start() 176 | wait(for: defaultWaitingTime) 177 | 178 | newTimer.cancel() 179 | wait(for: defaultWaitingTime) 180 | 181 | let timeAfterStop = currentTime 182 | wait(for: defaultWaitingTime) 183 | 184 | XCTAssertEqual(currentTime, 0) 185 | XCTAssertEqual(timeAfterStop, currentTime) 186 | } 187 | } 188 | 189 | // MARK: - Progress 190 | extension MTimerTests { 191 | func testTimerProgressCountsCorrectly_From0To10() { 192 | var progress: Double = 0 193 | 194 | try! timer 195 | .publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() } 196 | .onTimerProgressChange { progress = $0 } 197 | .start(from: 0, to: 10) 198 | wait(for: 1) 199 | 200 | XCTAssertEqual(progress, 0.1) 201 | } 202 | func testTimerProgressCountsCorrectly_From10To29() { 203 | var progress: Double = 0 204 | 205 | try! timer 206 | .publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() } 207 | .onTimerProgressChange { progress = $0 } 208 | .start(from: 10, to: 29) 209 | wait(for: 1) 210 | 211 | XCTAssertEqual(progress, 1/19) 212 | } 213 | func testTimerProgressCountsCorrectly_From31To100() { 214 | var progress: Double = 0 215 | 216 | try! timer 217 | .publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() } 218 | .onTimerProgressChange { progress = $0 } 219 | .start(from: 31, to: 100) 220 | wait(for: 1) 221 | 222 | XCTAssertEqual(progress, 1/69) 223 | } 224 | func testTimerProgressCountsCorrectly_From100To0() { 225 | var progress: Double = 0 226 | 227 | try! timer 228 | .publish(every: 0.5, tolerance: 0) { self.currentTime = $0.toTimeInterval() } 229 | .onTimerProgressChange { progress = $0 } 230 | .start(from: 100, to: 0) 231 | wait(for: 1.5) 232 | 233 | XCTAssertEqual(progress, 1.5/100) 234 | } 235 | func testTimerProgressCountsCorrectly_From31To14() { 236 | var progress: Double = 0 237 | 238 | try! timer 239 | .publish(every: 0.25, tolerance: 0) { self.currentTime = $0.toTimeInterval() } 240 | .onTimerProgressChange { progress = $0 } 241 | .start(from: 31, to: 14) 242 | wait(for: 1) 243 | 244 | XCTAssertEqual(progress, 1/17) 245 | XCTAssertEqual(timer.timerProgress, 1/17) 246 | } 247 | func timerShouldPublishStatusUpdateAtTheEndIfPublishersNotSetUpped() { 248 | let timer = MTimer(.timerWithoutPublishers) 249 | try! timer.start(to: 1) 250 | wait(for: 1) 251 | 252 | XCTAssertEqual(1.0, timer.timerTime.toTimeInterval()) 253 | } 254 | } 255 | 256 | // MARK: - Errors 257 | extension MTimerTests { 258 | func testTimerCannotBeInitialised_PublishTimeIsTooLess() { 259 | XCTAssertThrowsError(try timer.publish(every: -1, { _ in })) { error in 260 | let error = error as! MTimerError 261 | XCTAssertEqual(error, .publisherTimeCannotBeLessThanZero) 262 | } 263 | } 264 | func testTimerDoesNotStart_StartTimeEqualsEndTime() { 265 | XCTAssertThrowsError(try defaultTimer.start(from: 0, to: 0)) { error in 266 | let error = error as! MTimerError 267 | XCTAssertEqual(error, .startTimeCannotBeTheSameAsEndTime) 268 | } 269 | } 270 | func testTimerDoesNotStart_StartTimeIsLessThanZero() { 271 | XCTAssertThrowsError(try defaultTimer.start(from: -10, to: 5)) { error in 272 | let error = error as! MTimerError 273 | XCTAssertEqual(error, .timeCannotBeLessThanZero) 274 | } 275 | } 276 | func testTimerDoesNotStart_EndTimeIsLessThanZero() { 277 | XCTAssertThrowsError(try defaultTimer.start(from: 10, to: -15)) { error in 278 | let error = error as! MTimerError 279 | XCTAssertEqual(error, .timeCannotBeLessThanZero) 280 | } 281 | } 282 | func testCannotResumeTimer_WhenTimerIsNotInitialised() { 283 | XCTAssertThrowsError(try timer.resume()) { error in 284 | let error = error as! MTimerError 285 | XCTAssertEqual(error, .cannotResumeNotInitialisedTimer) 286 | } 287 | } 288 | func testCannotStartTimer_WhenTimerIsRunning() { 289 | try! defaultTimer.start() 290 | 291 | XCTAssertThrowsError(try defaultTimer.start()) { error in 292 | let error = error as! MTimerError 293 | XCTAssertEqual(error, .timerIsAlreadyRunning) 294 | } 295 | } 296 | } 297 | 298 | 299 | // MARK: - Helpers 300 | private extension MTimerTests { 301 | func wait(for duration: TimeInterval) { 302 | let waitExpectation = expectation(description: "Waiting") 303 | 304 | DispatchQueue.main.asyncAfter(deadline: .now() + duration) { 305 | waitExpectation.fulfill() 306 | } 307 | 308 | waitForExpectations(timeout: duration + 0.5) 309 | } 310 | } 311 | private extension MTimerTests { 312 | var defaultWaitingTime: TimeInterval { 0.15 } 313 | var defaultTimer: MTimer { try! timer.publish(every: 0.05, tolerance: 20) { self.currentTime = $0.toTimeInterval() } } 314 | var timer: MTimer { .init(.testTimer) } 315 | } 316 | fileprivate extension MTimerID { 317 | static let testTimer: MTimerID = .init(rawValue: "Test timer") 318 | static let timerWithoutPublishers: MTimerID = .init(rawValue: "Timer Without Publishers") 319 | static let stoppableTimer: MTimerID = .init(rawValue: "Stoppable Timer") 320 | static let multipleInstancesTimer: MTimerID = .init(rawValue: "Multiple Instances") 321 | } 322 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright ©2023 Mijick 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------