mirror of
https://github.com/NinjaCheetah/RIT-Dining.git
synced 2025-10-19 06:36:18 -04:00
Rewrote lots of bad "async" code
Code that should've been fully async that was not previously actually async, such as the networking code, is now actually async. This makes it much easier to read and means that it makes a lot more sense in the context of the program. This also means that all uses of DispatchQueue.main.sync{} and DispatchQueue.main.async{} are now gone. DetailView's fetching code benefitted the most from these changes, and is now just one iterative function instead of two cursed functions involving callbacks. As a result, you can now also pull down on the detail view to refresh the data, which is handy. Note that occupancy isn't updated when doing this currently due to a task issue I haven't figured out yet, but you can just go back out and in to update that anyway. ContentView will also now automatically refresh the data if the last refreshed date is no longer today, so if you let the app sit in the background and then focus it, dates should automatically update to being for today.
This commit is contained in:
parent
059209c9e5
commit
f01c041885
@ -257,7 +257,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -291,7 +291,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 12;
|
CURRENT_PROJECT_VERSION = 13;
|
||||||
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
DEVELOPMENT_TEAM = 5GF7GKNTK4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
@ -7,6 +7,71 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
// This view handles the actual location list, because having it inside ContentView was too complex (both visually and for the
|
||||||
|
// type checker too, apparently).
|
||||||
|
struct LocationList: View {
|
||||||
|
@State var filteredLocations: [DiningLocation]
|
||||||
|
@Environment(Favorites.self) var favorites
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ForEach($filteredLocations) { $location in
|
||||||
|
NavigationLink(destination: DetailView(location: $location)) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Text(location.name)
|
||||||
|
if favorites.contains(location) {
|
||||||
|
Image(systemName: "star.fill")
|
||||||
|
.foregroundStyle(.yellow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch location.open {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if let times = location.diningTimes, !times.isEmpty {
|
||||||
|
ForEach(times, id: \.self) { time in
|
||||||
|
Text("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Not Open Today")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.swipeActions {
|
||||||
|
Button(action: {
|
||||||
|
withAnimation {
|
||||||
|
if favorites.contains(location) {
|
||||||
|
favorites.remove(location)
|
||||||
|
} else {
|
||||||
|
favorites.add(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}) {
|
||||||
|
if favorites.contains(location) {
|
||||||
|
Label("Unfavorite", systemImage: "star")
|
||||||
|
} else {
|
||||||
|
Label("Favorite", systemImage: "star")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tint(favorites.contains(location) ? .yellow : nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
// Save sort/filter options in AppStorage so that they actually get saved.
|
// Save sort/filter options in AppStorage so that they actually get saved.
|
||||||
@AppStorage("openLocationsOnly") var openLocationsOnly: Bool = false
|
@AppStorage("openLocationsOnly") var openLocationsOnly: Bool = false
|
||||||
@ -29,20 +94,18 @@ struct ContentView: View {
|
|||||||
// Asynchronously fetch the data for all of the locations and parse their data to display it.
|
// Asynchronously fetch the data for all of the locations and parse their data to display it.
|
||||||
private func getDiningData() async {
|
private func getDiningData() async {
|
||||||
var newDiningLocations: [DiningLocation] = []
|
var newDiningLocations: [DiningLocation] = []
|
||||||
getAllDiningInfo(date: nil) { result in
|
switch await getAllDiningInfo(date: nil) {
|
||||||
switch result {
|
case .success(let locations):
|
||||||
case .success(let locations):
|
for i in 0..<locations.locations.count {
|
||||||
for i in 0..<locations.locations.count {
|
let diningInfo = parseLocationInfo(location: locations.locations[i], forDate: nil)
|
||||||
let diningInfo = parseLocationInfo(location: locations.locations[i], forDate: nil)
|
newDiningLocations.append(diningInfo)
|
||||||
newDiningLocations.append(diningInfo)
|
|
||||||
}
|
|
||||||
diningLocations = newDiningLocations
|
|
||||||
lastRefreshed = Date()
|
|
||||||
isLoading = false
|
|
||||||
case .failure(let error):
|
|
||||||
print(error)
|
|
||||||
loadFailed = true
|
|
||||||
}
|
}
|
||||||
|
diningLocations = newDiningLocations
|
||||||
|
lastRefreshed = Date()
|
||||||
|
isLoading = false
|
||||||
|
case .failure(let error):
|
||||||
|
print(error)
|
||||||
|
loadFailed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +117,13 @@ struct ContentView: View {
|
|||||||
for location in diningLocations.indices {
|
for location in diningLocations.indices {
|
||||||
diningLocations[location].updateOpenStatus()
|
diningLocations[location].updateOpenStatus()
|
||||||
}
|
}
|
||||||
|
// If the last refreshed date isn't today, that means we probably passed midnight and need to refresh the data.
|
||||||
|
// So do that.
|
||||||
|
if !Calendar.current.isDateInToday(lastRefreshed ?? Date()) {
|
||||||
|
Task {
|
||||||
|
await getDiningData()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,61 +212,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
Section(content: {
|
Section(content: {
|
||||||
ForEach(filteredLocations, id: \.self) { location in
|
LocationList(filteredLocations: filteredLocations)
|
||||||
NavigationLink(destination: DetailView(location: location)) {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
HStack {
|
|
||||||
Text(location.name)
|
|
||||||
if favorites.contains(location) {
|
|
||||||
Image(systemName: "star.fill")
|
|
||||||
.foregroundStyle(.yellow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch location.open {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if let times = location.diningTimes, !times.isEmpty {
|
|
||||||
ForEach(times, id: \.self) { time in
|
|
||||||
Text("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("Not Open Today")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.swipeActions {
|
|
||||||
Button(action: {
|
|
||||||
withAnimation {
|
|
||||||
if favorites.contains(location) {
|
|
||||||
favorites.remove(location)
|
|
||||||
} else {
|
|
||||||
favorites.add(location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}) {
|
|
||||||
if favorites.contains(location) {
|
|
||||||
Label("Unfavorite", systemImage: "star")
|
|
||||||
} else {
|
|
||||||
Label("Favorite", systemImage: "star")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tint(favorites.contains(location) ? .yellow : nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, footer: {
|
}, footer: {
|
||||||
if let lastRefreshed {
|
if let lastRefreshed {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
|
@ -11,117 +11,88 @@ enum InvalidHTTPError: Error {
|
|||||||
case invalid
|
case invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
// This API requesting code came from another project of mine and was used to fetch the GitHub API for update checking. I just copied it
|
// This code has now been mostly rewritten to be pretty and async instead of being horrifying callback based code in a context where
|
||||||
// here, but it can probably be made simpler for this use case.
|
// callback based code made no sense. I love async!
|
||||||
|
|
||||||
// Get information for all dining locations.
|
// Get information for all dining locations.
|
||||||
func getAllDiningInfo(date: String?, completionHandler: @escaping (Result<DiningLocationsParser, Error>) -> Void) {
|
func getAllDiningInfo(date: String?) async -> Result<DiningLocationsParser, Error> {
|
||||||
// The endpoint requires that you specify a date, so get today's.
|
// The endpoint requires that you specify a date, so get today's.
|
||||||
let date_string: String = date ?? getAPIFriendlyDateString(date: Date())
|
let dateString: String = date ?? getAPIFriendlyDateString(date: Date())
|
||||||
let url_string = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-all?date=\(date_string)"
|
let urlString = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-all?date=\(dateString)"
|
||||||
|
|
||||||
guard let url = URL(string: url_string) else {
|
guard let url = URL(string: urlString) else {
|
||||||
print("Invalid URL")
|
return .failure(URLError(.badURL))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let request = URLRequest(url: url)
|
|
||||||
|
do {
|
||||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
let (data, response) = try await URLSession.shared.data(from: url)
|
||||||
if let error = error {
|
|
||||||
completionHandler(.failure(error))
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
return
|
(200...299).contains(httpResponse.statusCode) else {
|
||||||
|
return .failure(InvalidHTTPError.invalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let data = data else {
|
let decoded = try JSONDecoder().decode(DiningLocationsParser.self, from: data)
|
||||||
completionHandler(.failure(URLError(.badServerResponse)))
|
return .success(decoded)
|
||||||
return
|
} catch {
|
||||||
}
|
return .failure(error)
|
||||||
|
}
|
||||||
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
|
|
||||||
completionHandler(.failure(InvalidHTTPError.invalid))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let decoded: Result<DiningLocationsParser, Error> = Result(catching: { try JSONDecoder().decode(DiningLocationsParser.self, from: data) })
|
|
||||||
completionHandler(decoded)
|
|
||||||
}.resume()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get information for just one dining location based on its location ID.
|
// Get information for just one dining location based on its location ID.
|
||||||
func getSingleDiningInfo(date: String?, locationId: Int, completionHandler: @escaping (Result<DiningLocationParser, Error>) -> Void) {
|
func getSingleDiningInfo(date: String?, locId: Int) async -> Result<DiningLocationParser, Error> {
|
||||||
// The current date and the location ID are required to get information for just one location.
|
// The current date and the location ID are required to get information for just one location.
|
||||||
let date_string: String = date ?? getAPIFriendlyDateString(date: Date())
|
let dateString: String = date ?? getAPIFriendlyDateString(date: Date())
|
||||||
let url_string = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-single?date=\(date_string)&locId=\(locationId)"
|
let urlString = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-single?date=\(dateString)&locId=\(locId)"
|
||||||
print("making request to \(url_string)")
|
print("making request to \(urlString)")
|
||||||
|
|
||||||
guard let url = URL(string: url_string) else {
|
guard let url = URL(string: urlString) else {
|
||||||
print("Invalid URL")
|
return .failure(URLError(.badURL))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let request = URLRequest(url: url)
|
|
||||||
|
do {
|
||||||
URLSession.shared.dataTask(with: request) { data, response, error in
|
let (data, response) = try await URLSession.shared.data(from: url)
|
||||||
if let error = error {
|
|
||||||
completionHandler(.failure(error))
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
return
|
(200...299).contains(httpResponse.statusCode) else {
|
||||||
|
return .failure(InvalidHTTPError.invalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let data = data else {
|
let decoded = try JSONDecoder().decode(DiningLocationParser.self, from: data)
|
||||||
completionHandler(.failure(URLError(.badServerResponse)))
|
return .success(decoded)
|
||||||
return
|
} catch {
|
||||||
}
|
return .failure(error)
|
||||||
|
}
|
||||||
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
|
|
||||||
completionHandler(.failure(InvalidHTTPError.invalid))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let decoded: Result<DiningLocationParser, Error> = Result(catching: { try JSONDecoder().decode(DiningLocationParser.self, from: data) })
|
|
||||||
completionHandler(decoded)
|
|
||||||
}.resume()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the occupancy information for a location using its MDO ID, whatever that stands for. This ID is provided alongside the other main
|
// Get the occupancy information for a location using its MDO ID, whatever that stands for. This ID is provided alongside the other
|
||||||
// ID in the data returned by the TigerCenter API.
|
// main ID in the data returned by the TigerCenter API.
|
||||||
func getOccupancyPercentage(mdoId: Int, completionHandler: @escaping (Result<Double, Error>) -> Void) {
|
func getOccupancyPercentage(mdoId: Int) async -> Result<Double, Error> {
|
||||||
let urlString = "https://maps.rit.edu/proxySearch/densityMapDetail.php?mdo=\(mdoId)"
|
let urlString = "https://maps.rit.edu/proxySearch/densityMapDetail.php?mdo=\(mdoId)"
|
||||||
print("making request to \(urlString)")
|
print("making request to \(urlString)")
|
||||||
|
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
print("Invalid URL")
|
return .failure(URLError(.badURL))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let occRequest = URLRequest(url: url)
|
|
||||||
|
|
||||||
URLSession.shared.dataTask(with: occRequest) { data, response, error in
|
do {
|
||||||
if let error = error {
|
let (data, response) = try await URLSession.shared.data(from: url)
|
||||||
completionHandler(.failure(error))
|
|
||||||
return
|
guard let httpResponse = response as? HTTPURLResponse,
|
||||||
|
(200...299).contains(httpResponse.statusCode) else {
|
||||||
|
return .failure(InvalidHTTPError.invalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let data = data else {
|
let occupancy = try JSONDecoder().decode([DiningOccupancyParser].self, from: data)
|
||||||
completionHandler(.failure(URLError(.badServerResponse)))
|
if !occupancy.isEmpty {
|
||||||
return
|
print("current occupancy: \(occupancy[0].count)")
|
||||||
|
print("maximum occupancy: \(occupancy[0].max_occ)")
|
||||||
|
let occupancyPercentage = Double(occupancy[0].count) / Double(occupancy[0].max_occ) * 100
|
||||||
|
print("occupancy percentage: \(occupancyPercentage)%")
|
||||||
|
return .success(occupancyPercentage)
|
||||||
|
} else {
|
||||||
|
return .failure(DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Failed to decode JSON")))
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
|
return .failure(error)
|
||||||
completionHandler(.failure(InvalidHTTPError.invalid))
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let occupancy = try JSONDecoder().decode([DiningOccupancyParser].self, from: data)
|
|
||||||
if !occupancy.isEmpty {
|
|
||||||
print("current occupancy: \(occupancy[0].count)")
|
|
||||||
print("maximum occupancy: \(occupancy[0].max_occ)")
|
|
||||||
let occupancyPercentage = Double(occupancy[0].count) / Double(occupancy[0].max_occ) * 100
|
|
||||||
print("occupancy percentage: \(occupancyPercentage)%")
|
|
||||||
completionHandler(.success(occupancyPercentage))
|
|
||||||
} else {
|
|
||||||
completionHandler(.failure(DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Failed to decode JSON"))))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
completionHandler(.failure(DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Failed to decode JSON"))))
|
|
||||||
}
|
|
||||||
}.resume()
|
|
||||||
}
|
}
|
||||||
|
@ -9,17 +9,15 @@ import SwiftUI
|
|||||||
import SafariServices
|
import SafariServices
|
||||||
|
|
||||||
struct DetailView: View {
|
struct DetailView: View {
|
||||||
@State var location: DiningLocation
|
@Binding var location: DiningLocation
|
||||||
@Environment(Favorites.self) var favorites
|
@Environment(Favorites.self) var favorites
|
||||||
@State private var isLoading: Bool = true
|
@State private var isLoading: Bool = true
|
||||||
@State private var rotationDegrees: Double = 0
|
@State private var rotationDegrees: Double = 0
|
||||||
@State private var showingSafari: Bool = false
|
@State private var showingSafari: Bool = false
|
||||||
@State private var openString: String = ""
|
@State private var openString: String = ""
|
||||||
@State private var week: [Date] = []
|
|
||||||
@State private var weeklyHours: [[String]] = []
|
@State private var weeklyHours: [[String]] = []
|
||||||
@State private var occupancyLoading: Bool = true
|
@State private var occupancyLoading: Bool = true
|
||||||
@State private var occupancyPercentage: Double = 0.0
|
@State private var occupancyPercentage: Double = 0.0
|
||||||
@State private var focusedDate: Date = Date()
|
|
||||||
private let daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
private let daysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
||||||
|
|
||||||
private var animation: Animation {
|
private var animation: Animation {
|
||||||
@ -28,76 +26,63 @@ struct DetailView: View {
|
|||||||
.repeatForever(autoreverses: false)
|
.repeatForever(autoreverses: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestDone(result: Result<DiningLocationParser, Error>) -> Void {
|
// This function is now actaully async and iterative! Wow! It doesn't suck ass anymore!
|
||||||
switch result {
|
private func getWeeklyHours() async {
|
||||||
case .success(let location):
|
|
||||||
let diningInfo = parseLocationInfo(location: location, forDate: focusedDate)
|
|
||||||
if let times = diningInfo.diningTimes, !times.isEmpty {
|
|
||||||
var timeStrings: [String] = []
|
|
||||||
for time in times {
|
|
||||||
timeStrings.append("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
|
|
||||||
}
|
|
||||||
weeklyHours.append(timeStrings)
|
|
||||||
} else {
|
|
||||||
weeklyHours.append(["Closed"])
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
if week.count > 0 {
|
|
||||||
// Saving this to a state variable SUCKS, but I needed a quick fix and all of this request code is still pending a
|
|
||||||
// rewrite anyway to be properly async like the code in ContentView and VisitingChefs.
|
|
||||||
focusedDate = week.removeFirst()
|
|
||||||
DispatchQueue.global().async {
|
|
||||||
let dateString = focusedDate.formatted(.iso8601
|
|
||||||
.year().month().day()
|
|
||||||
.dateSeparator(.dash))
|
|
||||||
getSingleDiningInfo(date: dateString, locationId: location.id, completionHandler: requestDone)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isLoading = false
|
|
||||||
print(weeklyHours)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getWeeklyHours() {
|
|
||||||
let calendar = Calendar.current
|
let calendar = Calendar.current
|
||||||
let today = calendar.startOfDay(for: Date())
|
let today = calendar.startOfDay(for: Date())
|
||||||
let dayOfWeek = calendar.component(.weekday, from: today)
|
let dayOfWeek = calendar.component(.weekday, from: today)
|
||||||
week = calendar.range(of: .weekday, in: .weekOfYear, for: today)!
|
let week = calendar.range(of: .weekday, in: .weekOfYear, for: today)!
|
||||||
.compactMap { calendar.date(byAdding: .day, value: $0 - dayOfWeek, to: today) }
|
.compactMap { calendar.date(byAdding: .day, value: $0 - dayOfWeek, to: today) }
|
||||||
DispatchQueue.global().async {
|
var newWeeklyHours: [[String]] = []
|
||||||
let date_string = week.removeFirst().formatted(.iso8601
|
for day in week {
|
||||||
|
let date_string = day.formatted(.iso8601
|
||||||
.year().month().day()
|
.year().month().day()
|
||||||
.dateSeparator(.dash))
|
.dateSeparator(.dash))
|
||||||
getSingleDiningInfo(date: date_string, locationId: location.id, completionHandler: requestDone)
|
switch await getSingleDiningInfo(date: date_string, locId: location.id) {
|
||||||
|
case .success(let location):
|
||||||
|
let diningInfo = parseLocationInfo(location: location, forDate: day)
|
||||||
|
if let times = diningInfo.diningTimes, !times.isEmpty {
|
||||||
|
var timeStrings: [String] = []
|
||||||
|
for time in times {
|
||||||
|
timeStrings.append("\(dateDisplay.string(from: time.openTime)) - \(dateDisplay.string(from: time.closeTime))")
|
||||||
|
}
|
||||||
|
newWeeklyHours.append(timeStrings)
|
||||||
|
} else {
|
||||||
|
newWeeklyHours.append(["Closed"])
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
weeklyHours = newWeeklyHours
|
||||||
|
isLoading = false
|
||||||
|
print(weeklyHours)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getOccupancy() {
|
private func getOccupancy() async {
|
||||||
// Only fetch occupancy data if the location is actually open right now. Otherwise, just exit early and hide the spinner.
|
// Only fetch occupancy data if the location is actually open right now. Otherwise, just exit early and hide the spinner.
|
||||||
if location.open == .open || location.open == .closingSoon {
|
if location.open == .open || location.open == .closingSoon {
|
||||||
DispatchQueue.main.async {
|
occupancyLoading = true
|
||||||
getOccupancyPercentage(mdoId: location.mdoId) { result in
|
switch await getOccupancyPercentage(mdoId: location.mdoId) {
|
||||||
switch result {
|
case .success(let occupancy):
|
||||||
case .success(let occupancy):
|
occupancyPercentage = occupancy
|
||||||
DispatchQueue.main.sync {
|
occupancyLoading = false
|
||||||
occupancyPercentage = occupancy
|
case .failure(let error):
|
||||||
occupancyLoading = false
|
print(error)
|
||||||
}
|
occupancyLoading = false
|
||||||
case .failure(let error):
|
|
||||||
print(error)
|
|
||||||
DispatchQueue.main.sync {
|
|
||||||
occupancyLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
occupancyLoading = false
|
occupancyLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same label update timer from ContentView.
|
||||||
|
private func updateOpenStatuses() async {
|
||||||
|
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { _ in
|
||||||
|
location.updateOpenStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
VStack {
|
VStack {
|
||||||
@ -114,8 +99,8 @@ struct DetailView: View {
|
|||||||
Text("Loading...")
|
Text("Loading...")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.task {
|
||||||
getWeeklyHours()
|
await getWeeklyHours()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
} else {
|
} else {
|
||||||
@ -200,8 +185,8 @@ struct DetailView: View {
|
|||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
.frame(width: 18, height: 18)
|
.frame(width: 18, height: 18)
|
||||||
.opacity(occupancyLoading ? 1 : 0)
|
.opacity(occupancyLoading ? 1 : 0)
|
||||||
.onAppear {
|
.task {
|
||||||
getOccupancy()
|
await getOccupancy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundStyle(Color.accent.opacity(occupancyLoading ? 0.5 : 1.0))
|
.foregroundStyle(Color.accent.opacity(occupancyLoading ? 0.5 : 1.0))
|
||||||
@ -294,20 +279,24 @@ struct DetailView: View {
|
|||||||
.sheet(isPresented: $showingSafari) {
|
.sheet(isPresented: $showingSafari) {
|
||||||
SafariView(url: URL(string: location.mapsUrl)!)
|
SafariView(url: URL(string: location.mapsUrl)!)
|
||||||
}
|
}
|
||||||
|
.refreshable {
|
||||||
|
await getWeeklyHours()
|
||||||
|
await getOccupancy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
//#Preview {
|
||||||
DetailView(location: DiningLocation(
|
// DetailView(location: DiningLocation(
|
||||||
id: 0,
|
// id: 0,
|
||||||
mdoId: 0,
|
// mdoId: 0,
|
||||||
name: "Example",
|
// name: "Example",
|
||||||
summary: "A Place",
|
// summary: "A Place",
|
||||||
desc: "A long description of the place",
|
// desc: "A long description of the place",
|
||||||
mapsUrl: "https://example.com",
|
// mapsUrl: "https://example.com",
|
||||||
diningTimes: [DiningTimes(openTime: Date(), closeTime: Date())],
|
// diningTimes: [DiningTimes(openTime: Date(), closeTime: Date())],
|
||||||
open: .open,
|
// open: .open,
|
||||||
visitingChefs: nil,
|
// visitingChefs: nil,
|
||||||
dailySpecials: nil))
|
// dailySpecials: nil))
|
||||||
}
|
//}
|
||||||
|
@ -30,21 +30,20 @@ struct VisitingChefs: View {
|
|||||||
// information.
|
// information.
|
||||||
private func getDiningDataForDate(date: String) async {
|
private func getDiningDataForDate(date: String) async {
|
||||||
var newDiningLocations: [DiningLocation] = []
|
var newDiningLocations: [DiningLocation] = []
|
||||||
getAllDiningInfo(date: date) { result in
|
switch await getAllDiningInfo(date: date) {
|
||||||
switch result {
|
case .success(let locations):
|
||||||
case .success(let locations):
|
for i in 0..<locations.locations.count {
|
||||||
for i in 0..<locations.locations.count {
|
let diningInfo = parseLocationInfo(location: locations.locations[i], forDate: nil)
|
||||||
let diningInfo = parseLocationInfo(location: locations.locations[i], forDate: nil)
|
print(diningInfo.name)
|
||||||
print(diningInfo.name)
|
// Only save the locations that actually have visiting chefs to avoid extra iterations later.
|
||||||
// Only save the locations that actually have visiting chefs to avoid extra iterations later.
|
if let visitingChefs = diningInfo.visitingChefs, !visitingChefs.isEmpty {
|
||||||
if let visitingChefs = diningInfo.visitingChefs, !visitingChefs.isEmpty {
|
newDiningLocations.append(diningInfo)
|
||||||
newDiningLocations.append(diningInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
locationsWithChefs = newDiningLocations
|
|
||||||
isLoading = false
|
|
||||||
case .failure(let error): print(error)
|
|
||||||
}
|
}
|
||||||
|
locationsWithChefs = newDiningLocations
|
||||||
|
isLoading = false
|
||||||
|
case .failure(let error):
|
||||||
|
print(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user