Split IntervalTimer model into view and coredata models

Trying to use the same object for the view and Core Data was a bit
clumsy and made working with SwiftUI previews difficult. Splitting
them up fixed the issue at the expense of a little more maintenance
work.
This commit is contained in:
William Brawner 2020-11-05 18:51:51 -07:00
parent cedd5403c5
commit ead9b2ebe0
7 changed files with 88 additions and 31 deletions

View file

@ -127,7 +127,25 @@ struct LabeledCounter: View {
} }
struct ActiveTimerView_Previews: PreviewProvider { struct ActiveTimerView_Previews: PreviewProvider {
static var dataStore: TimerDataStore {
get {
let store = TimerDataStore() {}
store.openTimer(IntervalTimer(
id: UUID(),
name: "Test",
description: nil,
warmUpDuration: 300,
lowIntensityDuration: 10,
highIntensityDuration: 20,
restDuration: 50,
cooldownDuration: 300,
sets: 4,
rounds: 2
))
return store
}
}
static var previews: some View { static var previews: some View {
ActiveTimerView().environmentObject(TimerDataStore() {}) ActiveTimerView().environmentObject(dataStore)
} }
} }

View file

@ -11,7 +11,20 @@ import CoreData
import Foundation import Foundation
import SwiftUI import SwiftUI
class IntervalTimer: NSManagedObject, Identifiable { struct IntervalTimer: Identifiable, Equatable {
let id: UUID?
let name: String
let description: String?
let warmUpDuration: Int64
let lowIntensityDuration: Int64
let highIntensityDuration: Int64
let restDuration: Int64
let cooldownDuration: Int64
let sets: Int64
let rounds: Int64
}
class IntervalTimerMO: NSManagedObject, Identifiable {
@NSManaged var id: UUID? @NSManaged var id: UUID?
@NSManaged var name: String @NSManaged var name: String
@NSManaged var userDescription: String? @NSManaged var userDescription: String?
@ -72,6 +85,34 @@ extension IntervalTimer {
return warmUpDuration + ((((lowIntensityDuration + highIntensityDuration) * sets) + restDuration) * rounds) + cooldownDuration return warmUpDuration + ((((lowIntensityDuration + highIntensityDuration) * sets) + restDuration) * rounds) + cooldownDuration
} }
} }
func copy(toMO: IntervalTimerMO) {
toMO.id = self.id
toMO.name = self.name
toMO.userDescription = self.description
toMO.warmUpDuration = self.warmUpDuration
toMO.lowIntensityDuration = self.lowIntensityDuration
toMO.highIntensityDuration = self.highIntensityDuration
toMO.restDuration = self.restDuration
toMO.cooldownDuration = self.cooldownDuration
toMO.sets = self.sets
toMO.rounds = self.rounds
}
static func create(fromMO: IntervalTimerMO) -> IntervalTimer {
return IntervalTimer(
id: fromMO.id,
name: fromMO.name,
description: fromMO.userDescription,
warmUpDuration: fromMO.warmUpDuration,
lowIntensityDuration: fromMO.lowIntensityDuration,
highIntensityDuration: fromMO.highIntensityDuration,
restDuration: fromMO.restDuration,
cooldownDuration: fromMO.cooldownDuration,
sets: fromMO.sets,
rounds: fromMO.rounds
)
}
} }
struct ActiveTimerState { struct ActiveTimerState {

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19H2" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19H2" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="IntervalTimer" representedClassName=".IntervalTimer" syncable="YES"> <entity name="IntervalTimer" representedClassName=".IntervalTimerMO" syncable="YES">
<attribute name="cooldownDuration" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="cooldownDuration" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="highIntensityDuration" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="highIntensityDuration" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/> <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>

View file

@ -183,20 +183,14 @@ class TimerDataStore: ObservableObject {
} }
func loadTimers() { func loadTimers() {
DispatchQueue.global(qos: .background).async {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "IntervalTimer") let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "IntervalTimer")
do { do {
let fetchedTimers = try self.persistentContainer.viewContext.fetch(fetchRequest) as! [IntervalTimer] let fetchedTimers = try self.persistentContainer.viewContext.fetch(fetchRequest) as! [IntervalTimerMO]
DispatchQueue.main.async { self.timers = .success(fetchedTimers.map { IntervalTimer.create(fromMO: $0) })
self.timers = .success(fetchedTimers)
}
} catch { } catch {
DispatchQueue.main.async {
self.timers = .failure(.failed(error)) self.timers = .failure(.failed(error))
} }
} }
}
}
func saveTimer( func saveTimer(
id: UUID? = nil, id: UUID? = nil,
@ -210,18 +204,11 @@ class TimerDataStore: ObservableObject {
sets: Int64, sets: Int64,
rounds: Int64 rounds: Int64
) { ) {
var timer: IntervalTimer var timer: IntervalTimerMO
if let uuid = id { if let uuid = id {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "IntervalTimer") timer = loadTimer(byId: uuid)!
fetchRequest.predicate = NSPredicate(format: "id = %@", uuid.uuidString)
let result = try! persistentContainer.viewContext.fetch(fetchRequest) as! [IntervalTimer]
if result.count == 1 {
timer = result[0]
} else { } else {
timer = IntervalTimer.init(entity: NSEntityDescription.entity(forEntityName: "IntervalTimer", in: persistentContainer.viewContext)!, insertInto: persistentContainer.viewContext) as IntervalTimer timer = IntervalTimerMO.init(entity: NSEntityDescription.entity(forEntityName: "IntervalTimer", in: persistentContainer.viewContext)!, insertInto: persistentContainer.viewContext) as IntervalTimerMO
}
} else {
timer = IntervalTimer.init(entity: NSEntityDescription.entity(forEntityName: "IntervalTimer", in: persistentContainer.viewContext)!, insertInto: persistentContainer.viewContext) as IntervalTimer
} }
timer.id = id ?? UUID() timer.id = id ?? UUID()
@ -242,19 +229,31 @@ class TimerDataStore: ObservableObject {
loadTimers() loadTimers()
if let currentState = activeTimer { if let currentState = activeTimer {
if currentState.timer.id == timer.id { if currentState.timer.id == timer.id {
activeTimer = currentState.copy(timer: timer) activeTimer = currentState.copy(timer: IntervalTimer.create(fromMO: timer))
} }
} }
} }
func deleteTimer(at: IndexSet) { func deleteTimer(at: IndexSet) {
let timer = try! self.timers.get()[at.first!] guard let timer = loadTimer(byId: try! self.timers.get()[at.first!].id!) else {
return
}
let viewContext = persistentContainer.viewContext let viewContext = persistentContainer.viewContext
viewContext.delete(timer) viewContext.delete(timer)
try! viewContext.save() try! viewContext.save()
loadTimers() loadTimers()
} }
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
}
private func loadSound(_ phase: Phase) { private func loadSound(_ phase: Phase) {
let filePath = Bundle.main.path(forResource: phase.rawValue, ofType: "mp3") let filePath = Bundle.main.path(forResource: phase.rawValue, ofType: "mp3")
let url = NSURL(fileURLWithPath: filePath!) let url = NSURL(fileURLWithPath: filePath!)

View file

@ -162,7 +162,7 @@ struct TimerFormView: View {
self.title = "New Timer" self.title = "New Timer"
} }
self._name = State(initialValue: timer?.name ?? "") self._name = State(initialValue: timer?.name ?? "")
self._description = State(initialValue: timer?.userDescription ?? "") self._description = State(initialValue: timer?.description ?? "")
self._warmDuration = State(initialValue: timer?.warmUpDuration ?? 300) self._warmDuration = State(initialValue: timer?.warmUpDuration ?? 300)
self._lowDuration = State(initialValue: timer?.lowIntensityDuration ?? 30) self._lowDuration = State(initialValue: timer?.lowIntensityDuration ?? 30)
self._highDuration = State(initialValue: timer?.highIntensityDuration ?? 60) self._highDuration = State(initialValue: timer?.highIntensityDuration ?? 60)

View file

@ -29,8 +29,8 @@ struct TimerListView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(timer.name) Text(timer.name)
.lineLimit(1) .lineLimit(1)
if timer.userDescription?.count ?? 0 > 0 { if timer.description?.count ?? 0 > 0 {
Text(timer.userDescription!) Text(timer.description!)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)

View file

@ -30,5 +30,4 @@ class IntervalTimerTests: XCTestCase {
// Put the code you want to measure the time of here. // Put the code you want to measure the time of here.
} }
} }
} }