Build out most of the UI and functionality
Kinda forgot to commit as I went. Better late than never I guess
This commit is contained in:
parent
dfa4780bd6
commit
e73a8b198e
13 changed files with 968 additions and 31 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
IntervalTimer.xcodeproj/xcuserdata
|
|
@ -7,15 +7,22 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
80465F802543932100710741 /* TimerDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80465F7F2543932100710741 /* TimerDataStore.swift */; };
|
||||
80465F82254395D700710741 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80465F81254395D700710741 /* Extensions.swift */; };
|
||||
80465F862544774700710741 /* CollapsibleFormRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80465F852544774700710741 /* CollapsibleFormRowView.swift */; };
|
||||
80465F8825447ACE00710741 /* DurationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80465F8725447ACE00710741 /* DurationPicker.swift */; };
|
||||
80465F8A2547A74700710741 /* ActiveTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80465F892547A74700710741 /* ActiveTimerView.swift */; };
|
||||
80F8B6F5254276380024077E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B6F4254276380024077E /* AppDelegate.swift */; };
|
||||
80F8B6F7254276380024077E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B6F6254276380024077E /* SceneDelegate.swift */; };
|
||||
80F8B6FA254276380024077E /* IntervalTimer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B6F8254276380024077E /* IntervalTimer.xcdatamodeld */; };
|
||||
80F8B6FC254276380024077E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B6FB254276380024077E /* ContentView.swift */; };
|
||||
80F8B6FC254276380024077E /* TimerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B6FB254276380024077E /* TimerListView.swift */; };
|
||||
80F8B6FE2542763A0024077E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 80F8B6FD2542763A0024077E /* Assets.xcassets */; };
|
||||
80F8B7012542763A0024077E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 80F8B7002542763A0024077E /* Preview Assets.xcassets */; };
|
||||
80F8B7042542763A0024077E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 80F8B7022542763A0024077E /* LaunchScreen.storyboard */; };
|
||||
80F8B70F2542763B0024077E /* IntervalTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B70E2542763B0024077E /* IntervalTimerTests.swift */; };
|
||||
80F8B71A2542763B0024077E /* IntervalTimerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B7192542763B0024077E /* IntervalTimerUITests.swift */; };
|
||||
80F8B728254276E70024077E /* IntervalTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B727254276E70024077E /* IntervalTimer.swift */; };
|
||||
80F8B72A254312820024077E /* TimerFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80F8B729254312820024077E /* TimerFormView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -36,11 +43,16 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
80465F7F2543932100710741 /* TimerDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerDataStore.swift; sourceTree = "<group>"; };
|
||||
80465F81254395D700710741 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
80465F852544774700710741 /* CollapsibleFormRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleFormRowView.swift; sourceTree = "<group>"; };
|
||||
80465F8725447ACE00710741 /* DurationPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationPicker.swift; sourceTree = "<group>"; };
|
||||
80465F892547A74700710741 /* ActiveTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveTimerView.swift; sourceTree = "<group>"; };
|
||||
80F8B6F1254276380024077E /* IntervalTimer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntervalTimer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
80F8B6F4254276380024077E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
80F8B6F6254276380024077E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
80F8B6F9254276380024077E /* IntervalTimer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = IntervalTimer.xcdatamodel; sourceTree = "<group>"; };
|
||||
80F8B6FB254276380024077E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
80F8B6FB254276380024077E /* TimerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerListView.swift; sourceTree = "<group>"; };
|
||||
80F8B6FD2542763A0024077E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
80F8B7002542763A0024077E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
80F8B7032542763A0024077E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
|
@ -51,6 +63,8 @@
|
|||
80F8B7152542763B0024077E /* IntervalTimerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntervalTimerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
80F8B7192542763B0024077E /* IntervalTimerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalTimerUITests.swift; sourceTree = "<group>"; };
|
||||
80F8B71B2542763B0024077E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
80F8B727254276E70024077E /* IntervalTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalTimer.swift; sourceTree = "<group>"; };
|
||||
80F8B729254312820024077E /* TimerFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerFormView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -103,12 +117,19 @@
|
|||
children = (
|
||||
80F8B6F4254276380024077E /* AppDelegate.swift */,
|
||||
80F8B6F6254276380024077E /* SceneDelegate.swift */,
|
||||
80F8B6FB254276380024077E /* ContentView.swift */,
|
||||
80F8B6FB254276380024077E /* TimerListView.swift */,
|
||||
80F8B6FD2542763A0024077E /* Assets.xcassets */,
|
||||
80F8B7022542763A0024077E /* LaunchScreen.storyboard */,
|
||||
80F8B7052542763B0024077E /* Info.plist */,
|
||||
80F8B6F8254276380024077E /* IntervalTimer.xcdatamodeld */,
|
||||
80F8B6FF2542763A0024077E /* Preview Content */,
|
||||
80F8B727254276E70024077E /* IntervalTimer.swift */,
|
||||
80F8B729254312820024077E /* TimerFormView.swift */,
|
||||
80465F7F2543932100710741 /* TimerDataStore.swift */,
|
||||
80465F81254395D700710741 /* Extensions.swift */,
|
||||
80465F852544774700710741 /* CollapsibleFormRowView.swift */,
|
||||
80465F8725447ACE00710741 /* DurationPicker.swift */,
|
||||
80465F892547A74700710741 /* ActiveTimerView.swift */,
|
||||
);
|
||||
path = IntervalTimer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -202,7 +223,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1150;
|
||||
LastUpgradeCheck = 1150;
|
||||
LastUpgradeCheck = 1200;
|
||||
ORGANIZATIONNAME = "William Brawner";
|
||||
TargetAttributes = {
|
||||
80F8B6F0254276380024077E = {
|
||||
|
@ -271,9 +292,16 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
80F8B6FA254276380024077E /* IntervalTimer.xcdatamodeld in Sources */,
|
||||
80465F802543932100710741 /* TimerDataStore.swift in Sources */,
|
||||
80F8B6F5254276380024077E /* AppDelegate.swift in Sources */,
|
||||
80F8B6FC254276380024077E /* ContentView.swift in Sources */,
|
||||
80465F82254395D700710741 /* Extensions.swift in Sources */,
|
||||
80F8B6FC254276380024077E /* TimerListView.swift in Sources */,
|
||||
80F8B728254276E70024077E /* IntervalTimer.swift in Sources */,
|
||||
80465F862544774700710741 /* CollapsibleFormRowView.swift in Sources */,
|
||||
80465F8825447ACE00710741 /* DurationPicker.swift in Sources */,
|
||||
80F8B72A254312820024077E /* TimerFormView.swift in Sources */,
|
||||
80F8B6F7254276380024077E /* SceneDelegate.swift in Sources */,
|
||||
80465F8A2547A74700710741 /* ActiveTimerView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -346,6 +374,7 @@
|
|||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
|
@ -370,7 +399,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.5;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
@ -406,6 +435,7 @@
|
|||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
|
@ -424,7 +454,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.5;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
|
111
IntervalTimer/ActiveTimerView.swift
Normal file
111
IntervalTimer/ActiveTimerView.swift
Normal file
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// ActiveTimerView.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/26/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActiveTimerView: View {
|
||||
@EnvironmentObject var dataStore: TimerDataStore
|
||||
|
||||
var body: some View {
|
||||
if let state = dataStore.activeTimer {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(state.phase.rawValue)
|
||||
Text(state.timeRemaining.toDurationString())
|
||||
.font(Font.system(size: 200).monospacedDigit())
|
||||
.lineLimit(1)
|
||||
.scaledToFit()
|
||||
.minimumScaleFactor(0.1)
|
||||
.padding()
|
||||
TimerControlsView(dataStore: self.dataStore, timerRunning: .constant(state.isRunning))
|
||||
Spacer()
|
||||
HStack {
|
||||
LabeledCounter(label: "Sets", counter: state.currentSet)
|
||||
Spacer()
|
||||
LabeledCounter(label: "Round", counter: state.currentRound)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("\(state.timer.name)", displayMode: .inline)
|
||||
.background(state.backgroundColor)
|
||||
.edgesIgnoringSafeArea(.vertical)
|
||||
.animation(.default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimerControlsView: View {
|
||||
let dataStore: TimerDataStore
|
||||
@Binding var timerRunning: Bool
|
||||
private var toggleButtonImage: String {
|
||||
get {
|
||||
if self.timerRunning {
|
||||
return "pause.fill"
|
||||
} else {
|
||||
return "play.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
var buttonSize: CGFloat = 32
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.dataStore.goBack()
|
||||
}, label: {
|
||||
Image(systemName: "backward.end.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: buttonSize, height: buttonSize)
|
||||
})
|
||||
.foregroundColor(.primary)
|
||||
.padding()
|
||||
Button(action: {
|
||||
self.dataStore.toggle()
|
||||
}, label: {
|
||||
Image(systemName: toggleButtonImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: buttonSize, height: buttonSize)
|
||||
})
|
||||
.foregroundColor(.primary)
|
||||
.padding()
|
||||
Button(action: {
|
||||
self.dataStore.goForward()
|
||||
}, label: {
|
||||
Image(systemName: "forward.end.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: buttonSize, height: buttonSize)
|
||||
})
|
||||
.foregroundColor(.primary)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LabeledCounter: View {
|
||||
let label: String
|
||||
let counter: Int64
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(label)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(String(counter))
|
||||
.multilineTextAlignment(.center)
|
||||
.font(Font.title.monospacedDigit())
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct ActiveTimerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ActiveTimerView().environmentObject(TimerDataStore() {})
|
||||
}
|
||||
}
|
45
IntervalTimer/CollapsibleFormRowView.swift
Normal file
45
IntervalTimer/CollapsibleFormRowView.swift
Normal file
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// CollapsibleFormRowView.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/24/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CollapsibleFormRowView: View {
|
||||
let rowTitle: String
|
||||
let rowValue: String
|
||||
let buttonAction: () -> Void
|
||||
@Binding var expandView: Bool
|
||||
let collapsibleView: AnyView
|
||||
|
||||
// This is a hacky workaround to get multiple if statements inside a single form
|
||||
var body: some View {
|
||||
VStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
self.buttonAction()
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
Text(rowTitle)
|
||||
Spacer()
|
||||
Text(rowValue)
|
||||
.font(Font.body.monospacedDigit())
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}).foregroundColor(.primary)
|
||||
if expandView {
|
||||
collapsibleView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CollapsibleFormRowView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CollapsibleFormRowView(rowTitle: "Sets", rowValue: "2", buttonAction: {}, expandView: .constant(true), collapsibleView: Text("Expanded").toAnyView())
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/22/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Hello, World!")
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
103
IntervalTimer/DurationPicker.swift
Normal file
103
IntervalTimer/DurationPicker.swift
Normal file
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// DurationPicker.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/24/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct DurationPicker: View {
|
||||
@State fileprivate var hours: Int64 {
|
||||
didSet {
|
||||
updateDuration()
|
||||
}
|
||||
}
|
||||
@State fileprivate var minutes: Int64 {
|
||||
didSet {
|
||||
updateDuration()
|
||||
}
|
||||
}
|
||||
@State fileprivate var seconds: Int64 {
|
||||
didSet {
|
||||
updateDuration()
|
||||
}
|
||||
}
|
||||
@Binding var selection: Int64
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
HStack {
|
||||
Picker("Hours", selection: self.$hours) {
|
||||
ForEach(0..<24) { hour in
|
||||
Text(String(format: "%02d", hour)).tag(Int64(hour))
|
||||
}
|
||||
}
|
||||
.onReceive(Just(self.hours)) { _ in
|
||||
self.updateDuration()
|
||||
}
|
||||
.frame(width: geometry.size.width/3, alignment: .center)
|
||||
.clipped()
|
||||
.pickerStyle(WheelPickerStyle())
|
||||
.labelsHidden()
|
||||
Picker("Minutes", selection: self.$minutes) {
|
||||
ForEach(0..<60) { minute in
|
||||
Text(String(format: "%02d", minute)).tag(Int64(minute))
|
||||
}
|
||||
}
|
||||
.onReceive(Just(self.minutes)) { _ in
|
||||
self.updateDuration()
|
||||
}
|
||||
.frame(width: geometry.size.width/3, alignment: .center)
|
||||
.clipped()
|
||||
.pickerStyle(WheelPickerStyle())
|
||||
.labelsHidden()
|
||||
Picker("Seconds", selection: self.$seconds) {
|
||||
ForEach(0..<60) { second in
|
||||
Text(String(format: "%02d", second)).tag(Int64(second))
|
||||
}
|
||||
}
|
||||
.onReceive(Just(self.seconds)) { _ in
|
||||
self.updateDuration()
|
||||
}
|
||||
.frame(width: geometry.size.width/3, alignment: .center)
|
||||
.clipped()
|
||||
.pickerStyle(WheelPickerStyle())
|
||||
.labelsHidden()
|
||||
}
|
||||
}.frame(height: 200)
|
||||
}
|
||||
|
||||
init (_ selection: Binding<Int64>) {
|
||||
self._selection = selection
|
||||
var seconds: Int64 = selection.wrappedValue
|
||||
var hours: Int64 = 0
|
||||
if (seconds >= 3600) {
|
||||
hours = seconds / 3600
|
||||
seconds -= hours * 3600
|
||||
}
|
||||
|
||||
var minutes: Int64 = 0
|
||||
if (seconds >= 60) {
|
||||
minutes = seconds / 60
|
||||
seconds -= minutes * 60
|
||||
}
|
||||
self._hours = State(initialValue: hours)
|
||||
self._minutes = State(initialValue: minutes)
|
||||
self._seconds = State(initialValue: seconds)
|
||||
}
|
||||
}
|
||||
|
||||
extension DurationPicker {
|
||||
func updateDuration() {
|
||||
self.selection = (self.hours * 3600) + (self.minutes * 60) + self.seconds
|
||||
}
|
||||
}
|
||||
|
||||
struct DurationPicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DurationPicker(.constant(0))
|
||||
}
|
||||
}
|
38
IntervalTimer/Extensions.swift
Normal file
38
IntervalTimer/Extensions.swift
Normal file
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// Extensions.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/23/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func toAnyView() -> AnyView {
|
||||
return AnyView(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Int64 {
|
||||
func toDurationString() -> String {
|
||||
var seconds: Int64 = self
|
||||
var hours: Int64 = 0
|
||||
if (seconds >= 3600) {
|
||||
hours = seconds / 3600
|
||||
seconds -= hours * 3600
|
||||
}
|
||||
|
||||
var minutes: Int64 = 0
|
||||
if (seconds >= 60) {
|
||||
minutes = seconds / 60
|
||||
seconds -= minutes * 60
|
||||
}
|
||||
|
||||
if hours > 0 {
|
||||
return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
return String(format: "%02d:%02d", minutes, seconds)
|
||||
}
|
||||
}
|
111
IntervalTimer/IntervalTimer.swift
Normal file
111
IntervalTimer/IntervalTimer.swift
Normal file
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// Timer.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/22/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import SwiftUI
|
||||
|
||||
class IntervalTimer: NSManagedObject, Identifiable {
|
||||
@NSManaged var id: UUID?
|
||||
@NSManaged var name: String
|
||||
@NSManaged var userDescription: String?
|
||||
@NSManaged var warmUpDuration: Int64
|
||||
@NSManaged var lowIntensityDuration: Int64
|
||||
@NSManaged var highIntensityDuration: Int64
|
||||
@NSManaged var restDuration: Int64
|
||||
@NSManaged var cooldownDuration: Int64
|
||||
@NSManaged var sets: Int64
|
||||
@NSManaged var rounds: Int64
|
||||
}
|
||||
|
||||
enum Phase: String {
|
||||
case warmUp = "Warm-Up"
|
||||
case low = "Low Intensity"
|
||||
case high = "High Intensity"
|
||||
case rest = "Rest"
|
||||
case cooldown = "Cooldown"
|
||||
}
|
||||
|
||||
extension Phase {
|
||||
var backgroundColor: Color {
|
||||
get {
|
||||
switch self {
|
||||
case .warmUp:
|
||||
return Color(UIColor.systemBackground)
|
||||
case .low:
|
||||
return Color(UIColor.systemRed)
|
||||
case .high:
|
||||
return Color(UIColor.systemGreen)
|
||||
case .rest:
|
||||
return Color(UIColor.systemYellow)
|
||||
case .cooldown:
|
||||
return Color(UIColor.systemBlue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension IntervalTimer {
|
||||
func durationForPhase(phase: Phase) -> Int64 {
|
||||
switch phase {
|
||||
case .warmUp:
|
||||
return self.warmUpDuration
|
||||
case .low:
|
||||
return self.lowIntensityDuration
|
||||
case .high:
|
||||
return self.highIntensityDuration
|
||||
case .rest:
|
||||
return self.restDuration
|
||||
case .cooldown:
|
||||
return self.cooldownDuration
|
||||
}
|
||||
}
|
||||
|
||||
var totalDuration: Int64 {
|
||||
get {
|
||||
return warmUpDuration + ((((lowIntensityDuration + highIntensityDuration) * sets) + restDuration) * rounds) + cooldownDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ActiveTimerState {
|
||||
let timer: IntervalTimer
|
||||
let timeRemaining: Int64
|
||||
let currentSet: Int64
|
||||
let currentRound: Int64
|
||||
let soundId: Int?
|
||||
let phase: Phase
|
||||
let isRunning: Bool
|
||||
var backgroundColor: Color {
|
||||
get {
|
||||
return self.phase.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveTimerState {
|
||||
func copy(
|
||||
timer: IntervalTimer? = nil,
|
||||
timeRemaining: Int64? = nil,
|
||||
currentSet: Int64? = nil,
|
||||
currentRound: Int64? = nil,
|
||||
soundId: Int? = nil,
|
||||
phase: Phase? = nil,
|
||||
isRunning: Bool? = nil
|
||||
) -> ActiveTimerState {
|
||||
return ActiveTimerState(
|
||||
timer: timer ?? self.timer,
|
||||
timeRemaining: timeRemaining ?? self.timeRemaining,
|
||||
currentSet: currentSet ?? self.currentSet,
|
||||
currentRound: currentRound ?? self.currentRound,
|
||||
soundId: soundId ?? self.soundId,
|
||||
phase: phase ?? self.phase,
|
||||
isRunning: isRunning ?? self.isRunning
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="true" userDefinedModelVersionIdentifier="">
|
||||
<elements/>
|
||||
<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">
|
||||
<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="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="lowIntensityDuration" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="restDuration" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rounds" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sets" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="userDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="warmUpDuration" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="IntervalTimer" positionX="-63" positionY="-18" width="128" height="193"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -24,7 +24,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
|
||||
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
|
||||
// Add `@Environment(\.managedObjectContext)` in the views that will need the context.
|
||||
let contentView = ContentView().environment(\.managedObjectContext, context)
|
||||
let contentView = TimerListView().environment(\.managedObjectContext, context)
|
||||
.environmentObject(TimerDataStore() {})
|
||||
|
||||
// Use a UIHostingController as window root view controller.
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
|
|
235
IntervalTimer/TimerDataStore.swift
Normal file
235
IntervalTimer/TimerDataStore.swift
Normal file
|
@ -0,0 +1,235 @@
|
|||
//
|
||||
// TimerDataStore.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/23/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
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
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
phase: .high
|
||||
)
|
||||
case .cooldown:
|
||||
self.activeTimer = state.copy(
|
||||
timeRemaining: state.timer.highIntensityDuration,
|
||||
phase: .high
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func updateTimer() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func loadTimers() {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "IntervalTimer")
|
||||
do {
|
||||
let fetchedTimers = try self.persistentContainer.viewContext.fetch(fetchRequest) as! [IntervalTimer]
|
||||
DispatchQueue.main.async {
|
||||
self.timers = .success(fetchedTimers)
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.timers = .failure(.failed(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveTimer(
|
||||
id: UUID? = nil,
|
||||
name: String,
|
||||
description: String? = nil,
|
||||
warmUpDuration: Int64,
|
||||
lowIntensityDuration: Int64,
|
||||
highIntensityDuration: Int64,
|
||||
restDuration: Int64,
|
||||
cooldownDuration: Int64,
|
||||
sets: Int64,
|
||||
rounds: Int64
|
||||
) {
|
||||
let timer = IntervalTimer.init(entity: NSEntityDescription.entity(forEntityName: "IntervalTimer", in: persistentContainer.viewContext)!, insertInto: persistentContainer.viewContext) as IntervalTimer
|
||||
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
|
||||
viewContext.insert(timer)
|
||||
try! viewContext.save()
|
||||
loadTimers()
|
||||
}
|
||||
|
||||
func deleteTimer(_ timer: IntervalTimer) {
|
||||
let viewContext = persistentContainer.viewContext
|
||||
viewContext.delete(timer)
|
||||
try! viewContext.save()
|
||||
loadTimers()
|
||||
}
|
||||
|
||||
init(_ completionClosure: @escaping () -> ()) {
|
||||
persistentContainer = NSPersistentContainer(name: "IntervalTimer")
|
||||
persistentContainer.loadPersistentStores() { (description, error) in
|
||||
if let error = error {
|
||||
fatalError("Failed to load Core Data stack: \(error)")
|
||||
}
|
||||
self.loadTimers()
|
||||
completionClosure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TimerError: Error {
|
||||
case loading
|
||||
case failed(_ error: Error)
|
||||
}
|
188
IntervalTimer/TimerFormView.swift
Normal file
188
IntervalTimer/TimerFormView.swift
Normal file
|
@ -0,0 +1,188 @@
|
|||
//
|
||||
// TimerFormView.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/23/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TimerFormView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@EnvironmentObject var dataStore: TimerDataStore
|
||||
let title: String
|
||||
let timerId: UUID?
|
||||
@State var name: String
|
||||
@State var description: String
|
||||
@State var warmDuration: Int64
|
||||
@State var lowDuration: Int64
|
||||
@State var highDuration: Int64
|
||||
@State var restDuration: Int64
|
||||
@State var coolDuration: Int64
|
||||
@State var sets: Int64
|
||||
@State var rounds: Int64
|
||||
@State var activePicker: ActivePicker = .none
|
||||
|
||||
var setsPicker: AnyView {
|
||||
if self.activePicker == .sets {
|
||||
return Picker("Sets", selection: self.$sets) {
|
||||
ForEach(1..<100) {
|
||||
Text(String($0)).tag(Int64($0))
|
||||
}
|
||||
}
|
||||
.pickerStyle(WheelPickerStyle())
|
||||
.labelsHidden()
|
||||
.toAnyView()
|
||||
} else {
|
||||
return EmptyView().toAnyView()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
TextField("Name", text: self.$name)
|
||||
TextField("Description", text: self.$description)
|
||||
CollapsibleFormRowView(
|
||||
rowTitle: "Warm-up",
|
||||
rowValue: "\(warmDuration.toDurationString())",
|
||||
buttonAction: {
|
||||
self.activePicker = (self.activePicker == .warmUp) ? .none : .warmUp
|
||||
},
|
||||
expandView: .constant(self.activePicker == .warmUp),
|
||||
collapsibleView: DurationPicker($warmDuration).toAnyView()
|
||||
)
|
||||
CollapsibleFormRowView(
|
||||
rowTitle: "Low Intensity",
|
||||
rowValue: "\(lowDuration.toDurationString())",
|
||||
buttonAction: {
|
||||
self.activePicker = (self.activePicker == .lowIntensity) ? .none : .lowIntensity
|
||||
},
|
||||
expandView: .constant(self.activePicker == .lowIntensity),
|
||||
collapsibleView: DurationPicker($lowDuration).toAnyView()
|
||||
)
|
||||
CollapsibleFormRowView(
|
||||
rowTitle: "High Intensity",
|
||||
rowValue: "\(highDuration.toDurationString())",
|
||||
buttonAction: {
|
||||
self.activePicker = (self.activePicker == .highIntensity) ? .none : .highIntensity
|
||||
},
|
||||
expandView: .constant(self.activePicker == .highIntensity),
|
||||
collapsibleView: DurationPicker($highDuration).toAnyView()
|
||||
)
|
||||
CollapsibleFormRowView(
|
||||
rowTitle: "Rest",
|
||||
rowValue: "\(restDuration.toDurationString())",
|
||||
buttonAction: {
|
||||
self.activePicker = (self.activePicker == .rest) ? .none : .rest
|
||||
},
|
||||
expandView: .constant(self.activePicker == .rest),
|
||||
collapsibleView: DurationPicker($restDuration).toAnyView()
|
||||
)
|
||||
CollapsibleFormRowView(
|
||||
rowTitle: "Cooldown",
|
||||
rowValue: "\(coolDuration.toDurationString())",
|
||||
buttonAction: {
|
||||
self.activePicker = (self.activePicker == .cooldown) ? .none : .cooldown
|
||||
},
|
||||
expandView: .constant(self.activePicker == .cooldown),
|
||||
collapsibleView: DurationPicker($coolDuration).toAnyView()
|
||||
)
|
||||
CollapsibleFormRowView(
|
||||
rowTitle: "Sets",
|
||||
rowValue: "\(sets)",
|
||||
buttonAction: {
|
||||
self.activePicker = (self.activePicker == .sets) ? .none : .sets
|
||||
},
|
||||
expandView: .constant(self.activePicker == .sets),
|
||||
collapsibleView: Picker("Sets", selection: $sets) {
|
||||
ForEach(1..<100) {
|
||||
Text(String($0)).tag(Int64($0))
|
||||
}
|
||||
}
|
||||
.pickerStyle(WheelPickerStyle())
|
||||
.labelsHidden()
|
||||
.toAnyView()
|
||||
)
|
||||
CollapsibleFormRowView(
|
||||
rowTitle: "Rounds",
|
||||
rowValue: "\(rounds)",
|
||||
buttonAction: {
|
||||
self.activePicker = (self.activePicker == .rounds) ? .none : .rounds
|
||||
},
|
||||
expandView: .constant(self.activePicker == .rounds),
|
||||
collapsibleView: Picker("Rounds", selection: $rounds) {
|
||||
ForEach(1..<100) {
|
||||
Text(String($0)).tag(Int64($0))
|
||||
}
|
||||
}
|
||||
.pickerStyle(WheelPickerStyle())
|
||||
.labelsHidden()
|
||||
.toAnyView()
|
||||
)
|
||||
}
|
||||
.navigationBarTitle("\(self.title)", displayMode: .inline)
|
||||
.navigationBarItems(leading:
|
||||
Button(action: {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Text("Cancel")
|
||||
}), trailing:
|
||||
Button(action: {
|
||||
self.dataStore.saveTimer(
|
||||
id: self.timerId,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
warmUpDuration: self.warmDuration,
|
||||
lowIntensityDuration: self.lowDuration,
|
||||
highIntensityDuration: self.highDuration,
|
||||
restDuration: self.restDuration,
|
||||
cooldownDuration: self.coolDuration,
|
||||
sets: self.sets,
|
||||
rounds: self.rounds
|
||||
)
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Text("Save")
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init(_ timer: IntervalTimer? = nil) {
|
||||
self.timerId = timer?.id
|
||||
if self.timerId != nil {
|
||||
self.title = "Edit Timer"
|
||||
} else {
|
||||
self.title = "New Timer"
|
||||
}
|
||||
self._name = State(initialValue: timer?.name ?? "")
|
||||
self._description = State(initialValue: timer?.userDescription ?? "")
|
||||
self._warmDuration = State(initialValue: timer?.warmUpDuration ?? 300)
|
||||
self._lowDuration = State(initialValue: timer?.lowIntensityDuration ?? 30)
|
||||
self._highDuration = State(initialValue: timer?.highIntensityDuration ?? 60)
|
||||
self._restDuration = State(initialValue: timer?.restDuration ?? 60)
|
||||
self._coolDuration = State(initialValue: timer?.cooldownDuration ?? 300)
|
||||
self._sets = State(initialValue: timer?.sets ?? 4)
|
||||
self._rounds = State(initialValue: timer?.rounds ?? 2)
|
||||
}
|
||||
}
|
||||
|
||||
enum ActivePicker {
|
||||
case none
|
||||
case warmUp
|
||||
case lowIntensity
|
||||
case highIntensity
|
||||
case rest
|
||||
case cooldown
|
||||
case sets
|
||||
case rounds
|
||||
}
|
||||
|
||||
struct TimerFormView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
TimerFormView().environmentObject(TimerDataStore() {})
|
||||
}
|
||||
}
|
81
IntervalTimer/TimerListView.swift
Normal file
81
IntervalTimer/TimerListView.swift
Normal file
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// IntervalTimer
|
||||
//
|
||||
// Created by William Brawner on 10/22/20.
|
||||
// Copyright © 2020 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TimerListView: View {
|
||||
@State var isEditing: Bool = false
|
||||
@EnvironmentObject var dataStore: TimerDataStore
|
||||
|
||||
var stateContent: AnyView {
|
||||
switch dataStore.timers {
|
||||
case .success(let timers):
|
||||
if timers.count == 0 {
|
||||
return Text("Create a timer to get started")
|
||||
.toAnyView()
|
||||
} else {
|
||||
return List(timers) { timer in
|
||||
NavigationLink(
|
||||
destination: ActiveTimerView()
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: Button(action: {
|
||||
// TODO: Confirm before exiting if the timer isn't complete
|
||||
self.dataStore.closeTimer()
|
||||
}, label: { Image(systemName: "xmark") })
|
||||
.foregroundColor(.primary)
|
||||
),
|
||||
isActive: .constant(self.dataStore.hasActiveTimer)
|
||||
) {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(timer.name)
|
||||
.lineLimit(1)
|
||||
if timer.userDescription?.count ?? 0 > 0 {
|
||||
Text(timer.userDescription!)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text(timer.totalDuration.toDurationString())
|
||||
.font(Font.subheadline.monospacedDigit())
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(minHeight: 50)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
self.dataStore.openTimer(timer)
|
||||
}
|
||||
}
|
||||
}.toAnyView()
|
||||
}
|
||||
default:
|
||||
return Text("Loading...").toAnyView()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
stateContent
|
||||
.navigationBarTitle("Timers")
|
||||
.navigationBarItems(trailing: Button(action: {
|
||||
self.isEditing = true
|
||||
}, label: { Image(systemName: "plus").padding() } ))
|
||||
}
|
||||
.sheet(isPresented: $isEditing,
|
||||
onDismiss: { self.isEditing = false },
|
||||
content: { TimerFormView()})
|
||||
}
|
||||
}
|
||||
|
||||
struct TimerListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TimerListView().environmentObject(TimerDataStore() {})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue