diff --git a/SwiftLeeds/Data/Model/Schedule.swift b/SwiftLeeds/Data/Model/Schedule.swift index 6bfd074..c6d88ba 100644 --- a/SwiftLeeds/Data/Model/Schedule.swift +++ b/SwiftLeeds/Data/Model/Schedule.swift @@ -7,16 +7,27 @@ import Foundation -struct Schedule: Decodable { +struct Schedule: Codable { let data: Data - struct Data: Decodable { + + struct Data: Codable { let event: Event let events: [Event] + let days: [Day] + } + + struct Day: Codable, Identifiable { + let date: Date + let name: String let slots: [Slot] + + var id: String { + "\(name)-\(date.timeIntervalSince1970.description)" + } } - struct Event: Decodable, Identifiable { + struct Event: Codable, Identifiable { let id: UUID let name: String let location: String diff --git a/SwiftLeeds/Network/Requests.swift b/SwiftLeeds/Network/Requests.swift index 881fb41..312716d 100644 --- a/SwiftLeeds/Network/Requests.swift +++ b/SwiftLeeds/Network/Requests.swift @@ -9,14 +9,37 @@ import Foundation enum Requests { private static let host = "swiftleeds.co.uk" - private static let apiPath = "/api/v1" + private static let apiVersion1 = "/api/v1" + private static let apiVersion2 = "/api/v2" - static let schedule = Request(host: host, path: "\(apiPath)/schedule", eTagKey: "etag-schedule") - static let local = Request(host: host, path: "\(apiPath)/local", eTagKey: "etag-local") - static let sponsors = Request(host: host, path: "\(apiPath)/sponsors", eTagKey: "etag-sponsors") + static let schedule = Request( + host: host, + path: "\(apiVersion2)/schedule", + eTagKey: "etag-schedule" + ) + + static let local = Request( + host: host, + path: "\(apiVersion1)/local", + eTagKey: "etag-local" + ) + + static let sponsors = Request( + host: host, + path: "\(apiVersion1)/sponsors", + eTagKey: "etag-sponsors" + ) static func schedule(for eventID: UUID) -> Request { - Request(host: host, path: "\(apiPath)/schedule", method: .get([.init(name: "event", value: eventID.uuidString)]), eTagKey: "etag-schedule-\(eventID.uuidString)") + Request( + host: host, + path: "\(apiVersion2)/schedule", + method: .get([.init( + name: "event", + value: eventID.uuidString) + ]), + eTagKey: "etag-schedule-\(eventID.uuidString)" + ) } static var defaultDateDecodingStratergy: JSONDecoder.DateDecodingStrategy = { @@ -24,4 +47,30 @@ enum Requests { dateFormatter.dateFormat = "dd-MM-yyyy" return .formatted(dateFormatter) }() + + // Custom strategy for v2 schedule endpoint (handles both ISO8601 and dd-MM-yyyy) + static var scheduleDateDecodingStrategy: JSONDecoder.DateDecodingStrategy = { + return .custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + + // Try ISO8601 first (for slot dates) + if let date = ISO8601DateFormatter().date(from: dateString) { + return date + } + + // Fallback to dd-MM-yyyy format (for event dates) + let formatter = DateFormatter() + formatter.dateFormat = "dd-MM-yyyy" + if let date = formatter.date(from: dateString) { + return date + } + + // If neither works, throw an error + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Date string does not match expected format. Expected ISO8601 or dd-MM-yyyy, got: \(dateString)" + ) + } + }() } diff --git a/SwiftLeeds/Views/My Conference/MyConferenceView.swift b/SwiftLeeds/Views/My Conference/MyConferenceView.swift index caa0828..f467ddf 100644 --- a/SwiftLeeds/Views/My Conference/MyConferenceView.swift +++ b/SwiftLeeds/Views/My Conference/MyConferenceView.swift @@ -26,7 +26,7 @@ struct MyConferenceView: View { .progressViewStyle(.circular) .scaleEffect(2) } - } else if viewModel.slots.isEmpty { + } else if viewModel.days.isEmpty { empty } else { schedule @@ -69,7 +69,6 @@ struct MyConferenceView: View { VStack(spacing: 0) { ViewThatFits { scheduleHeaders - ScrollView(.horizontal) { scheduleHeaders } @@ -77,8 +76,8 @@ struct MyConferenceView: View { TabView(selection: $currentIndex) { ForEach(Array(zip(viewModel.days.indices, viewModel.days)), - id: \.0) { index, key in - ScheduleView(slots: viewModel.slots[key] ?? [], showSlido: viewModel.showSlido) + id: \.0) { index, day in + ScheduleView(slots: day.slots, showSlido: viewModel.showSlido) .tag(index) } } @@ -89,19 +88,11 @@ struct MyConferenceView: View { @ViewBuilder private var scheduleHeaders: some View { - if viewModel.days.count == 3 { - // Temporary solution until new API is ready to support days correctly - HStack(spacing: 20) { - tabBarHeader(title: "Talkshow", index: 0) - tabBarHeader(title: "Day 1", index: 1) - tabBarHeader(title: "Day 2", index: 2) - } - .padding(.horizontal) - .padding(.top) - } else if viewModel.days.count > 1 { + if viewModel.days.count > 1 { HStack(spacing: 20) { - ForEach(Array(zip(viewModel.days.indices, viewModel.days)), id: \.0) { index, key in - tabBarHeader(title: "Day \(index + 1)", index: index) + ForEach(Array(zip(viewModel.days.indices, viewModel.days)), id: \.0) { index, day in + // Use the actual day names from the API + tabBarHeader(title: day.name, index: index) } } .padding(.horizontal) diff --git a/SwiftLeeds/Views/My Conference/MyConferenceViewModel.swift b/SwiftLeeds/Views/My Conference/MyConferenceViewModel.swift index 9bca1a2..21b88bb 100644 --- a/SwiftLeeds/Views/My Conference/MyConferenceViewModel.swift +++ b/SwiftLeeds/Views/My Conference/MyConferenceViewModel.swift @@ -13,23 +13,28 @@ class MyConferenceViewModel: ObservableObject { @Published private(set) var hasLoaded = false @Published private(set) var event: Schedule.Event? @Published private(set) var events: [Schedule.Event] = [] - @Published private(set) var days: [String] = [] - @Published private(set) var slots: [String: [Schedule.Slot]] = [:] + @Published private(set) var days: [Schedule.Day] = [] @Published private(set) var currentEvent: Schedule.Event? func loadSchedule() async throws { do { - let schedule = try await URLSession.awaitConnectivity.decode(Requests.schedule, dateDecodingStrategy: Requests.defaultDateDecodingStratergy) + let schedule = try await URLSession.awaitConnectivity.decode( + Requests.schedule, + dateDecodingStrategy: Requests.scheduleDateDecodingStrategy + ) await updateSchedule(schedule) do { - let data = try PropertyListEncoder().encode(slots) - UserDefaults(suiteName: "group.uk.co.swiftleeds")?.setValue(data, forKey: "Slots") + let data = try PropertyListEncoder().encode(schedule) + UserDefaults(suiteName: "group.uk.co.swiftleeds")?.setValue(data, forKey: "Schedule") } catch { throw(error) } } catch { - if let cachedResponse = try? await URLSession.shared.cached(Requests.schedule, dateDecodingStrategy: Requests.defaultDateDecodingStratergy) { + if let cachedResponse = try? await URLSession.shared.cached( + Requests.schedule, + dateDecodingStrategy: Requests.scheduleDateDecodingStrategy + ) { await updateSchedule(cachedResponse) } else { throw(error) @@ -47,15 +52,15 @@ class MyConferenceViewModel: ObservableObject { currentEvent = event } - let individualDates = Set(schedule.data.slots.compactMap { $0.date?.withoutTimeAtConferenceVenue }).sorted(by: (<)) - days = individualDates.map { Helper.shortDateFormatter.string(from: $0) } - - for date in individualDates { - let key = Helper.shortDateFormatter.string(from: date) - slots[key] = schedule.data.slots - .filter { Calendar.current.compare(date, to: $0.date ?? Date(), toGranularity: .day) == .orderedSame } - .sorted { $0.startTime < $1.startTime } - } + days = schedule.data.days + .sorted(by: { $0.date < $1.date }) + .map { day in + Schedule.Day( + date: day.date, + name: day.name, + slots: day.slots.sorted { $0.startTime < $1.startTime } + ) + } hasLoaded = true } @@ -63,7 +68,11 @@ class MyConferenceViewModel: ObservableObject { private func reloadSchedule() async throws { guard let currentEvent else { return } - let schedule = try await URLSession.awaitConnectivity.decode(Requests.schedule(for: currentEvent.id), dateDecodingStrategy: Requests.defaultDateDecodingStratergy, filename: "schedule-\(currentEvent.id.uuidString)") + let schedule = try await URLSession.awaitConnectivity.decode( + Requests.schedule(for: currentEvent.id), + dateDecodingStrategy: Requests.scheduleDateDecodingStrategy, + filename: "schedule-\(currentEvent.id.uuidString)" + ) await updateSchedule(schedule) } diff --git a/SwiftLeedsWidget/WidgetSetup/TimeineProvider.swift b/SwiftLeedsWidget/WidgetSetup/TimeineProvider.swift index 75775ac..7cba89c 100644 --- a/SwiftLeedsWidget/WidgetSetup/TimeineProvider.swift +++ b/SwiftLeedsWidget/WidgetSetup/TimeineProvider.swift @@ -21,12 +21,14 @@ struct Provider: TimelineProvider { func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { var entries: [SwiftLeedsWidgetEntry] = [] var slots: [Schedule.Slot] = [] - + do { - if let data = UserDefaults(suiteName: "group.uk.co.swiftleeds")?.data(forKey: "Slots") { - slots = try PropertyListDecoder().decode([Schedule.Slot].self, from: data) + if let data = UserDefaults(suiteName: "group.uk.co.swiftleeds")?.data(forKey: "Schedule") { + // Decode the full schedule and flatten days into slots + let schedule = try PropertyListDecoder().decode(Schedule.self, from: data) + slots = schedule.data.days.flatMap { $0.slots }.sorted { $0.startTime < $1.startTime } } - + for slot in slots { let date = buildDate(for: slot) if date > Date() { @@ -44,23 +46,20 @@ struct Provider: TimelineProvider { completion(timeline) } } - + private func buildDate(for slot: Schedule.Slot) -> Date { + guard let slotDate = slot.date else { return Date() } + let slotTime = slot.startTime let slotTimeComponents = slotTime.components(separatedBy: ":") - let slotHour = Int(slotTimeComponents.first ?? "0") - let slotMinute = Int(slotTimeComponents.last ?? "0") - - var dateComponents = DateComponents() - dateComponents.year = 2022 - dateComponents.month = 10 - dateComponents.day = 20 - dateComponents.timeZone = TimeZone.current + let slotHour = Int(slotTimeComponents.first ?? "0") ?? 0 + let slotMinute = Int(slotTimeComponents.last ?? "0") ?? 0 + + var dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: slotDate) dateComponents.hour = slotHour dateComponents.minute = slotMinute - let userCalendar = Calendar(identifier: .gregorian) - let dateTime = userCalendar.date(from: dateComponents) ?? Date() - - return dateTime + dateComponents.timeZone = TimeZone.current + + return Calendar.current.date(from: dateComponents) ?? Date() } }