Our application uses the SpeziScheduler module to send several survey notifications per day at randomized intervals.
Occasionally, when a user clicks on a notification, the survey that has just become eligible does not appear in the "Survey" tab in the application (i.e., the view has not been updated). After switching tabs or closing and re-opening the application, the new survey appears.
Similarly, after a user completes a survey, the survey is occasionally not marked as completed in the survey view, and the user can repeat the survey again.
Reproduction
The behavior is non-deterministic and unfortunately cannot always reliably be reproduced.
Expected behavior
The survey view should always be up-to-date -- showing all surveys that are eligible and marking completed surveys as complete -- whenever the view is loaded.
Additional context
Here is the code we use for scheduling notifications:
import Foundation
import SpeziFHIR
import SpeziScheduler
/// A `Scheduler` using the `FHIR` standard as well as the ``HPDSTaskContext`` to schedule and manage tasks and events in the
/// Spezi Template Applciation.
typealias HPDSScheduler = Scheduler<FHIR, HPDSTaskContext>
extension HPDSScheduler {
/// Creates a default instance of the ``HPDSScheduler`` by scheduling the tasks listed below.
convenience init() {
self.init(
tasks: [
Task(
title: String(localized: "MORNING_SURVEY_TITLE"),
description: String(localized: "MORNING_SURVEY_DESCRIPTION"),
schedule: Schedule(
start: Calendar.current.startOfDay(for: Date()),
repetition: .randomBetween(start: .init(hour: 8, minute: 00), end: .init(hour: 11, minute: 00)), // Every Day between 8-11 AM
end: .numberOfEvents(356)
),
notifications: true,
context: HPDSTaskContext.questionnaire(Bundle.main.questionnaire(withName: "morning-en-US"))
),
Task(
title: String(localized: "MID_DAY_SURVEY_TITLE"),
description: String(localized: "MID_DAY_SURVEY_DESCRIPTION"),
schedule: Schedule(
start: Calendar.current.startOfDay(for: Date()),
repetition: .randomBetween(start: .init(hour: 11, minute: 00), end: .init(hour: 14, minute: 00)), // Every Day at 11-14 PM
end: .numberOfEvents(356)
),
notifications: true,
context: HPDSTaskContext.questionnaire(Bundle.main.questionnaire(withName: "mid-day-en-US"))
),
Task(
title: String(localized: "AFTERNOON_SURVEY_TITLE"),
description: String(localized: "AFTERNOON_SURVEY_DESCRIPTION"),
schedule: Schedule(
start: Calendar.current.startOfDay(for: Date()),
repetition: .randomBetween(start: .init(hour: 14, minute: 00), end: .init(hour: 17, minute: 00)), // Every Day at 14-17 PM
end: .numberOfEvents(356)
),
notifications: true,
context: HPDSTaskContext.questionnaire(Bundle.main.questionnaire(withName: "afternoon-en-US"))
),
Task(
title: String(localized: "END_OF_THE_DAY_SURVEY_TITLE"),
description: String(localized: "END_OF_THE_DAY_SURVEY_DESCRIPTION"),
schedule: Schedule(
start: Calendar.current.startOfDay(for: Date()),
repetition: .matching(.init(hour: 17, minute: 00)), // Every Day at 17:00 PM
end: .numberOfEvents(356)
),
notifications: true,
context: HPDSTaskContext.questionnaire(Bundle.main.questionnaire(withName: "end-of-day-en-US"))
)
]
)
}
}
and here is our ScheduleView.swift, only with minor aesthetic changes from the SpeziTemplateApplication in the body
import SpeziQuestionnaire
import SpeziScheduler
import SwiftUI
struct ScheduleView: View {
@EnvironmentObject var scheduler: HPDSScheduler
@State var eventContextsByDate: [Date: [EventContext]] = [:]
@State var presentedContext: EventContext?
var startOfDays: [Date] {
Array(eventContextsByDate.keys)
}
var body: some View {
NavigationStack {
List(startOfDays, id: \.timeIntervalSinceNow) { startOfDay in
ForEach(eventContextsByDate[startOfDay] ?? [], id: \.event) { eventContext in
Section {
EventContextView(eventContext: eventContext)
.onTapGesture {
if !eventContext.event.complete {
presentedContext = eventContext
}
}
}
}
}
.onReceive(scheduler.objectWillChange) { _ in
calculateEventContextsByDate()
}
.task {
calculateEventContextsByDate()
}
.sheet(item: $presentedContext) { presentedContext in
destination(withContext: presentedContext)
}
.navigationTitle("SCHEDULE_LIST_TITLE")
}
}
private func destination(withContext eventContext: EventContext) -> some View {
@ViewBuilder
var destination: some View {
switch eventContext.task.context {
case let .questionnaire(questionnaire):
QuestionnaireView(questionnaire: questionnaire) { _ in
_Concurrency.Task {
await eventContext.event.complete(true)
}
}
}
}
return destination
}
private func format(startOfDay: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .none
return dateFormatter.string(from: startOfDay)
}
private func calculateEventContextsByDate() {
// Adjust the following parameters to your needs of the application:
// Display start date.
// We just display all tasks that have been scheduled for today
let startDate = Calendar.current.startOfDay(for: .now)
// Display end cutoff. Can be either a number of events or a date.
// In this case, we display a maximum of 100 events up until the current point in time.
let eventCutoffDate = Date.now.addingTimeInterval(1)
let eventCutoff = Schedule.End.numberOfEventsOrEndDate(100, eventCutoffDate)
let eventContexts = scheduler.tasks.flatMap { task in
task
.events(
from: startDate,
to: eventCutoff
)
.map { event in
EventContext(event: event, task: task)
}
}
.sorted()
let newEventContextsByDate = Dictionary(grouping: eventContexts) { eventContext in
Calendar.current.startOfDay(for: eventContext.event.scheduledAt)
}
if newEventContextsByDate != eventContextsByDate {
eventContextsByDate = newEventContextsByDate
}
}
}
Code of Conduct
[X] I agree to follow this project's Code of Conduct and Contributing Guidelines
Description
Our application uses the SpeziScheduler module to send several survey notifications per day at randomized intervals.
Occasionally, when a user clicks on a notification, the survey that has just become eligible does not appear in the "Survey" tab in the application (i.e., the view has not been updated). After switching tabs or closing and re-opening the application, the new survey appears.
Similarly, after a user completes a survey, the survey is occasionally not marked as completed in the survey view, and the user can repeat the survey again.
Reproduction
The behavior is non-deterministic and unfortunately cannot always reliably be reproduced.
Expected behavior
The survey view should always be up-to-date -- showing all surveys that are eligible and marking completed surveys as complete -- whenever the view is loaded.
Additional context
Here is the code we use for scheduling notifications:
and here is our ScheduleView.swift, only with minor aesthetic changes from the SpeziTemplateApplication in the body
Code of Conduct