Initial commit
@ -94,6 +94,8 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 376AE0522E6495EB00AB698B;
|
mainGroup = 376AE0522E6495EB00AB698B;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 376AE05C2E6495EB00AB698B /* Products */;
|
productRefGroup = 376AE05C2E6495EB00AB698B /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -261,6 +263,7 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -289,6 +292,7 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
{
|
{
|
||||||
"colors" : [
|
"colors" : [
|
||||||
{
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "display-p3",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.000",
|
||||||
|
"green" : "0.412",
|
||||||
|
"red" : "0.969"
|
||||||
|
}
|
||||||
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1,31 +1,172 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"filename" : "notification40.png",
|
||||||
"platform" : "ios",
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "notification60.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "settings58.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "settings87.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "spotlight80.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "spotlight120.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "iphone120.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "iphone180.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipadNotification20.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipadNotification40.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipadSettings29.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipadSettings58.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipadSpotlight40.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipadSpotlight80.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipad76.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipad152.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ipadPro167.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "appstore1024.png",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances" : [
|
"filename" : "mac16.png",
|
||||||
{
|
"idiom" : "mac",
|
||||||
"appearance" : "luminosity",
|
"scale" : "1x",
|
||||||
"value" : "dark"
|
"size" : "16x16"
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom" : "universal",
|
|
||||||
"platform" : "ios",
|
|
||||||
"size" : "1024x1024"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances" : [
|
"filename" : "mac32.png",
|
||||||
{
|
"idiom" : "mac",
|
||||||
"appearance" : "luminosity",
|
"scale" : "2x",
|
||||||
"value" : "tinted"
|
"size" : "16x16"
|
||||||
}
|
},
|
||||||
],
|
{
|
||||||
"idiom" : "universal",
|
"filename" : "mac32.png",
|
||||||
"platform" : "ios",
|
"idiom" : "mac",
|
||||||
"size" : "1024x1024"
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "mac64.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "mac128.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "mac256.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "mac256.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "mac512.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "mac512.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "mac1024.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/appstore1024.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/ipad152.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/ipad76.png
Normal file
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 4.7 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadPro167.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 7.3 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/iphone120.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/iphone180.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/mac1024.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/mac128.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/mac16.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/mac256.png
Normal file
After Width: | Height: | Size: 122 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/mac32.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/mac512.png
Normal file
After Width: | Height: | Size: 492 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/mac64.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/notification40.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/notification60.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/settings58.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/settings87.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/spotlight120.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
RIT Dining/Assets.xcassets/AppIcon.appiconset/spotlight80.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
@ -2,20 +2,126 @@
|
|||||||
// ContentView.swift
|
// ContentView.swift
|
||||||
// RIT Dining
|
// RIT Dining
|
||||||
//
|
//
|
||||||
// Created by Campbell Bagley on 8/31/25.
|
// Created by Campbell on 8/31/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
struct Location: Hashable {
|
||||||
|
let name: String
|
||||||
|
let todaysHours: String
|
||||||
|
let isOpen: openStatus
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
var body: some View {
|
@State private var isLoading = true
|
||||||
VStack {
|
@State private var rotationDegrees: Double = 0
|
||||||
Image(systemName: "globe")
|
@State private var diningLocations: [Location] = []
|
||||||
.imageScale(.large)
|
|
||||||
.foregroundStyle(.tint)
|
private var animation: Animation {
|
||||||
Text("Hello, world!")
|
.linear
|
||||||
|
.speed(0.1)
|
||||||
|
.repeatForever(autoreverses: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asynchronously fetch the data for all of the locations and parse their data to display it.
|
||||||
|
private func getDiningData() {
|
||||||
|
var newDiningLocations: [Location] = []
|
||||||
|
getDiningLocation { result in
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
switch result {
|
||||||
|
case .success(let locations):
|
||||||
|
for i in 0..<locations.locations.count {
|
||||||
|
let diningInfo = getLocationInfo(location: locations.locations[i])
|
||||||
|
print(diningInfo.name)
|
||||||
|
|
||||||
|
// I forgot this before and was really confused why all of the times were in UTC.
|
||||||
|
let display = DateFormatter()
|
||||||
|
display.timeZone = TimeZone(identifier: "America/New_York")
|
||||||
|
display.dateStyle = .none
|
||||||
|
display.timeStyle = .short
|
||||||
|
|
||||||
|
// Parse the open status and times to create the hours string. If either time is missing, assume it has no openings
|
||||||
|
// and use "Not Open Today". If there are times, then set those to be displayed.
|
||||||
|
let todaysHours: String
|
||||||
|
if diningInfo.openTime == .none || diningInfo.closeTime == .none {
|
||||||
|
todaysHours = "Not Open Today"
|
||||||
|
} else {
|
||||||
|
print("Open:", display.string(from: diningInfo.openTime!),
|
||||||
|
"Close:", display.string(from: diningInfo.closeTime!))
|
||||||
|
|
||||||
|
todaysHours = "\(display.string(from: diningInfo.openTime!)) - \(display.string(from: diningInfo.closeTime!))"
|
||||||
|
}
|
||||||
|
DispatchQueue.global().sync {
|
||||||
|
newDiningLocations.append(
|
||||||
|
Location(
|
||||||
|
name: diningInfo.name,
|
||||||
|
todaysHours: todaysHours,
|
||||||
|
isOpen: diningInfo.open
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.global().sync {
|
||||||
|
diningLocations = newDiningLocations
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
case .failure(let error): print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack() {
|
||||||
|
if isLoading {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "fork.knife.circle")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 75, height: 75)
|
||||||
|
.foregroundStyle(.accent)
|
||||||
|
.rotationEffect(.degrees(rotationDegrees))
|
||||||
|
.onAppear {
|
||||||
|
withAnimation(animation) {
|
||||||
|
rotationDegrees = 360.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text("Loading...")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
} else {
|
||||||
|
Form {
|
||||||
|
ForEach(diningLocations, id: \.self) { location in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(location.name)
|
||||||
|
Text(location.todaysHours)
|
||||||
|
switch location.isOpen {
|
||||||
|
case .open:
|
||||||
|
Text("Open")
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
case .closed:
|
||||||
|
Text("Closed")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
case .openingSoon:
|
||||||
|
Text("Opening Soon")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
case .closingSoon:
|
||||||
|
Text("Closing Soon")
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("RIT Dining")
|
||||||
|
.refreshable {
|
||||||
|
getDiningData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
getDiningData()
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
177
RIT Dining/FetchData.swift
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
//
|
||||||
|
// FetchData.swift
|
||||||
|
// RIT Dining
|
||||||
|
//
|
||||||
|
// Created by Campbell on 8/31/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// I'll be honest, I am NOT good at representing other people's JSON in my code. This kinda sucks but it gets the job done and can
|
||||||
|
// be improved later when I feel like it.
|
||||||
|
struct DiningLocation: Decodable {
|
||||||
|
struct Events: Decodable {
|
||||||
|
struct HoursException: Decodable {
|
||||||
|
let id: Int
|
||||||
|
let name: String
|
||||||
|
let startTime: String
|
||||||
|
let endTime: String
|
||||||
|
let startDate: String
|
||||||
|
let endDate: String
|
||||||
|
let open: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
let startTime: String
|
||||||
|
let endTime: String
|
||||||
|
let exceptions: [HoursException]?
|
||||||
|
}
|
||||||
|
|
||||||
|
let id: Int
|
||||||
|
let name: String
|
||||||
|
let events: [Events]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiningLocations: Decodable {
|
||||||
|
let locations: [DiningLocation]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InvalidHTTPError: Error {
|
||||||
|
case invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// This code came from another project of mine and was used to fetch the GitHub API for update checking. I just copied it here, but it can
|
||||||
|
// probably be made simpler for this use case.
|
||||||
|
func getDiningLocation(completionHandler: @escaping (Result<DiningLocations, Error>) -> Void) {
|
||||||
|
// The endpoint requires that you specify a date, so get today's.
|
||||||
|
let date_string = Date().formatted(.iso8601
|
||||||
|
.year().month().day()
|
||||||
|
.dateSeparator(.dash))
|
||||||
|
let url_string = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-all?date=\(date_string)"
|
||||||
|
|
||||||
|
guard let url = URL(string: url_string) else {
|
||||||
|
print("Invalid URL")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let request = URLRequest(url: url)
|
||||||
|
|
||||||
|
URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
guard case .none = error else { return }
|
||||||
|
|
||||||
|
guard let data = data else {
|
||||||
|
print("Data error.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
|
||||||
|
completionHandler(.failure(InvalidHTTPError.invalid))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded: Result<DiningLocations, Error> = Result(catching: { try JSONDecoder().decode(DiningLocations.self, from: data) })
|
||||||
|
completionHandler(decoded)
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum openStatus {
|
||||||
|
case open
|
||||||
|
case closed
|
||||||
|
case openingSoon
|
||||||
|
case closingSoon
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiningInfo {
|
||||||
|
let id: Int
|
||||||
|
let name: String
|
||||||
|
let openTime: Date?
|
||||||
|
let closeTime: Date?
|
||||||
|
let open: openStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLocationInfo(location: DiningLocation) -> DiningInfo {
|
||||||
|
print("beginning parse for \(location.name)")
|
||||||
|
|
||||||
|
// Early return if there are no events, good for things like the food trucks which can very easily have no openings in a week.
|
||||||
|
if location.events.isEmpty {
|
||||||
|
return DiningInfo(
|
||||||
|
id: location.id,
|
||||||
|
name: location.name,
|
||||||
|
openTime: .none,
|
||||||
|
closeTime: .none,
|
||||||
|
open: .closed)
|
||||||
|
}
|
||||||
|
|
||||||
|
let openString: String
|
||||||
|
let closeString: String
|
||||||
|
|
||||||
|
// Dining locations have a regular schedule, but then they also have exceptions listed for days like weekends or holidays. If there
|
||||||
|
// are exceptions, use those times for the day, otherwise we can just use the default times.
|
||||||
|
if let exceptions = location.events[0].exceptions, !exceptions.isEmpty {
|
||||||
|
// Early return if the exception for the day specifies that the location is closed. Used for things like holidays.
|
||||||
|
if !location.events[0].exceptions![0].open {
|
||||||
|
return DiningInfo(
|
||||||
|
id: location.id,
|
||||||
|
name: location.name,
|
||||||
|
openTime: .none,
|
||||||
|
closeTime: .none,
|
||||||
|
open: .closed)
|
||||||
|
}
|
||||||
|
openString = location.events[0].exceptions![0].startTime
|
||||||
|
closeString = location.events[0].exceptions![0].endTime
|
||||||
|
} else {
|
||||||
|
openString = location.events[0].startTime
|
||||||
|
closeString = location.events[0].endTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// I hate all of this date component nonsense.
|
||||||
|
let openParts = openString.split(separator: ":").map { Int($0) ?? 0 }
|
||||||
|
let openTimeComponents = DateComponents(hour: openParts[0], minute: openParts[1], second: openParts[2])
|
||||||
|
|
||||||
|
let closeParts = closeString.split(separator: ":").map { Int($0) ?? 0 }
|
||||||
|
let closeTimeComponents = DateComponents(hour: closeParts[0], minute: closeParts[1], second: closeParts[2])
|
||||||
|
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let now = Date()
|
||||||
|
|
||||||
|
let openDate = calendar.date(
|
||||||
|
bySettingHour: openTimeComponents.hour!,
|
||||||
|
minute: openTimeComponents.minute!,
|
||||||
|
second: openTimeComponents.second!,
|
||||||
|
of: now)!
|
||||||
|
|
||||||
|
var closeDate = calendar.date(
|
||||||
|
bySettingHour: closeTimeComponents.hour!,
|
||||||
|
minute: closeTimeComponents.minute!,
|
||||||
|
second: closeTimeComponents.second!,
|
||||||
|
of: now)!
|
||||||
|
|
||||||
|
// If the closing time is less than or equal to the opening time, it's probably midnight and means either open until midnight
|
||||||
|
// or open 24/7, in the case of Bytes.
|
||||||
|
if closeDate <= openDate {
|
||||||
|
closeDate = calendar.date(byAdding: .day, value: 1, to: closeDate)!
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can probably be done in a cleaner way but it's okay for now. If the location is open but the close date is within the next
|
||||||
|
// 30 minutes, label it as closing soon, and do the opposite if it's closed but the open date is within the next 30 minutes.
|
||||||
|
let isOpen = (now >= openDate && now <= closeDate)
|
||||||
|
let openStatus: openStatus
|
||||||
|
if isOpen {
|
||||||
|
if closeDate < calendar.date(byAdding: .minute, value: 30, to: now)! {
|
||||||
|
openStatus = .closingSoon
|
||||||
|
} else {
|
||||||
|
openStatus = .open
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if openDate < calendar.date(byAdding: .minute, value: 30, to: now)! {
|
||||||
|
openStatus = .openingSoon
|
||||||
|
} else {
|
||||||
|
openStatus = .closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiningInfo(
|
||||||
|
id: location.id,
|
||||||
|
name: location.name,
|
||||||
|
openTime: openDate,
|
||||||
|
closeTime: closeDate,
|
||||||
|
open: openStatus)
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
// RIT_DiningApp.swift
|
// RIT_DiningApp.swift
|
||||||
// RIT Dining
|
// RIT Dining
|
||||||
//
|
//
|
||||||
// Created by Campbell Bagley on 8/31/25.
|
// Created by Campbell on 8/31/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|