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.
Related
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.
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"
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.
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.
In QueryHK I run a HealthKit query for steps and the corresponding date. I return the values in a completion handler. In ViewController I declare the completion. My problem is that the method only returns the last value from the iteration sample in samples.
Question: I want all of the data returned in the completion, not just the last value.. How can I return all the data from the query in an NSArray ?
QueryHK.swift:
import UIKit
import HealthKit
class QueryHK: NSObject {
var steps = Double()
var date = NSDate()
func performHKQuery (completion: (steps: Double, date: NSDate) -> Void){
let healthKitManager = HealthKitManager.sharedInstance
let stepsSample = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
let stepsUnit = HKUnit.countUnit()
let sampleQuery = HKSampleQuery(
sampleType: stepsSample,
predicate: nil,
limit: 0,
sortDescriptors: nil)
{
(sampleQuery, samples, error) in
for sample in samples as [HKQuantitySample]
{
self.steps = sample.quantity.doubleValueForUnit(stepsUnit)
self.date = sample.startDate
}
// Calling the completion handler with the results here
completion(steps: self.steps, date: self.date)
}
healthKitManager.healthStore.executeQuery(sampleQuery)
}
}
ViewController:
import UIKit
class ViewController: UIViewController {
var dt = NSDate()
var stp = Double()
var query = QueryHK()
override func viewDidLoad() {
super.viewDidLoad()
printStepsAndDate()
}
func printStepsAndDate() {
query.performHKQuery() {
(steps, date) in
self.stp = steps
self.dt = date
println(self.stp)
println(self.dt)
}
}
}
Have your completion handler receive an array of steps/date pairs:
completion: ([(steps: Double, date: NSDate)]) -> Void
(you could pass two arrays, one of steps and one of dates, but I feel like it’s clearer to pass an array of pairs since the two are tied together)
Then build an array of pairs of step counts and dates:
if let samples = samples as? [HKQuantitySample] {
let steps = samples.map { (sample: HKQuantitySample)->(steps: Double, date: NSDate) in
let stepCount = sample.quantity.doubleValueForUnit(stepsUnit)
let date = sample.startDate
return (steps: stepCount, date: date)
}
completion(steps)
}
If you want the query class to retain this information as well, make the member variable an array of the same type and store the result in that as well as pass it to the callback.