Query HealthKit in Widget on Apple Watch - apple-watch

I want to query steps from HealthKit every 15mins to update my Complication via WidgetKit.
In the Timeline Provider getTimeline() I execute the query, create one timeline entry with the steps and set timeline refresh policy to .after(now + 15min).
Currently I am stuck because the refresh is never triggered. The steps are queried and shown after initially setting the complication on the Watchface but never refresh.
`
func getTimeline(in context: Context, completion: #escaping (Timeline<HealthKitWidgetEntry>) -> Void) {
let currentDate = Date()
let refreshMinuteGranuity = 15
let refreshDate = Calendar.current.date(
byAdding: .minute,
value: refreshMinuteGranuity,
to: currentDate
)!
healthData.getTodaysSteps { steps in
let entry = HealthKitWidgetEntry(
date: currentDate, steps: steps
)
let timeline = Timeline(
entries: [entry], policy: .after(refreshDate)
)
print("Next refresh: \(refreshDate)")
completion(timeline)
}
}
`
Any input on how to solve this?

I had the same problem. I was able to work around it by starting an observer like this: enableBackgroundDelivery Health-kit iOS15 not working correctly
In the ChangeHandler you can cause the widget to update itself:
WidgetCenter.shared.reloadAllTimelines()
Be sure you set the Entitlement for "HealthKit Observer Query Background Delivery"

Related

iOS14 Widget update every midnight

