How can I keep the existing entry on WidgetKit refresh? - ios

I'm using HealthKit data in my widget. If the phone is locked, it's not possible to get HealthKit data, only if the phone is unlocked. However, my widget timeline's will try to update even if the phone is locked.
Is it possible to return an empty completion somehow, so it will keep the current widget data untouched?
This is my code:
struct Provider: IntentTimelineProvider {
private let healthKitService = HealthKitService()
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
healthKitService.getHeartRate() { data, error in
//Create an empty entry
var entry = SimpleEntry(date: currentDate, configuration: ConfigurationIntent(), data: nil)
//If no errors, set data
if(error == nil) {
entry.data = data
} else {
print(error) //this runs when a locked phone does the widget update
}
//Return response
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
completion(timeline)
}
}
}
What I can do is to store the entry data in UserDefaults and load that up in the error route? I'm not sure if thats a good solution though.

The main issue is that you don't have a state in the getTimeline function. This is a similar problem as in How to refresh multiple timers in widget iOS14? - you need some way to store information outside getTimeline.
As you've already mentioned, a possible solution is storing the last entry in the UserDefatuls.
However, you can also try creating your own EntryCache:
class EntryCache {
var previousEntry: SimpleEntry?
}
struct SimpleEntry: TimelineEntry {
let date: Date
var previousDate: Date?
}
struct IntentProvider: IntentTimelineProvider {
private let entryCache = EntryCache()
// ...
// in this example I'm testing if the `previousDate` is loaded correctly from the cache
func getTimeline(for configuration: TestIntentIntent, in context: Context, completion: #escaping (Timeline<SimpleEntry>) -> Void) {
let currentDate = Date()
let entry = SimpleEntry(date: currentDate, previousDate: entryCache.previousEntry?.date)
let refreshDate = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)!
let refreshEntry = SimpleEntry(date: refreshDate, previousDate: entryCache.previousEntry?.date)
let timeline = Timeline(entries: [entry, refreshEntry], policy: .atEnd)
// store the `entry` in the `entryCache`
entryCache.previousEntry = entry
completion(timeline)
}
}
Note
I didn't find any information as to when the TimelineProvider may be re-created. In my tests the Widget was using the same Provider for every refresh but it's safer to assume that the Provider might be re-initialised at one some point in the future. Then, theoretically, for one refresh cycle the previousEntry will be nil.

Related

Widget ios 14 not updating daily

I have a simple widget that needs to be updated daily, however many of the users have complained that their widget doesn't update daily.
What am doing wrong here?
func getTimeline(in context: Context, completion: #escaping (Timeline<TodayInfoEntry>) -> Void) {
let now = Date() + 2
widgtBrain.date = now
let nextUpdateDate = Calendar.autoupdatingCurrent.date(byAdding: .day, value: 1, to: Calendar.autoupdatingCurrent.startOfDay(for: now))!
let todayInfo = widgtBrain.widgetInfoDicGenerator(forActualWidget: true, forPlaceholder: false, forSnapshot: false)
let entry = TodayInfoEntry(
date: now,
info: todayInfo
)
let timeline = Timeline(
entries:[entry],
policy: .after(nextUpdateDate)
)
completion(timeline)
}
the widgetBrain generates a dictionary with new data based on the given date "now"

WidgetKit won't share data between iOS and widget

to make it short, my iOS app download some data from a server and put it into an array.
I wanna share the array count with the widget using the AppGroups.
I save the array count number to UserDefaults like this:
if let userDefaults = UserDefaults(suiteName: "group.com.etc") {
// I save just a simple Int
userDefaults.set(loaded.count, forKey: userDefaultsKey)
}
Then on the widget side I have this class to retrieve the data:
class MyDataProvider {
static func getCountFromUserDefaults()-> Int {
if let userDefaults = UserDefaults(suiteName: "group.com.etc") {
let myFlag = userDefaults.integer(forKey: userDefaultsKey)
print("myFlag is \(myFlag)")
return myFlag
}
print("my flag is 0")
return 0
}
}
Last, my getTimeLine func is this
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .second, value: hourOffset * 30, to: currentDate)!
let entry = SimpleEntry(date: entryDate, myString: "\(MyDataProvider.getCountFromUserDefaults())")
print("my entry is \(entry)")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
The issue is that the number is always 0. On iOS side I'm sure the number is saved correctly but the widget get always 0 even when the iOS app is opened.
Do i mistake something?
Solved: I forgot to add the AppGroup to the Widget Extension too.

How to change view on midnight in WidgetKit, SwiftUI?

