288 lines
9.7 KiB
Swift
288 lines
9.7 KiB
Swift
//
|
|
// ContentView.swift
|
|
// Splits
|
|
//
|
|
// Created by Isaac Greene on 4/3/22.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Foundation
|
|
|
|
/// Creates the primary tab of ``SplitsApp``
|
|
///
|
|
/// e
|
|
struct ContentView: View {
|
|
enum Field {
|
|
case distance
|
|
case hours
|
|
case minutes
|
|
case seconds
|
|
}
|
|
|
|
@FocusState private var nameIsFocused: Bool
|
|
@FocusState private var focusedField: Field?
|
|
var SISystem = ["km","mi"]
|
|
@State var timeHours: String = ""
|
|
@State var timeMinutes: String = ""
|
|
@State var timeSeconds: String = ""
|
|
@State var selectedSystem: String = "km"
|
|
@State var distance: String = ""
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
VStack {
|
|
VStack {
|
|
TextField("Enter distance", text: $distance)
|
|
.padding()
|
|
.keyboardType(.decimalPad)
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused($nameIsFocused)
|
|
.focused($focusedField, equals: .distance)
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .keyboard) {
|
|
Button("Done") {
|
|
nameIsFocused = false
|
|
}
|
|
Button("Clear") {
|
|
distance = ""
|
|
timeHours = ""
|
|
timeMinutes = ""
|
|
timeSeconds = ""
|
|
}
|
|
Spacer()
|
|
Button("Back") {
|
|
if focusedField == .distance {
|
|
focusedField = nil
|
|
} else if focusedField == .hours {
|
|
focusedField = .distance
|
|
} else if focusedField == .minutes {
|
|
focusedField = .hours
|
|
} else {
|
|
focusedField = .minutes
|
|
}
|
|
}
|
|
Button("Next") {
|
|
if focusedField == .distance {
|
|
focusedField = .hours
|
|
} else if focusedField == .hours {
|
|
focusedField = .minutes
|
|
} else if focusedField == .minutes {
|
|
focusedField = .seconds
|
|
} else {
|
|
focusedField = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
VStack {
|
|
Text("Unit of measurement:")
|
|
Picker("System of measurement", selection: $selectedSystem, content: {
|
|
ForEach(SISystem, id: \.self, content: { unit in
|
|
Text(unit)
|
|
})
|
|
})
|
|
.pickerStyle(.segmented)
|
|
.frame(minWidth: 60, maxWidth: 300)
|
|
HStack {
|
|
VStack {
|
|
Text("Hours")
|
|
TextField("Enter hours", text: $timeHours)
|
|
.keyboardType(.numberPad)
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused($nameIsFocused)
|
|
.focused($focusedField, equals: .hours)
|
|
}
|
|
.frame(minWidth: 100)
|
|
.padding(.trailing, -15)
|
|
.padding()
|
|
VStack {
|
|
Text("Minutes")
|
|
TextField("Enter minutes", text: $timeMinutes)
|
|
.keyboardType(.numberPad)
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused($nameIsFocused)
|
|
.focused($focusedField, equals: .minutes)
|
|
}
|
|
.frame(minWidth: 100)
|
|
.padding(.trailing, -15)
|
|
.padding(.leading, -15)
|
|
.padding()
|
|
VStack {
|
|
Text("Seconds")
|
|
TextField("Enter seconds", text: $timeSeconds)
|
|
.keyboardType(.numberPad)
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused($nameIsFocused)
|
|
.focused($focusedField, equals: .seconds)
|
|
}
|
|
.frame(minWidth: 100)
|
|
.padding(.leading, -15)
|
|
.padding()
|
|
}
|
|
}
|
|
PaceResults(timeHours: $timeHours, timeMinutes: $timeMinutes, timeSeconds: $timeSeconds, selectedSystem: $selectedSystem, distance: $distance)
|
|
}
|
|
.frame(maxWidth: 700)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PaceResults: View {
|
|
@Binding var timeHours: String
|
|
@Binding var timeMinutes: String
|
|
@Binding var timeSeconds: String
|
|
@Binding var selectedSystem: String
|
|
@Binding var distance: String
|
|
|
|
var body: some View {
|
|
let distanceDub:Double = ((Double(distance) != 0 ? Double(removeLeadingZeros(distance: &distance)) ?? 1.0 : 1.0))
|
|
// because of some conversions I have to do,
|
|
// this constant is a double just to make things easier.
|
|
// this has to be one because the pace is calculated
|
|
// by time / distance, and you can't divide by 0.
|
|
// I could just make the TextField have a default value
|
|
// but then my message would disappear to let you know
|
|
// what to enter in that box
|
|
|
|
let multiplier = (selectedSystem == "mi" ? 1.609344 : 0.6213711922)
|
|
let notSelectedSystem = (selectedSystem == "km" ? "mi" : "km")
|
|
let convertedDistanceString:String = {
|
|
let convertedDistance = distanceDub * multiplier
|
|
return (distance == "" ? "0" : String(format: "%.3f", convertedDistance))
|
|
}()
|
|
|
|
let convertedSeconds:Double = (Double(timeSeconds) ?? 0) * (1.6666666666666666666666666)
|
|
let timeSecondsInt:Int = Int(timeSeconds) ?? 0
|
|
let timeSecondsUnderSixty:Int = (timeSecondsInt % 60)
|
|
// this section takes the seconds and multiplies it by 1.66
|
|
// so that 60 seconds becomes 100 (as in 100% of a minute)
|
|
// and this means that 30 seconds becomes 50 (as in 50% of a minute)
|
|
// which allows us to to calculate our pace. Without this
|
|
// the pace would be all wrong.
|
|
|
|
let timeSecondsToMinutes:Int = (timeSecondsInt - timeSecondsUnderSixty) / 60
|
|
// this takes the seconde and converts it to minutes so 78
|
|
// seconds will turn into 1 minute and 18 seconds leftover
|
|
// with the minutes saved in this value and the seconds
|
|
// disregarded because they're saved in timeSecondsUnderSixty
|
|
|
|
let timeMinutesInt:Int = (Int(timeMinutes) ?? 0) + (timeSecondsToMinutes)
|
|
let timeMinutesUnderSixty:Int = timeMinutesInt % 60
|
|
let timeMinutesToHours:Int = (timeMinutesInt - timeMinutesUnderSixty) / 60
|
|
// this section takes the minutes (which it combines the
|
|
// minutes with the timeSecondsToMinutes) then finds out how
|
|
// many hours (multiples of 60) are in the minutes value
|
|
// and saves the hours in timeMinutesToHours and the remaining
|
|
// minutes in `timeMinutesUnderSixty`
|
|
|
|
let timeMinutesDouble:Double = Double(timeMinutes) ?? 0.0
|
|
// this line of code takes the binding $timeMinutes which
|
|
// is a string and turns it into a number of type Double.
|
|
// while the TextField is a number pad, on iPads, Mac computers
|
|
// and using copy/paste you can get other characters.
|
|
// if the text field contains anything other than a number
|
|
// or a single decimal, this value instantly becomes 0.0
|
|
|
|
let pace:Double = {
|
|
let actualTime:Double = timeMinutesDouble + (convertedSeconds / 100) + ((Double(timeHours) ?? 0) * 60)
|
|
// adds the minutes, hours, and seconds all together to get
|
|
// a single value in terms of minutes, so that 1:08:45
|
|
// becomes 68.75 which is a nice number we can do math on
|
|
// and this number is never directly seen by the user
|
|
return actualTime / distanceDub
|
|
}()
|
|
|
|
let paceOpposite = (selectedSystem == "km" ? (pace * 1.609344) : (pace * 0.6213711922))
|
|
|
|
let paceFormatted:String = {
|
|
let paceSeconds = pace.truncatingRemainder(dividingBy: 1.0)
|
|
let paceMinutes = (pace.truncatingRemainder(dividingBy: 60.0) - paceSeconds)
|
|
let paceHours = String(format: "%.0f", ((pace - paceMinutes) / 60))
|
|
let reConvertedSeconds = (paceSeconds / 1.666666666666666666) * 100
|
|
let properTimeMS = String(format: "%02d:%02d", Int(paceMinutes), Int(reConvertedSeconds.rounded()))
|
|
return (pace >= 60 ? "\(paceHours):\(properTimeMS)" : "\(properTimeMS)")
|
|
}()
|
|
|
|
let paceFormattedOpposite:String = {
|
|
let paceSecondsOpposite = paceOpposite.truncatingRemainder(dividingBy: 1.0)
|
|
let paceMinutesOpposite = (paceOpposite.truncatingRemainder(dividingBy: 60.0) - paceSecondsOpposite)
|
|
let paceHoursOpposite = String(format: "%.0f", ((paceOpposite - paceMinutesOpposite) / 60))
|
|
let reConvertedSecondsOpposite = (paceSecondsOpposite / 1.666666666666666666) * 100
|
|
let properTimeMSOpposite = String(format: "%02d:%02d", Int(paceMinutesOpposite), Int(reConvertedSecondsOpposite.rounded()))
|
|
return (paceOpposite >= 60 ? "\(paceHoursOpposite):\(properTimeMSOpposite)" : "\(properTimeMSOpposite)")
|
|
}()
|
|
|
|
let hoursFormatted:String = {
|
|
let totalHours:Double = Double(timeMinutesToHours) + (Double(timeHours) ?? 0)
|
|
// this takes the number of hours in the binding $timeHours
|
|
// and the hours calculated in the previous section
|
|
// and adds them together to get our total number of hours.
|
|
return String(format: "%.0f", totalHours)
|
|
}()
|
|
|
|
|
|
let leadingZeros:String = String(format: "%02d:%02d", timeMinutesUnderSixty, timeSecondsUnderSixty)
|
|
// this takes the minutes and the seconds and adds leading
|
|
// zeros to them, so that 5 minutes 7 seconds will show
|
|
// as 05:07 which is a format most people, and certainly
|
|
// the people this app is intended for, will understand
|
|
|
|
let pacePerHour: String = {
|
|
let pph = 60 / pace
|
|
return (pace != 0 ? String(format: "%.2f", pph) : "0")
|
|
}()
|
|
|
|
let pacePerHourOpposite: String = {
|
|
let npph = 60 / paceOpposite
|
|
return (pace != 0 ? String(format: "%.2f", npph) : "0")
|
|
}()
|
|
|
|
VStack {
|
|
HStack {
|
|
Text("\(hoursFormatted):\(leadingZeros)")
|
|
.padding()
|
|
.frame(minWidth: 100)
|
|
VStack (alignment: .leading) {
|
|
Text("\(roundString(Double(removeLeadingZeros(distance: &distance)) ?? 0))\(selectedSystem)")
|
|
Text("\(convertedDistanceString)\(notSelectedSystem)")
|
|
}
|
|
.frame(minWidth: 100)
|
|
}
|
|
HStack {
|
|
VStack(alignment: .leading) {
|
|
Text("\(paceFormatted)/\(selectedSystem)")
|
|
Text("\(paceFormattedOpposite)/\(notSelectedSystem)")
|
|
}
|
|
.frame(minWidth: 100)
|
|
VStack(alignment: .leading) {
|
|
Text("\(pacePerHour) \(selectedSystem)/hr")
|
|
Text("\(pacePerHourOpposite) \(notSelectedSystem)/hr")
|
|
}
|
|
.frame(minWidth: 100)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
func removeLeadingZeros(distance: inout String) -> String {
|
|
while distance.starts(with: "0") {
|
|
distance = distance.removing(prefix: "0")
|
|
}
|
|
return distance
|
|
}
|
|
|
|
func roundString(_ variable: Double) -> String {
|
|
if distance.contains(".") {
|
|
let formattedString = String(format: "%.3f", variable)
|
|
return formattedString
|
|
} else {
|
|
return distance
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ContentView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
ContentView()
|
|
}
|
|
}
|