I'm making a widget app that shows today's date and calendar.
I want the widget refresh every midnight, so I tried several methods.
But eventually I failed to figure out how to do it.
First try : give entries that update every midnight. But it doesn't update the widget every midnight exactly.
struct Provider: TimelineProvider {
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> Void) {
var entries: [SomeEntry] = []
let currentDate = Date()
let currentEntry = SomeEntry(date: currentDate), content: content)
entries.append(currentEntry)
for offset in 1 ..< 5 {
let day = Calendar.autoupdatingCurrent.date(byAdding: .day, value: offset, to: currentDate)!
let midnight = Calendar.autoupdatingCurrent.startOfDay(for: day)
let entry = SomeEntry(date: midnight, content: content)
entries.append(entry)
}
completion(entries, .atEnd)
}
}
Second try : Dynamic Dates
I tried to use dynamic dates which was described here (https://developer.apple.com/documentation/widgetkit/displaying-dynamic-dates)
But it's not customizable, so I think I can't use it when making calendar widget.
Third try : Local notification
I tried to use local notification to reload widget every midnight.
But I found in iOS, I can't use silent local notification.
Fourth try : Background Tasks
I tried background tasks, but it won't refresh widget if the app is terminated.
I know that other popular widget app's widget updates exact every midnight. Even if I manually change device time, they work.
I think there is a way that can alert widget extension when date changes.
Any idea how to do it??

If I go to another view controller and return to the same one using a segue, does the snapshotListener reread all the documents?

This is a little snip it from my code.
db.collection("tasks").document(Auth.auth().currentUser?.uid ?? "").collection("currentUser").whereField("Date", isEqualTo: date).addSnapshotListener{ (querySnapshot, err) in
self.task = []
if querySnapshot!.documents.isEmpty{
self.firstView.alpha = 1
self.labelText.alpha = 1
}
else{
self.firstView.alpha = 0
self.labelText.alpha = 0
if let snapshotDocuments = querySnapshot?.documents{
for doc in snapshotDocuments{
let data = doc.data()
print("xx")
if let descS = data["Task Description"] as? String, let dateS = data["Date"] as? String, let titleS = data["Task Title"] as? String, let type1 = data["Type"] as? String{
let newTask = Tasks(title: titleS, desc: descS, date: dateS, type: type1, docId: doc.documentID)
self.task.append(newTask)
DispatchQueue.main.async {
self.taskTableView.reloadData()
let indexPath = IndexPath(row: self.task.count - 1, section: 0)
self.taskTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
}
}
The problem is that when I go to add another task I perform a segue to another view controller. From there I add a document but I need to perform another segue to go back because I am using a hamburger menu.
I did try using getDocuments(source: cache), which did reduce writes when the user did not add a task. But when they did add a task it reloads all the documents, adding tons of reads. The goal of using a snapshotListner is to reduce reads, however, I'm not sure if it will reread data when I perform a segue to the screen again. Thank-You!
The snapshot listener doesn't care at all about anything going on the UI, such as segues. The snapshot listener only fires when you first add it and when there are updates in the remote data. You can finetune the snapshot listener to omit or include cached or presumed data (using the snapshot's metadata property). But this is the nature of realtime data, it may get updated a lot as the user does things. The only workaround is coming up with the most efficient data architecture possible (on the server side) to reduce your read cost.
As an aside, your code is very dangerously written. You should first check if there is a valid userId before attaching a listener to a userId that even your code suggests could be an empty string. You should never force unwrap snapshots like you are doing because it will crash the entire app when there is even a slight network error.

Notification Center and changing timezones

Swift 4 & >iOS 10.0
I want to schedule a local notification at a certain date and at a given time (let's say 3PM). I want the notifications to always be fired at 3PM, whatever the timezone I am in (automatic rescheduling of notifications according to timezones).
Previously, you could tweak UILocalNotifications' time zone to achieve exactly this, like perfectly explained in this SO post. However, in >iOS 10.0, UILocalNotifications is deprecated.
Here is my code:
func scheduleNotification(title: String, message: String, atDate: Date){
let center = UNUserNotificationCenter.current()
// Create content
let content = UNMutableNotificationContent()
content.title = title
content.body = message
content.sound = UNNotificationSound.default()
// Create trigger
let calendar = Calendar(identifier: .gregorian)
let triggerDate = calendar.dateComponents([.year,.month,.day,.hour,.minute,.second,], from: atDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate, repeats: false)
// Create identifier
let identifier = "\(UUID())"
// Create request & add to center
let request = UNNotificationRequest(identifier: identifier,
content: content,
trigger: trigger)
center.add(request, withCompletionHandler: { (error) in
})
}
Question:
How do you make the notification triggers properly with changing timezones ?
So, I managed to make it work. The triggerDate has a timeZone variable which is automatically nil, exactly like UILocalNotification.
triggerDate.timeZone behaves exactly like UILocalNotification.timeZone (behaviour described in this post, the same as mentioned in the question).
One of the reason it did not seem to work on the simulator was because I was not restarting the simulator when changing the timezone. Restarting will make everything work as expected.
Nota bene: Maybe a restart is not mandatory but since it's not obvious how much time a running simulator will take to detect the new timezone, I think restarting it is the most efficient solution.

Add Reminder List programmatically

I'm building an application that interacts with the macOS Reminder App. I'm trying to create a new Reminder list into which I later can import reminders.
This is what I have so far:
func setCalendar(_ type: EKEntityType) {
let eventStore = EKEventStore()
let newCalendar = EKCalendar(for: type, eventStore: eventStore)
newCalendar.title="newcal"
print("Cal: " + newCalendar.title)
try? eventStore.saveCalendar(newCalendar, commit: true)
}
However, there is no reminder list being created.
The problem is that you have omitted to specify the new calendar's .source. You cannot create a calendar of any kind (event or reminder) without doing that.

Complication won't update

I have a very simple complication with a random number.
But my number won't update. Everytime I look on my watch it's the same. Only if I reinstall the complication (reinstalling apple watch app) I'm getting a new number.
I have set update to 1 second. Anyone an idea what could be wrong?
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
handler(CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: getTemplateForComplication(family: complication.family)!))
}
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
handler(NSDate(timeIntervalSinceNow: 1))
}
func getPlaceholderTemplateForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void) {
handler(getTemplateForComplication(family: complication.family))
}
func getTemplateForComplication(family family: CLKComplicationFamily) -> CLKComplicationTemplate? {
let bitcoinPrice = Double(arc4random_uniform(400))
switch family {
case .ModularSmall:
let template = CLKComplicationTemplateModularSmallSimpleText()
template.textProvider = CLKSimpleTextProvider(text: String(format: "%.2f", bitcoinPrice))
return template
case .ModularLarge:
let template = CLKComplicationTemplateModularLargeTallBody()
template.headerTextProvider = CLKSimpleTextProvider(text: "Bitcoin")
template.bodyTextProvider = CLKSimpleTextProvider(text: String(format: "%.2f €", bitcoinPrice))
return template
case .UtilitarianSmall:
let template = CLKComplicationTemplateUtilitarianSmallFlat()
template.textProvider = CLKSimpleTextProvider(text: String(format: "%.2f", bitcoinPrice))
return template
case .UtilitarianLarge:
let template = CLKComplicationTemplateUtilitarianLargeFlat()
template.textProvider = CLKSimpleTextProvider(text: String(format: " %.2f €", bitcoinPrice))
return template
default:
return nil
}
}
Your data source is missing the methods that are responsible for handling the scheduled update.
At the start of a scheduled update, ClockKit calls either the requestedUpdateDidBegin or requestedUpdateBudgetExhausted method, depending on the state of your complication’s time budget. You must implement one or both of those methods if you want to add data to your timeline. Your implementation of those methods should extend or reload the timeline of your complication as needed. When you do that, ClockKit requests the new timeline entries from your data source. If you do not extend or reload your timeline, ClockKit does not ask for any new timeline entries.
Here's how you can reload the timeline once your scheduled update occurs:
// MARK: - Responding to Scheduled Updates
func requestedUpdateDidBegin() {
let server=CLKComplicationServer.sharedInstance()
for complication in server.activeComplications {
server.reloadTimelineForComplication(complication)
}
}
You should also implement requestedUpdateBudgetExhausted().
Keep in mind that scheduled updates can only occur every 10 minutes; it's not possible to update your complication every second. Also consider that updating too often might exhaust your update budget:
Specify a date as far into the future as you can manage. Do not ask the system to update your complication within minutes. Instead, provide data to last for many hours or for an entire day. If your budget is exhausted, the next scheduled update does not occur until after your budget is replenished.