// // 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("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) } } } } 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 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 paceOpposite = (selectedSystem == "km" ? (pace * 1.609344) : (pace * 0.6213711922)) 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 HStack { VStack (alignment: .leading) { Text("\(roundString(Double(removeLeadingZeros(distance: &distance)) ?? 0))\(selectedSystem)") Text("\(convertedDistanceString)\(notSelectedSystem)") } .frame(minWidth: 100) Text("\(hoursFormatted):\(leadingZeros)") .padding() .frame(minWidth: 100) VStack(alignment: .trailing) { Text("\(paceFormatted)/\(selectedSystem)") Text("\(paceFormattedOpposite)/\(notSelectedSystem)") } .frame(minWidth: 100) } } 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() } }