mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2025-10-18 22:26:19 -04:00
Lots of code cleanup
Separated types out into their own file to make them easier to keep track of, and described what they're all for better. Also removed unnecessary "Location" type used in ContentView to display data, because it was almost an exact duplicate of the type that it was being created from. Removing that removed a lot of pointless extra logic, and should make the processs of how it pulls and parses the data easier to understand. Multiple open periods for one location are also now sorted, so that the earliest open time will be shown first. Some locations have them flipped in the response data, so they were backwards before.
This commit is contained in:
parent
9f1d5c2078
commit
30c025e113
@ -7,24 +7,24 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Location: Hashable {
|
||||
let name: String
|
||||
let summary: String
|
||||
let desc: String
|
||||
let mapsUrl: String
|
||||
let todaysHours: [String]
|
||||
let isOpen: openStatus
|
||||
}
|
||||
|
||||
struct LocationList: View {
|
||||
let diningLocations: [Location]
|
||||
let diningLocations: [DiningLocation]
|
||||
|
||||
// I forgot this before and was really confused why all of the times were in UTC.
|
||||
private let display: DateFormatter = {
|
||||
let display = DateFormatter()
|
||||
display.timeZone = TimeZone(identifier: "America/New_York")
|
||||
display.dateStyle = .none
|
||||
display.timeStyle = .short
|
||||
return display
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
ForEach(diningLocations, id: \.self) { location in
|
||||
NavigationLink(destination: DetailView(location: location)) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(location.name)
|
||||
switch location.isOpen {
|
||||
switch location.open {
|
||||
case .open:
|
||||
Text("Open")
|
||||
.foregroundStyle(.green)
|
||||
@ -38,8 +38,13 @@ struct LocationList: View {
|
||||
Text("Closing Soon")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
ForEach(location.todaysHours, id: \.self) { hours in
|
||||
Text(hours)
|
||||
if let times = location.diningTimes, !times.isEmpty {
|
||||
ForEach(times, id: \.self) { time in
|
||||
Text("\(display.string(from: time.openTime)) - \(display.string(from: time.closeTime))")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
Text("Not Open Today")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@ -51,7 +56,7 @@ struct LocationList: View {
|
||||
struct ContentView: View {
|
||||
@State private var isLoading = true
|
||||
@State private var rotationDegrees: Double = 0
|
||||
@State private var diningLocations: [Location] = []
|
||||
@State private var diningLocations: [DiningLocation] = []
|
||||
@State private var lastRefreshed: Date?
|
||||
@State private var searchText: String = ""
|
||||
@State private var openLocationsOnly: Bool = false
|
||||
@ -64,49 +69,21 @@ struct ContentView: View {
|
||||
|
||||
// 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
|
||||
var newDiningLocations: [DiningLocation] = []
|
||||
getAllDiningInfo { 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.
|
||||
var todaysHours: [String] = []
|
||||
if diningInfo.diningTimes == .none {
|
||||
todaysHours = ["Not Open Today"]
|
||||
} else {
|
||||
for time in diningInfo.diningTimes! {
|
||||
print("Open:", display.string(from: time.openTime),
|
||||
"Close:", display.string(from: time.closeTime))
|
||||
todaysHours.append("\(display.string(from: time.openTime)) - \(display.string(from: time.closeTime))")
|
||||
}
|
||||
}
|
||||
DispatchQueue.global().sync {
|
||||
newDiningLocations.append(
|
||||
Location(
|
||||
name: diningInfo.name,
|
||||
summary: diningInfo.summary,
|
||||
desc: diningInfo.desc,
|
||||
mapsUrl: diningInfo.mapsUrl,
|
||||
todaysHours: todaysHours,
|
||||
isOpen: diningInfo.open
|
||||
)
|
||||
)
|
||||
lastRefreshed = Date()
|
||||
newDiningLocations.append(diningInfo)
|
||||
}
|
||||
}
|
||||
DispatchQueue.global().sync {
|
||||
diningLocations = newDiningLocations
|
||||
lastRefreshed = Date()
|
||||
isLoading = false
|
||||
}
|
||||
case .failure(let error): print(error)
|
||||
@ -117,10 +94,10 @@ struct ContentView: View {
|
||||
|
||||
// Allow for searching the list and hiding closed locations. Gets a list of locations that match the search and a list that match
|
||||
// the open only filter (.open and .closingSoon) and then returns the ones that match both lists.
|
||||
private var filteredLocations: [Location] {
|
||||
private var filteredLocations: [DiningLocation] {
|
||||
diningLocations.filter { location in
|
||||
let searchedLocations = searchText.isEmpty || location.name.localizedCaseInsensitiveContains(searchText)
|
||||
let openLocations = !openLocationsOnly || location.isOpen == .open || location.isOpen == .closingSoon
|
||||
let openLocations = !openLocationsOnly || location.open == .open || location.open == .closingSoon
|
||||
return searchedLocations && openLocations
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,17 @@ struct SafariView: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
struct DetailView: View {
|
||||
@State var location: Location
|
||||
@State var location: DiningLocation
|
||||
@State private var showingSafari: Bool = false
|
||||
|
||||
private let display: DateFormatter = {
|
||||
let display = DateFormatter()
|
||||
display.timeZone = TimeZone(identifier: "America/New_York")
|
||||
display.dateStyle = .none
|
||||
display.timeStyle = .short
|
||||
return display
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
@ -34,7 +42,7 @@ struct DetailView: View {
|
||||
.font(.title2)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(alignment: .top) {
|
||||
switch location.isOpen {
|
||||
switch location.open {
|
||||
case .open:
|
||||
Text("Open")
|
||||
.foregroundStyle(.green)
|
||||
@ -49,8 +57,13 @@ struct DetailView: View {
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
VStack {
|
||||
ForEach(location.todaysHours, id: \.self) { hours in
|
||||
Text(hours)
|
||||
if let times = location.diningTimes, !times.isEmpty {
|
||||
ForEach(times, id: \.self) { time in
|
||||
Text("\(display.string(from: time.openTime)) - \(display.string(from: time.closeTime))")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
Text("Not Open Today")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
@ -80,11 +93,12 @@ struct DetailView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DetailView(location: Location(
|
||||
DetailView(location: DiningLocation(
|
||||
id: 0,
|
||||
name: "Example",
|
||||
summary: "A Place",
|
||||
desc: "A long description of the place",
|
||||
mapsUrl: "https://example.com",
|
||||
todaysHours: ["Now - Later"],
|
||||
isOpen: .open))
|
||||
diningTimes: [DiningTimes(openTime: Date(), closeTime: Date())],
|
||||
open: .open))
|
||||
}
|
||||
|
@ -7,44 +7,13 @@
|
||||
|
||||
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 summary: String
|
||||
let description: String
|
||||
let mapsUrl: 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) {
|
||||
func getAllDiningInfo(completionHandler: @escaping (Result<DiningLocationsParser, Error>) -> Void) {
|
||||
// The endpoint requires that you specify a date, so get today's.
|
||||
let date_string = Date().formatted(.iso8601
|
||||
.year().month().day()
|
||||
@ -70,34 +39,12 @@ func getDiningLocation(completionHandler: @escaping (Result<DiningLocations, Err
|
||||
return
|
||||
}
|
||||
|
||||
let decoded: Result<DiningLocations, Error> = Result(catching: { try JSONDecoder().decode(DiningLocations.self, from: data) })
|
||||
let decoded: Result<DiningLocationsParser, Error> = Result(catching: { try JSONDecoder().decode(DiningLocationsParser.self, from: data) })
|
||||
completionHandler(decoded)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
enum openStatus {
|
||||
case open
|
||||
case closed
|
||||
case openingSoon
|
||||
case closingSoon
|
||||
}
|
||||
|
||||
struct DiningTimes: Equatable {
|
||||
let openTime: Date
|
||||
let closeTime: Date
|
||||
}
|
||||
|
||||
struct DiningInfo {
|
||||
let id: Int
|
||||
let name: String
|
||||
let summary: String
|
||||
let desc: String
|
||||
let mapsUrl: String
|
||||
let diningTimes: [DiningTimes]?
|
||||
let open: openStatus
|
||||
}
|
||||
|
||||
func getLocationInfo(location: DiningLocation) -> DiningInfo {
|
||||
func getLocationInfo(location: DiningLocationParser) -> DiningLocation {
|
||||
print("beginning parse for \(location.name)")
|
||||
|
||||
// The descriptions sometimes have HTML <br /> tags despite also having \n. Those need to be removed.
|
||||
@ -105,13 +52,13 @@ func getLocationInfo(location: DiningLocation) -> DiningInfo {
|
||||
|
||||
// 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(
|
||||
return DiningLocation(
|
||||
id: location.id,
|
||||
name: location.name,
|
||||
summary: location.summary,
|
||||
desc: desc,
|
||||
mapsUrl: location.mapsUrl,
|
||||
diningTimes: .none,
|
||||
diningTimes: nil,
|
||||
open: .closed)
|
||||
}
|
||||
|
||||
@ -124,13 +71,13 @@ func getLocationInfo(location: DiningLocation) -> DiningInfo {
|
||||
if let exceptions = event.exceptions, !exceptions.isEmpty {
|
||||
// Early return if the exception for the day specifies that the location is closed. Used for things like holidays.
|
||||
if !exceptions[0].open {
|
||||
return DiningInfo(
|
||||
return DiningLocation(
|
||||
id: location.id,
|
||||
name: location.name,
|
||||
summary: location.summary,
|
||||
desc: desc,
|
||||
mapsUrl: location.mapsUrl,
|
||||
diningTimes: .none,
|
||||
diningTimes: nil,
|
||||
open: .closed)
|
||||
}
|
||||
openStrings.append(exceptions[0].startTime)
|
||||
@ -167,26 +114,34 @@ func getLocationInfo(location: DiningLocation) -> DiningInfo {
|
||||
second: closeTimeComponents.second!,
|
||||
of: now)!)
|
||||
}
|
||||
var diningTimes: [DiningTimes] = []
|
||||
for i in 0..<openDates.count {
|
||||
diningTimes.append(DiningTimes(openTime: openDates[i], closeTime: closeDates[i]))
|
||||
}
|
||||
|
||||
// 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.
|
||||
for i in 0..<closeDates.count {
|
||||
if closeDates[i] <= openDates[i] {
|
||||
closeDates[i] = calendar.date(byAdding: .day, value: 1, to: closeDates[i])!
|
||||
for i in diningTimes.indices {
|
||||
if diningTimes[i].closeTime <= diningTimes[i].openTime {
|
||||
diningTimes[i].closeTime = calendar.date(byAdding: .day, value: 1, to: diningTimes[i].closeTime)!
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes the openings are not in order, for some reason. I'm observing this with Brick City, where for some reason the early opening
|
||||
// is event 1, and the later opening is event 0. This is silly so let's reverse it.
|
||||
diningTimes.sort { $0.openTime < $1.openTime }
|
||||
|
||||
// This can probably be done a little cleaner 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.
|
||||
var openStatus: openStatus = .closed
|
||||
for i in 0..<openDates.count {
|
||||
if now >= openDates[i] && now <= closeDates[i] {
|
||||
if closeDates[i] < calendar.date(byAdding: .minute, value: 30, to: now)! {
|
||||
var openStatus: OpenStatus = .closed
|
||||
for i in diningTimes.indices {
|
||||
if now >= diningTimes[i].openTime && now <= diningTimes[i].closeTime {
|
||||
if diningTimes[i].closeTime < calendar.date(byAdding: .minute, value: 30, to: now)! {
|
||||
openStatus = .closingSoon
|
||||
} else {
|
||||
openStatus = .open
|
||||
}
|
||||
} else if openDates[i] <= calendar.date(byAdding: .minute, value: 30, to: now)! && closeDates[i] > now {
|
||||
} else if diningTimes[i].openTime <= calendar.date(byAdding: .minute, value: 30, to: now)! && diningTimes[i].closeTime > now {
|
||||
openStatus = .openingSoon
|
||||
} else {
|
||||
openStatus = .closed
|
||||
@ -198,12 +153,7 @@ func getLocationInfo(location: DiningLocation) -> DiningInfo {
|
||||
}
|
||||
}
|
||||
|
||||
var diningTimes: [DiningTimes] = []
|
||||
for i in 0..<openDates.count {
|
||||
diningTimes.append(DiningTimes(openTime: openDates[i], closeTime: closeDates[i]))
|
||||
}
|
||||
|
||||
return DiningInfo(
|
||||
return DiningLocation(
|
||||
id: location.id,
|
||||
name: location.name,
|
||||
summary: location.summary,
|
||||
|
66
RIT Dining/Types.swift
Normal file
66
RIT Dining/Types.swift
Normal file
@ -0,0 +1,66 @@
|
||||
//
|
||||
// Types.swift
|
||||
// RIT Dining
|
||||
//
|
||||
// Created by Campbell on 9/2/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 DiningLocationParser: Decodable {
|
||||
// An individual "event", which is just an open period for the location.
|
||||
struct Events: Decodable {
|
||||
// Hour exceptions for the given event.
|
||||
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]?
|
||||
}
|
||||
// Other basic information to read from a location's JSON that we'll need later.
|
||||
let id: Int
|
||||
let name: String
|
||||
let summary: String
|
||||
let description: String
|
||||
let mapsUrl: String
|
||||
let events: [Events]
|
||||
}
|
||||
|
||||
// Struct that probably doesn't need to exist but this made parsing the list of location responses easy.
|
||||
struct DiningLocationsParser: Decodable {
|
||||
let locations: [DiningLocationParser]
|
||||
}
|
||||
|
||||
// Enum to represent the four possible states a given location can be in.
|
||||
enum OpenStatus {
|
||||
case open
|
||||
case closed
|
||||
case openingSoon
|
||||
case closingSoon
|
||||
}
|
||||
|
||||
// An individual open period for a location.
|
||||
struct DiningTimes: Equatable, Hashable {
|
||||
var openTime: Date
|
||||
var closeTime: Date
|
||||
}
|
||||
|
||||
// The basic information about a dining location needed to display it in the app after parsing is finished.
|
||||
struct DiningLocation: Identifiable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let summary: String
|
||||
let desc: String
|
||||
let mapsUrl: String
|
||||
let diningTimes: [DiningTimes]?
|
||||
let open: OpenStatus
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user