I have a code:
struct ContentView: View {
let entry: LessonWidgetEntry
private static let url: URL = URL(string: "widgetUrl")!
var body: some View {
VStack {
switch entry.state {
case .none:
ProgramNotStartedView()
case .currentLesson(let lesson):
LessonView(lesson: lesson, imageName: entry.program?.imageName)
case .lessonCompleted(let lesson):
LessonCompletedView(lesson: lesson)
case .programCompleted:
ProgramCompletedView()
}
}.widgetURL(ContentView.url)
}
}
At midnight LessonCompletedView should change to LessonView, but I am not sure how to do that.
Any ideas on how to change views on midnight from the widget?
Assuming you have an Entry (in your app you have entry.state... but for this example I used a simplified version):
struct SimpleEntry: TimelineEntry {
let date: Date
let lesson: Lesson
}
Setup your TimelineProvider to refresh timeline after the next midnight:
struct SimpleProvider: TimelineProvider {
...
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> Void) {
let currentDate = Date()
let midnight = Calendar.current.startOfDay(for: currentDate)
let nextMidnight = Calendar.current.date(byAdding: .day, value: 1, to: midnight)!
let entries = [
SimpleEntry(date: currentDate, lesson: Lesson()) // pass the lesson here
]
let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
completion(timeline)
}
}
In the TimelineProvider you may pass any lesson you want (depending on the day or the previous lesson - it's up to you). You may also pass a variable to an Entry indicating whether the lesson is completed.
By setting the .after(nextMidnight) policy you indicate when do you want your Timeline (and therefore you Widget View) to be reloaded.

Widget not getting updated even when UserDefaults are synchronized

I am using XCode 12 beta 2 (iOS 14 Sim) to pass data from my app to the widget using AppContainer.
I am using the below code to save data (here String) to app container.
let userDefaults = UserDefaults(suiteName: "group.abc.WidgetDemo")
userDefaults?.setValue(status, forKey: "widget")
userDefaults?.synchronize()
And in the Widget.swift file
struct Provider: TimelineProvider {
#AppStorage("widget", store: UserDefaults(suiteName: "group.abc.WidgetDemo"))
var status: String = String()
public func snapshot(with context: Context, completion: #escaping (MyEntry) -> ()) {
let entry = MyEntry(status: status, date: Date())
completion(entry)
}
public func timeline(with context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let entryDate = Calendar.current.date(byAdding: .second, value: 10, to: Date())!
let entry = MyEntry(status: status, date: entryDate)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
Please note: Timeline entry is 10 seconds post current date.
Even after giving a 10 seconds delay, I am unable to see the updated information in the widget.
Apparently, after reading the documentation, I happen to make it work by using the below
WidgetCenter.shared.reloadTimelines(ofKind: "WidgetDemo")
But if sometimes, the above doesn't work I try to reload all the timelines.
WidgetCenter.shared.reloadAllTimelines()
Please note: the reload Timelines code is written in the source file from where we are transmitting the data.

Healthkit not returning latest data on iphone. Using healthkit between iPhone and Apple watch

I am creating an app that provides workout information. On apple watch i have three labels that actively shows HEARTRATE ,DISTANCE TRAVELLED and CALORIES BURNED. Things work fine with apple watch, however the area where things go bad is when i try to show this data on iphone in realtime.
APPLE WATCH PART :-
I started workout on apple watch and saved it using folling code
func startWorkoutSession() {
// Start a workout session with the configuration
if let workoutConfiguration = configuration {
do {
workoutSession = try HKWorkoutSession(configuration: workoutConfiguration)
workoutSession?.delegate = self
workoutStartDate = Date()
healthStore.start(workoutSession!)
} catch {
// ...
}
}
}
Then i save that workout on apple watch using following code :-
func saveWorkout() {
// Create and save a workout sample
let configuration = workoutSession!.workoutConfiguration
let isIndoor = (configuration.locationType == .indoor) as NSNumber
print("locationType: \(configuration)")
let workout = HKWorkout(activityType: configuration.activityType,
start: workoutStartDate ?? Date(),
end: workoutEndDate ?? Date(),
workoutEvents: workoutEvents,
totalEnergyBurned: totalEnergyBurned,
totalDistance: totalDistance,
metadata: [HKMetadataKeyIndoorWorkout:isIndoor]);
healthStore.save(workout) { success, _ in
if success {
self.addSamples(toWorkout: workout)
}
}
// Pass the workout to Summary Interface Controller
// WKInterfaceController.reloadRootControllers(withNames: ["StopPauseInterfaceController"], contexts: [workout])
if #available(watchOSApplicationExtension 4.0, *){
// Create the route, save it, and associate it with the provided workout.
routeBuilder?.finishRoute(with: workout, metadata: metadata) { (newRoute, error) in
guard newRoute != nil else {
// Handle the error here...
return
}
}
}
else {
// fallback to earlier versions
}
}
Then i added samples to those workouts using following code :-
func addSamples(toWorkout workout: HKWorkout) {
// Create energy and distance samples
let totalEnergyBurnedSample = HKQuantitySample(type: HKQuantityType.activeEnergyBurned(),
quantity: totalEnergyBurned,
start: workoutStartDate!,
end: workoutEndDate ?? Date())
let totalDistanceSample = HKQuantitySample(type: HKQuantityType.distanceWalkingRunning(),
quantity: totalDistance,
start: workoutStartDate!,
end: workoutEndDate ?? Date())
// Add samples to workout
healthStore.add([totalEnergyBurnedSample, totalDistanceSample], to: workout) { (success: Bool, error: Error?) in
if success {
// Samples have been added
}
}
}
IPHONE PART :-
This is where things get worse. I try to access the healthkit store using HKSampleObjectQuery and It just returns static values. P.S - I am fetching healthkit store every one second for latest data using NSTimer.
The code which is called every second is :-
func extractDataFromHealthStore(identifier : HKQuantityTypeIdentifier,completion: #escaping (String) -> ()) {
let heightType = HKSampleType.quantityType(forIdentifier:identifier)!
let distantPastDate = Date.distantPast
let endDate = Date()
let predicate = HKQuery.predicateForSamples(withStart: distantPastDate, end: endDate, options: .strictStartDate)
// Get the single most recent Value
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let query = HKSampleQuery(sampleType: heightType, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor]) { (query, results, error) in
if let result = results?.first as? HKQuantitySample{
print("Height => \(result.quantity)")
completion("\(result.quantity)")
}else{
print("OOPS didnt get height \nResults => \(String(describing: results)), error => \(error)")
completion("")
}
}
self.healthStore.execute(query)
}
Please guide Me if my whole approach is wrong, or if it is right then what is the mistake at my end.

Resources