Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions SwiftLeeds/Data/Model/Schedule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 54 additions & 5 deletions SwiftLeeds/Network/Requests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,68 @@ 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<Schedule>(host: host, path: "\(apiPath)/schedule", eTagKey: "etag-schedule")
static let local = Request<Local>(host: host, path: "\(apiPath)/local", eTagKey: "etag-local")
static let sponsors = Request<Sponsors>(host: host, path: "\(apiPath)/sponsors", eTagKey: "etag-sponsors")
static let schedule = Request<Schedule>(
host: host,
path: "\(apiVersion2)/schedule",
eTagKey: "etag-schedule"
)

static let local = Request<Local>(
host: host,
path: "\(apiVersion1)/local",
eTagKey: "etag-local"
)

static let sponsors = Request<Sponsors>(
host: host,
path: "\(apiVersion1)/sponsors",
eTagKey: "etag-sponsors"
)

static func schedule(for eventID: UUID) -> Request<Schedule> {
Request<Schedule>(host: host, path: "\(apiPath)/schedule", method: .get([.init(name: "event", value: eventID.uuidString)]), eTagKey: "etag-schedule-\(eventID.uuidString)")
Request<Schedule>(
host: host,
path: "\(apiVersion2)/schedule",
method: .get([.init(
name: "event",
value: eventID.uuidString)
]),
eTagKey: "etag-schedule-\(eventID.uuidString)"
)
}

static var defaultDateDecodingStratergy: JSONDecoder.DateDecodingStrategy = {
let dateFormatter = DateFormatter()
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)"
)
}
}()
}
23 changes: 7 additions & 16 deletions SwiftLeeds/Views/My Conference/MyConferenceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct MyConferenceView: View {
.progressViewStyle(.circular)
.scaleEffect(2)
}
} else if viewModel.slots.isEmpty {
} else if viewModel.days.isEmpty {
empty
} else {
schedule
Expand Down Expand Up @@ -69,16 +69,15 @@ struct MyConferenceView: View {
VStack(spacing: 0) {
ViewThatFits {
scheduleHeaders

ScrollView(.horizontal) {
scheduleHeaders
}
}

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)
}
}
Expand All @@ -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)
Expand Down
41 changes: 25 additions & 16 deletions SwiftLeeds/Views/My Conference/MyConferenceViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -47,23 +52,27 @@ 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
}

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

Expand Down
33 changes: 16 additions & 17 deletions SwiftLeedsWidget/WidgetSetup/TimeineProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ struct Provider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<SwiftLeedsWidgetEntry>) -> ()) {
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() {
Expand All @@ -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()
}
}