iOS14 Widget update every midnight - swift

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??

Related

Query HealthKit in Widget on 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"

Reload collectionView when all data is ready

I'm fetching some data from an endpoint, every time I requested for 10 items. These items contains a URL that should be scraping, which slow down the process a bit.
Due to some bug probably, the received data from scraping is not the same number of the original data, which something i will deal with later. So I don't know when all data is ready.
Now, I want to make sure when I get all the data, then refresh the collection view. the only way that I can think of it, is using the timestamp of data and if the timestamp is not updated for about a sec, means, I have all the data that I need than I can refresh collection view. Not the best way of course
I added a timestamp variable
private lazy var fetchedTimestamp = Date().nanosecondsSince1970
then, whenever the data is recived I updated this timestamp with the the current time
Than, I use DispatchQueue.main.asyncAfter to check the timestamp on sec later and if the the timestamp one sec later is 1 sec after the last fetchedTimestampupdate, means the data hasn't been updated for about a sec, so I know it's ready to reload
KingfisherManager.shared.retrieveImage(with: imageURL) { result in
if let image = try? result.get().image {
scraper.setBaseImage(with: image)
DispatchQueue.main.async { [weak self] in
guard let this = self else { return }
this.feedsCollectionView.feeds.append(scraper)
this.fetchedTimestamp = Date().nanosecondsSince1970
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
let currentTime = Date().nanosecondsSince1970
if currentTime >= this.fetchedTimestamp + Int64(1e+9) {
this.feedsCollectionView.reload()
}
})
}
}
}
Is there any way to handle it better? Thank you so much

The behavior for system Date objects stored in Firestore is going to change AND YOUR APP MAY BREAK?

After running my app I get the following output in the terminal. The app does not crash. Where exactly should I put this code? And what exactly does it do?
2018-08-16 09:45:05.414410-0400 Yubi[2652:1608362] 4.13.0 - [Firebase/Firestore][I-FST000001] The behavior for system Date objects
stored in Firestore is going to change AND YOUR APP MAY BREAK. To hide
this warning and ensure your app does not break, you need to add the
following code to your app before calling any other Cloud Firestore
methods:
let db = Firestore.firestore() let settings = db.settings
settings.areTimestampsInSnapshotsEnabled = true db.settings = settings
With this change, timestamps stored in Cloud Firestore will be read
back as Firebase Timestamp objects instead of as system Date objects.
So you will also need to update code expecting a Date to instead
expect a Timestamp. For example:
// old: let date: Date = documentSnapshot.get("created_at") as! Date
// new: let timestamp: Timestamp = documentSnapshot.get("created_at")
as! Timestamp let date: Date = timestamp.dateValue()
Please audit all existing usages of Date when you enable the new
behavior. In a future release, the behavior will be changed to the new
behavior, so if you do not follow these steps, YOUR APP MAY BREAK.
You should add this in appDelegate, where you are adding the configuration.
FirebaseApp.configure()
let db = Firestore.firestore()
let settings = db.settings
settings.timestampsInSnapshotsEnabled = true
db.settings = settings
By setting areTimestampsInSnapshotsEnabled to true, it will save the timestamps instead of Date
So, when you read it will return the timestamps, so wherever in your code, there was Date being read, it should now read timestamp and convert it to Date objects as suggested.
Old code
// old: This is old code returning Date object
let date: Date = documentSnapshot.get("created_at") as! Date
Should be replace by new code
// new: This is new code returning Timestamp object
let timestamp: Timestamp = documentSnapshot.get("created_at") as! Timestamp
let date: Date = timestamp.dateValue()
Hope it helps you to understand the warning. You must make the changes to your existing code to meet the new guidelines, or later it will leads to crash due to type mismatch.

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.

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.