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:
Campbell 2025-09-02 15:03:43 -04:00
parent 9f1d5c2078
commit 30c025e113
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
4 changed files with 136 additions and 129 deletions

View File

@ -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
}
}

View File

@ -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))
}

View File

@ -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
View 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
}