2020-11-01 18:54:19 +00:00
|
|
|
//
|
|
|
|
// TimerDataStore.swift
|
|
|
|
// IntervalTimer
|
|
|
|
//
|
|
|
|
// Created by William Brawner on 10/23/20.
|
|
|
|
// Copyright © 2020 William Brawner. All rights reserved.
|
|
|
|
//
|
|
|
|
|
2020-11-02 01:44:21 +00:00
|
|
|
import AudioToolbox
|
2020-11-01 18:54:19 +00:00
|
|
|
import Combine
|
|
|
|
import CoreData
|
|
|
|
import Foundation
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
class TimerDataStore: ObservableObject {
|
|
|
|
private var persistentContainer: NSPersistentContainer
|
|
|
|
private let internalTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
|
|
|
private var timerCancellable: AnyCancellable? = nil
|
2020-11-02 01:44:21 +00:00
|
|
|
private var sounds: [Phase:SystemSoundID] = [:]
|
2020-11-01 18:54:19 +00:00
|
|
|
|
|
|
|
@Published var activeTimer: ActiveTimerState? = nil {
|
|
|
|
didSet {
|
|
|
|
self.hasActiveTimer = self.activeTimer != nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@Published var hasActiveTimer: Bool = false
|
|
|
|
@Published var timers: Result<[IntervalTimer], TimerError> = .failure(.loading)
|
|
|
|
|
|
|
|
func openTimer(_ timer: IntervalTimer) {
|
|
|
|
self.activeTimer = ActiveTimerState(
|
|
|
|
timer: timer,
|
|
|
|
timeRemaining: timer.warmUpDuration,
|
|
|
|
currentSet: timer.sets,
|
|
|
|
currentRound: timer.rounds,
|
|
|
|
soundId: nil,
|
|
|
|
phase: Phase.warmUp,
|
|
|
|
isRunning: false
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func closeTimer() {
|
|
|
|
self.activeTimer = nil
|
2020-11-03 15:00:17 +00:00
|
|
|
self.timerCancellable?.cancel()
|
|
|
|
self.timerCancellable = nil
|
|
|
|
UIApplication.shared.isIdleTimerDisabled = false
|
2020-11-01 18:54:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func goBack() {
|
|
|
|
guard let state = self.activeTimer else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch state.phase {
|
|
|
|
case .warmUp:
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.warmUpDuration
|
|
|
|
)
|
|
|
|
case .low:
|
|
|
|
if state.currentSet == state.timer.sets && state.currentRound == state.timer.rounds {
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.warmUpDuration,
|
|
|
|
phase: .warmUp
|
|
|
|
)
|
|
|
|
} else if state.currentSet == state.timer.sets && state.currentRound < state.timer.rounds {
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.restDuration,
|
2020-11-04 14:50:27 +00:00
|
|
|
currentSet: 1,
|
2020-11-01 18:54:19 +00:00
|
|
|
phase: .rest
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.highIntensityDuration,
|
|
|
|
currentSet: state.currentSet + 1,
|
|
|
|
phase: .high
|
|
|
|
)
|
|
|
|
}
|
|
|
|
case .high:
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.lowIntensityDuration,
|
|
|
|
phase: .low
|
|
|
|
)
|
|
|
|
case .rest:
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.highIntensityDuration,
|
2020-11-04 14:50:27 +00:00
|
|
|
currentRound: state.currentRound + 1,
|
2020-11-01 18:54:19 +00:00
|
|
|
phase: .high
|
|
|
|
)
|
|
|
|
case .cooldown:
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.highIntensityDuration,
|
|
|
|
phase: .high
|
|
|
|
)
|
|
|
|
}
|
2020-11-02 01:44:21 +00:00
|
|
|
if let newState = self.activeTimer {
|
|
|
|
if newState.isRunning {
|
|
|
|
AudioServicesPlaySystemSound(sounds[newState.phase]!)
|
|
|
|
}
|
|
|
|
}
|
2020-11-01 18:54:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func toggle() {
|
|
|
|
guard let state = self.activeTimer else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if self.timerCancellable != nil {
|
|
|
|
self.timerCancellable?.cancel()
|
|
|
|
self.timerCancellable = nil
|
|
|
|
} else {
|
|
|
|
self.timerCancellable = self.internalTimer.sink(receiveValue: { _ in
|
|
|
|
self.updateTimer()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
self.activeTimer = state.copy(isRunning: self.timerCancellable != nil)
|
|
|
|
UIApplication.shared.isIdleTimerDisabled = self.activeTimer?.isRunning ?? false
|
|
|
|
}
|
|
|
|
|
2020-11-02 17:14:49 +00:00
|
|
|
private func updateTimer() {
|
2020-11-01 18:54:19 +00:00
|
|
|
guard let state = self.activeTimer else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let newState = state.copy(timeRemaining: state.timeRemaining - 1)
|
|
|
|
if newState.timeRemaining == 0 {
|
|
|
|
goForward()
|
|
|
|
} else {
|
|
|
|
self.activeTimer = newState
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func goForward() {
|
|
|
|
guard let state = self.activeTimer else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
switch state.phase {
|
|
|
|
case .warmUp:
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.lowIntensityDuration,
|
|
|
|
phase: .low
|
|
|
|
)
|
|
|
|
case .low:
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.highIntensityDuration,
|
|
|
|
phase: .high
|
|
|
|
)
|
|
|
|
case .high:
|
|
|
|
if state.currentSet > 1 {
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.lowIntensityDuration,
|
|
|
|
currentSet: state.currentSet - 1,
|
|
|
|
phase: .low
|
|
|
|
)
|
|
|
|
} else if state.currentRound > 1 {
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.restDuration,
|
|
|
|
currentRound: state.currentRound - 1,
|
|
|
|
phase: .rest
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.cooldownDuration,
|
|
|
|
phase: .cooldown
|
|
|
|
)
|
|
|
|
}
|
|
|
|
case .rest:
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: state.timer.lowIntensityDuration,
|
|
|
|
currentSet: state.timer.sets,
|
|
|
|
phase: .low
|
|
|
|
)
|
|
|
|
case .cooldown:
|
|
|
|
self.activeTimer = state.copy(
|
|
|
|
timeRemaining: 0,
|
|
|
|
isRunning: false
|
|
|
|
)
|
|
|
|
self.timerCancellable?.cancel()
|
|
|
|
self.timerCancellable = nil
|
|
|
|
UIApplication.shared.isIdleTimerDisabled = false
|
|
|
|
}
|
2020-11-02 01:44:21 +00:00
|
|
|
if let newState = self.activeTimer {
|
|
|
|
if newState.isRunning {
|
|
|
|
AudioServicesPlaySystemSound(sounds[newState.phase]!)
|
|
|
|
}
|
|
|
|
}
|
2020-11-01 18:54:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func loadTimers() {
|
2020-11-06 01:51:51 +00:00
|
|
|
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "IntervalTimer")
|
|
|
|
do {
|
|
|
|
let fetchedTimers = try self.persistentContainer.viewContext.fetch(fetchRequest) as! [IntervalTimerMO]
|
|
|
|
self.timers = .success(fetchedTimers.map { IntervalTimer.create(fromMO: $0) })
|
|
|
|
} catch {
|
|
|
|
self.timers = .failure(.failed(error))
|
2020-11-01 18:54:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func saveTimer(
|
|
|
|
id: UUID? = nil,
|
|
|
|
name: String,
|
|
|
|
description: String? = nil,
|
|
|
|
warmUpDuration: Int64,
|
|
|
|
lowIntensityDuration: Int64,
|
|
|
|
highIntensityDuration: Int64,
|
|
|
|
restDuration: Int64,
|
|
|
|
cooldownDuration: Int64,
|
|
|
|
sets: Int64,
|
|
|
|
rounds: Int64
|
|
|
|
) {
|
2020-11-06 01:51:51 +00:00
|
|
|
var timer: IntervalTimerMO
|
2020-11-04 17:21:40 +00:00
|
|
|
if let uuid = id {
|
2020-11-06 01:51:51 +00:00
|
|
|
timer = loadTimer(byId: uuid)!
|
2020-11-04 17:21:40 +00:00
|
|
|
} else {
|
2020-11-06 01:51:51 +00:00
|
|
|
timer = IntervalTimerMO.init(entity: NSEntityDescription.entity(forEntityName: "IntervalTimer", in: persistentContainer.viewContext)!, insertInto: persistentContainer.viewContext) as IntervalTimerMO
|
2020-11-04 17:21:40 +00:00
|
|
|
}
|
|
|
|
|
2020-11-01 18:54:19 +00:00
|
|
|
timer.id = id ?? UUID()
|
|
|
|
timer.name = name
|
|
|
|
timer.userDescription = description
|
|
|
|
timer.warmUpDuration = warmUpDuration
|
|
|
|
timer.lowIntensityDuration = lowIntensityDuration
|
|
|
|
timer.highIntensityDuration = highIntensityDuration
|
|
|
|
timer.restDuration = restDuration
|
|
|
|
timer.cooldownDuration = cooldownDuration
|
|
|
|
timer.sets = sets
|
|
|
|
timer.rounds = rounds
|
|
|
|
let viewContext = persistentContainer.viewContext
|
2020-11-04 17:21:40 +00:00
|
|
|
if id == nil {
|
|
|
|
viewContext.insert(timer)
|
|
|
|
}
|
2020-11-01 18:54:19 +00:00
|
|
|
try! viewContext.save()
|
|
|
|
loadTimers()
|
2020-11-04 17:21:40 +00:00
|
|
|
if let currentState = activeTimer {
|
|
|
|
if currentState.timer.id == timer.id {
|
2020-11-06 01:51:51 +00:00
|
|
|
activeTimer = currentState.copy(timer: IntervalTimer.create(fromMO: timer))
|
2020-11-04 17:21:40 +00:00
|
|
|
}
|
|
|
|
}
|
2020-11-01 18:54:19 +00:00
|
|
|
}
|
|
|
|
|
2020-11-02 17:14:49 +00:00
|
|
|
func deleteTimer(at: IndexSet) {
|
2020-11-06 01:51:51 +00:00
|
|
|
guard let timer = loadTimer(byId: try! self.timers.get()[at.first!].id!) else {
|
|
|
|
return
|
|
|
|
}
|
2020-11-01 18:54:19 +00:00
|
|
|
let viewContext = persistentContainer.viewContext
|
|
|
|
viewContext.delete(timer)
|
|
|
|
try! viewContext.save()
|
|
|
|
loadTimers()
|
2020-11-06 01:51:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func loadTimer(byId: UUID) -> IntervalTimerMO? {
|
|
|
|
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "IntervalTimer")
|
|
|
|
fetchRequest.predicate = NSPredicate(format: "id = %@", byId.uuidString)
|
|
|
|
let result = try! persistentContainer.viewContext.fetch(fetchRequest) as! [IntervalTimerMO]
|
|
|
|
if result.count == 1 {
|
|
|
|
return result[0]
|
|
|
|
}
|
|
|
|
return nil
|
2020-11-01 18:54:19 +00:00
|
|
|
}
|
2020-11-02 01:44:21 +00:00
|
|
|
|
|
|
|
private func loadSound(_ phase: Phase) {
|
|
|
|
let filePath = Bundle.main.path(forResource: phase.rawValue, ofType: "mp3")
|
|
|
|
let url = NSURL(fileURLWithPath: filePath!)
|
|
|
|
var soundId: SystemSoundID = 0
|
|
|
|
AudioServicesCreateSystemSoundID(url, &soundId)
|
|
|
|
sounds[phase] = soundId
|
|
|
|
}
|
2020-11-01 18:54:19 +00:00
|
|
|
|
|
|
|
init(_ completionClosure: @escaping () -> ()) {
|
|
|
|
persistentContainer = NSPersistentContainer(name: "IntervalTimer")
|
|
|
|
persistentContainer.loadPersistentStores() { (description, error) in
|
|
|
|
if let error = error {
|
|
|
|
fatalError("Failed to load Core Data stack: \(error)")
|
|
|
|
}
|
2020-11-04 17:21:40 +00:00
|
|
|
self.persistentContainer.viewContext.mergePolicy = NSOverwriteMergePolicy
|
2020-11-01 18:54:19 +00:00
|
|
|
self.loadTimers()
|
|
|
|
completionClosure()
|
|
|
|
}
|
2020-11-02 01:44:21 +00:00
|
|
|
loadSound(.warmUp)
|
|
|
|
loadSound(.low)
|
|
|
|
loadSound(.high)
|
|
|
|
loadSound(.rest)
|
|
|
|
loadSound(.cooldown)
|
2020-11-01 18:54:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum TimerError: Error {
|
|
|
|
case loading
|
|
|
|
case failed(_ error: Error)
|
|
|
|
}
|