Initial commit

This commit is contained in:
Campbell 2025-09-01 10:36:24 -04:00
parent 7534ad69fe
commit c2fe65cb59
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
32 changed files with 467 additions and 29 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
# RIT-Dining

View File

@ -94,6 +94,8 @@
);
mainGroup = 376AE0522E6495EB00AB698B;
minimizedProjectReferenceProxies = 1;
packageReferences = (
);
preferredProjectObjectVersion = 77;
productRefGroup = 376AE05C2E6495EB00AB698B /* Products */;
projectDirPath = "";
@ -261,6 +263,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -289,6 +292,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -1,6 +1,15 @@
{
"colors" : [
{
"color" : {
"color-space" : "display-p3",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.412",
"red" : "0.969"
}
},
"idiom" : "universal"
}
],

View File

@ -1,31 +1,172 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"filename" : "notification40.png",
"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"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"filename" : "mac16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"filename" : "mac32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "mac32.png",
"idiom" : "mac",
"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" : {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -2,20 +2,126 @@
// ContentView.swift
// RIT Dining
//
// Created by Campbell Bagley on 8/31/25.
// Created by Campbell on 8/31/25.
//
import SwiftUI
struct Location: Hashable {
let name: String
let todaysHours: String
let isOpen: openStatus
}
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
@State private var isLoading = true
@State private var rotationDegrees: Double = 0
@State private var diningLocations: [Location] = []
private var animation: Animation {
.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
View 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)
}

View File

@ -2,7 +2,7 @@
// RIT_DiningApp.swift
// RIT Dining
//
// Created by Campbell Bagley on 8/31/25.
// Created by Campbell on 8/31/25.
//
import SwiftUI