Skip to content

Commit

Permalink
Merge pull request #1 from Loupehope/develop
Browse files Browse the repository at this point in the history
release: 1.0.0
  • Loading branch information
Loupehope authored Aug 15, 2021
2 parents dd9fe68 + fd1729d commit fdf4542
Show file tree
Hide file tree
Showing 9 changed files with 418 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ playground.xcworkspace
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.swiftpm/

.build/

Expand Down
27 changes: 27 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "TITimer",
platforms: [
.iOS(.v13)
],
products: [
.library(
name: "TITimer",
targets: ["TITimer"]),
],
targets: [
.target(
name: "TITimer",
dependencies: []),
.testTarget(
name: "TITimerTests",
dependencies: ["TITimer"]),
],
swiftLanguageVersions: [
.v5
]
)
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,37 @@
# TITimer
Pretty timer ⏰
## Pretty timer ⏰

The library allows you to create a timer that works either with [RunLoop](#Runloop) or with [GCD](#GCD).

### Features

- Track the time even while the application is in the background. For more, see - [TimerRunMode](/Sources/TITimer/Enums/TimerRunMode.swift)
- Create a timer on a personal queue or on a special Runloop mode. For more, see - [TimerType](Sources/TITimer/Enums/TimerType.swift)
- The code is covered by tests 🙂

### Examples

#### RunLoop

```swift
timer = TITimer(type: .runloopTimer(runloop: .current, mode: .default), mode: .activeAndBackground)

timer.eventHandler = {
// handle elapsed time
}

timer.start()
timer.invalidate()
```
#### GCD

```swift
timer = TITimer(type: .dispatchSourceTimer(queue: .main), mode: .activeAndBackground)

timer.eventHandler = {
// handle elapsed time
}

timer.start()
timer.invalidate()
```
15 changes: 15 additions & 0 deletions Sources/TITimer/Enums/TimerRunMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// TimerRunMode.swift
//
//
// Created by Vlad Suhomlinov on 15.08.2021.
//

public enum TimerRunMode {

// Время таймера изменяется только при активной работе приложения
case onlyActive

// Время таймера изменяется и при активном и свернутом состоянии приложения
case activeAndBackground
}
20 changes: 20 additions & 0 deletions Sources/TITimer/Enums/TimerType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// TimerType.swift
//
//
// Created by Vlad Suhomlinov on 15.08.2021.
//

import UIKit

public enum TimerType {

// Запуск таймера на определенной GCD очереди
case dispatchSourceTimer(queue: DispatchQueue)

// Запуск таймера на определенном режиме Runloop
case runloopTimer(runloop: RunLoop = .main, mode: RunLoop.Mode)

// Собственная реализация таймера
case custom(ITimer)
}
29 changes: 29 additions & 0 deletions Sources/TITimer/Protocols/IInvalidatable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// IInvalidatable.swift
//
//
// Created by Vlad Suhomlinov on 15.08.2021.
//

import UIKit

public protocol IInvalidatable: AnyObject {

// Уничтожить объект
func invalidate()
}

// MARK: - IInvalidatable

extension Timer: IInvalidatable { }

extension DispatchSource: IInvalidatable {

public func invalidate() {
setEventHandler(handler: nil)

if !isCancelled {
cancel()
}
}
}
23 changes: 23 additions & 0 deletions Sources/TITimer/Protocols/ITimer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// ITimer.swift
//
//
// Created by Vlad Suhomlinov on 15.08.2021.
//

import Foundation

public protocol ITimer: IInvalidatable {

// Прошедшее время
var elapsedTime: TimeInterval { get }

// Запущен таймер или нет
var isRunning: Bool { get }

// Подписка на изменение прошедшего времени
var eventHandler: ((TimeInterval) -> Void)? { get set }

// Запустить работу таймера
func start(with interval: TimeInterval)
}
157 changes: 157 additions & 0 deletions Sources/TITimer/TITimer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import UIKit

public final class TITimer: ITimer {

private let mode: TimerRunMode
private let type: TimerType

private var sourceTimer: IInvalidatable?

private var enterBackgroundDate: Date?
private var interval: TimeInterval = 0

public private(set) var elapsedTime: TimeInterval = 0

public var isRunning: Bool {
sourceTimer != nil
}

public var eventHandler: ((TimeInterval) -> Void)?

// MARK: - Initialization

public init(type: TimerType, mode: TimerRunMode) {
self.mode = mode
self.type = type

if mode == .activeAndBackground {
addObserver()
}
}

deinit {
if mode == .activeAndBackground {
removeObserver()
}

invalidate()
}

// MARK: - Public

public func start(with interval: TimeInterval = 1) {
invalidate()

self.interval = interval

switch type {
case let .dispatchSourceTimer(queue):
sourceTimer = startDispatchSourceTimer(interval: interval, queue: queue)

case let .runloopTimer(runloop, mode):
sourceTimer = startTimer(interval: interval, runloop: runloop, mode: mode)

case let .custom(timer):
sourceTimer = timer

timer.start(with: interval)
}

eventHandler?(elapsedTime)
}

public func invalidate() {
elapsedTime = 0
sourceTimer?.invalidate()
sourceTimer = nil
}

// MARK: - Private

@objc private func handleSourceUpdate() {
guard enterBackgroundDate == nil else {
return
}

elapsedTime += interval

eventHandler?(elapsedTime)
}
}

// MARK: - Factory

extension TITimer {

public static var `default`: TITimer {
.init(type: .runloopTimer(runloop: .main, mode: .default), mode: .activeAndBackground)
}
}

// MARK: - NotificationCenter

private extension TITimer {

func addObserver() {
NotificationCenter.default.addObserver(self,
selector: #selector(willEnterForegroundNotification),
name: UIApplication.willEnterForegroundNotification,
object: nil)

NotificationCenter.default.addObserver(self,
selector: #selector(didEnterBackgroundNotification),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
}

func removeObserver() {
NotificationCenter.default.removeObserver(self)
NotificationCenter.default.removeObserver(self)
}

@objc func willEnterForegroundNotification() {
guard let unwrappedEnterBackgroundDate = enterBackgroundDate else {
return
}

let timeInBackground = -unwrappedEnterBackgroundDate.timeIntervalSinceNow.rounded()

enterBackgroundDate = nil
elapsedTime += timeInBackground
eventHandler?(elapsedTime)
}

@objc func didEnterBackgroundNotification() {
enterBackgroundDate = Date()
}
}

// MARK: - DispatchSourceTimer

private extension TITimer {

func startDispatchSourceTimer(interval: TimeInterval, queue: DispatchQueue) -> IInvalidatable? {
let timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
timer.schedule(deadline: .now() + interval, repeating: interval)
timer.setEventHandler(handler: handleSourceUpdate)

timer.resume()

return timer as? DispatchSource
}
}

// MARK: - Timer

private extension TITimer {

func startTimer(interval: TimeInterval, runloop: RunLoop, mode: RunLoop.Mode) -> IInvalidatable {
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
self?.handleSourceUpdate()
}

runloop.add(timer, forMode: mode)

return timer
}
}
Loading

0 comments on commit fdf4542

Please sign in to comment.