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.
Related
I try to create reusable function. It select data only for current week. But I need use this function few times in my app, so I've decided to do it generic.
I have two structs
struct BodyWeightCalendarModel {
let weight: Float
let date: Date
}
struct RetrievedWorkoutsByExercise {
let exerciseTitle: String
let sets: Int
let maxWeight: Int
let maxReps: Int
let date: Date
let volume: Int
}
func loopForWeek<T> (data: [T], completionHandler: ([T]) -> Void) {
let arrayForPeriod2: [T] = []
for value in data where value.date >= (calendar.currentWeekBoundary()?.startOfWeek)! && value.date <= (calendar.currentWeekBoundary()?.endOfWeek)! {
arrayForPeriod.append(value)
}
completionHandler(arrayForPeriod2)
}
How to get access to data values? I can't get access through "value.data".
So I want to use this function for different struct (but all this structs needs to have field "date").
As mentioned in other answers using a protocol with date property makes most sense in your case. However theoretically you could also use keypaths to achieve that. You could make a function that takes any instances and gets dates from them in a similar way as below:
func printDates(data: [Any], keyPath:AnyKeyPath) {
for value in data {
if let date = value[keyPath: keyPath] as? Date {
print("date = \(date)")
}
}
}
and then pass for example your BodyWeightCalendarModel instances as below:
let one = BodyWeightCalendarModel(date: Date(), weight: 1)
let two = BodyWeightCalendarModel(date: Date(), weight: 2)
printDates(data: [one, two], keyPath: \BodyWeightCalendarModel.date)
But still a protocol makes more sense in your case.
The reason you can't access value.date is that your function knows nothing about T. Your function declares T but it doesn't constrain it. To the compiler, T can be anything.
You need to create a protocol that tells your function what to expect, and make your structs conform to it:
protocol Timed { // You might find a better name
var date: Date { get }
}
struct BodyWeightCalendarModel: Timed {
let date: Date
...
}
struct RetrievedWorkoutsByExercise: Timed {
let date: Date
...
}
Now you can constrain T to Timed and your function will know that value has a date property.
func loopForWeek<T: Timed> (data: [T], completionHandler: ([T]) -> Void) {
What you're trying to do is close. A Generic value doesn't necessarily have a value on it called date so this won't work. What you can do instead is create a protocol that has a variable of date on it. With that protocol you can do something neat with an extension.
protocol Dated {
var date: Date { get set }
}
extension Dated {
func loopForWeek() -> [Dated] {
let arrayForPeriod2: [Dated] = []
for value in date >= (calendar.currentWeekBoundary()?.startOfWeek)! && value.date <= (calendar.currentWeekBoundary()?.endOfWeek)! {
arrayForPeriod.append(value)
}
return arrayForPeriod2
}
}
Then we can make your two structs use that protocol like:
struct BodyWeightCalendarModel: Dated {
let date: Date
}
struct RetrievedWorkoutsByExercise: Dated {
let date: Date
}
And they can call the function like:
let workouts = RetrievedWorkoutsByExercise()
let result = workouts.loopForWeek()
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"
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.
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.