I'm making an app where there are two widgets. Both widgets are supposed to get their timelines once per day, as all data for the day is known at midnight. I'm having an issue where when put on a development device, the widgets' timelines work correctly, but do not refresh the next day. I've tried both .atEnd
and .after(Date)
refresh policies and neither seems to work. Does anyone know why the widget isn't refreshing properly? I'm almost certain that I'm under the daily limit of refreshes (one timeline refresh and ~12 timeline entries per day). Thank you for any help! Dev device iPhone 15 Pro on 17.5.1 (21F90) with Xcode Version 15.4 (15F31d). Below is the timeline code for one of the widgets:
struct EndTimeProvider: TimelineProvider {
var currentHour: Int {
Calendar.current.component(.hour, from: .now)
}
var currentMinute : Int {
Calendar.current.component(.minute, from: .now)
}
var placeholderSixthPeriod: Period {
var endMinute: Int = 0
var endHour: Int = 0
if currentMinute > 60-14 {
endHour = currentHour + 1
endMinute = (currentMinute + 14) % 60
} else {
endHour = currentHour
endMinute = currentMinute + 14
}
return Period(name: "Period 6", start: "00:00", end: "\(endHour):\(endMinute)")
}
func placeholder(in context: Context) -> EndTimeEntry {
return EndTimeEntry(date: .now, displayPeriod: placeholderSixthPeriod, scheduleName: "Regular Day")
}
func getSnapshot(in context: Context, completion: @escaping (EndTimeEntry) -> ()) {
if context.isPreview {
completion(placeholder(in: context))
return
}
let entry = EndTimeEntry(date: .now, displayPeriod: placeholderSixthPeriod, scheduleName: "Regular Day")
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [EndTimeEntry] = []
let context = PersistenceController.shared.backgroundContext
let scheduleFetch = StoredScheduleOnDate.fetchRequest()
do {
let storedSchedules = try context.fetch(scheduleFetch)
let currentDate = Date()
if let todaySchedule = storedSchedules.first(where: {
Calendar.current.isDate($0.date!, equalTo: currentDate, toGranularity: .day)
})?.schedule?.asDayType() {
// Have an entry at midnight when schedules are needed
let morningStart = Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: .now)!
let morningPeriod = Period(name: "Good morning", start: "00:00", end: todaySchedule.periods.first!.start)
let morningEntry = EndTimeEntry(date: morningStart, displayPeriod: morningPeriod, scheduleName: todaySchedule.name)
entries.append(morningEntry)
// Passing periods should show the next full period's end time.
// This means that an entry's date should be the past period's end, or the start in the first period's case.
let firstPeriod = todaySchedule.periods.first!
let firstPeriodEntry = EndTimeEntry(date: firstPeriod.getStartAsDate(), displayPeriod: firstPeriod, scheduleName: todaySchedule.name)
entries.append(firstPeriodEntry)
for index in 1..<todaySchedule.periods.count {
let entry = EndTimeEntry(date: todaySchedule.periods[index-1].getEndAsDate(), displayPeriod: todaySchedule.periods[index], scheduleName: todaySchedule.name)
entries.append(entry)
}
// Have an entry at the end of the day to have the start time of the next day shown
if let tomorrowSchedule = storedSchedules.first(where: {
Calendar.current.isDate($0.date!, equalTo: Calendar.current.date(byAdding: .day, value: 1, to: currentDate)!, toGranularity: .day)
})?.schedule?.asDayType() {
// At EOD, show tomorrow's start
let endOfDay: Date = todaySchedule.periods.last!.getEndAsDate()
let overnightPeriod: Period = Period(name: "Good night", start: todaySchedule.periods.last!.end, end: "00:00")
let overnightEntry = EndTimeEntry(date: endOfDay, displayPeriod: overnightPeriod, scheduleName: tomorrowSchedule.name, overrideDisplayDate: tomorrowSchedule.periods.first!.getStartAsDate())
entries.append(overnightEntry)
}
}
} catch {
fatalError("Could not fetch from Core Data for widget timeline. \(error)")
}
let tomorrowMorning = Calendar.current.date(bySettingHour: 0, minute: 1, second: 0, of: Calendar.current.date(byAdding: .day, value: 1, to: .now)!)!
let timeline = Timeline(entries: entries, policy: .after(tomorrowMorning))
completion(timeline)
}
}
struct EndTimeEntry: TimelineEntry {
let date: Date
let displayPeriod: Period
let scheduleName: String
let overrideDisplayDate: Date?
init(date: Date, displayPeriod: Period, scheduleName: String, overrideDisplayDate: Date) {
self.date = date
self.displayPeriod = displayPeriod
self.scheduleName = scheduleName
self.overrideDisplayDate = overrideDisplayDate
}
init(date: Date, displayPeriod: Period, scheduleName: String) {
self.date = date
self.displayPeriod = displayPeriod
self.scheduleName = scheduleName
self.overrideDisplayDate = nil
}
}
...
For those wondering – the issue was not with the timeline code but rather more with the persistent stores I was using for Core Data. There seems to be an issue when background tasks try to access properties without inverses. When I was adding back inverses to migrate to CloudKit, the issue seemed to be resolved so as a general rule of thumb make sure that all your core data relationships have inverses or random things will break I guess.