Updating a WidgetKit widget from a Siri intents extension - ios

I have an interesting quandary I've been grappling with for the past few days: I have an app that also has a widget extension and a Siri intents extension. There is a button in the app that updates a shared data file. Siri intents extension also updates that data file and the widget reflects those changes (to avoid conflicts, I'm using the NSFileCoordinator API).
When I switch to the app, tap the button, and return to the springboard, the widget contents are updated. When I invoke the intent through a shortcut, the app is updated. However, when I invoke the intent, the widget is not updated.
An interesting wrinkle is that widget updates if the app is run in the debug mode from Xcode, which leads me to suspect that this is some sort of a timing issue.
The app, the widget and the intent extension use shared code to read from and to write to the shared data file.
Here is some code:
A. Button in the app:
// …
Button("Update") {
storage.write(updatedData) { (error) in
guard error == nil else { return }
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
}
}
// …
B. Code in the intents extension
// …
storage.write(updatedData) { (error) in
guard error == nil else {
completion(MyIntentResponse(code: .failure, userActivity: nil))
return
}
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
completion(MyIntentResponse(code: .success, userActivity: nil))
}
// …
As you can see, I handle the situation when writing the data fails in the intents extesion. If that happens, Siri informs me about it – this is how I know that writing succeeds.
C. Widget entry/timeline code
// …
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (MyEntry) -> ()) {
storage.read { (data, error) in
completion(makeEntryFromData(data))
}
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
storage.read { (data, error) in
let timeline = Timeline(entries: [makeEntryFromData(data)], policy: .never)
completion(timeline)
}
}
// …
Here, although the error state seems not to be handled, in actuality, "data" becomes nil when there is an error, and nil values are handled in the view part of the widget – that way I know that data corruption is not the issue either.
Please guide me, as I'm sorely perplexed!
Cheers,
–Baglan
UPDATE: widget seem to update eventually most of the time. My best guess is, there is some some of a mechanism that throttles updates or schedules them for some time convenient for the OS and that results in unpredictable widget update delays.

Related

iOS 14 Widget background refresh not happening - how to get diagnostics / refresh budget counter information?

We are trying to update our home widget around 4 to 5 times an hour within a 10 hours period a day after our application reaches a given state (we can not predict when exactly this will happen).
When the application is in the foreground, everything works fine and as expected, is the application in the background though, the widget gets refresh maybe once an hour or not at all (background processing is enabled and working properly) . It feels totally random and currently we simply can't retrace or comprehend what is happening or why it is happening.
This is our TimelineProvider:
struct Provider: TimelineProvider {
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> Void) {
let preferences = UserDefaults.init(suiteName:widgetGroupId)
let title = preferences?.string(forKey: "title")
let entry = SimpleEntry(date: Date(), title: title ?? "")
completion(entry)
}
func placeholder(in context: Context) -> SimpleEntry {
...
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
getSnapshot(in: context) { (entry) in
let timeline = Timeline(entries: [entry], policy: .never)
completion(timeline)
}
}
}
Our application tries to update the home widget via WidgetCenter.shared.reloadAllTimelines().
I have now the following questions:
How can I understand why the refresh is not working and when it will be possible again? Is there something like a cooldown time?
How can I check the refresh budget counter? Can we obtain it somehow in the code?
How can I reset the budget counter for testing? Shall I recreate the widget? Restart the phone? Reinstall the app?
From my experience, calling reloadAllTimelines() from anywhere but the foreground app seems to be unreliable. Instead of setting your timeline expiration policy to .never, try using .after with a date 15 minutes or so in the future. That should ensure your widget is updated ~4x an hour with the latest data. Hopefully that’s close enough for your use case.

Facing issue in Siri Integration with custom intents

I’m trying to integrate Siri Shortcuts to my application. The concept which I’m trying is to get reward points of my card with secret pin confirmation. Please find what I have done for this below.
Enabled Siri in capabilities and added Siri Intent definition file.
Added new custom intent named say Rewards.
Defined the title. Subtitle and params(accType, pin) with confirmation enabled. Pin will be sent separately to user.
Then defined the intent response with param ‘rewardPoints’ and defined the response messages.
Added Siri intent extensions.
Added custom intent to info.plist files within project and intent extension.
Verified and added new handler for the custom intent and define the resolve, handle and confirm methods as below. For now, I’m returning random no for reward points.
//
// RewardsIntentHandler.swift
// SiriIntentExt
//
import UIKit
import Intents
class RewardsIntentHandler: NSObject, RewardsIntentHandling {
func resolveAccType(for intent:RewardsIntent, with completion: #escaping ([INStringResolutionResult]) -> Void) {
guard let accType = intent.accType else {
completion([INStringResolutionResult.needsValue()])
return
}
completion([INStringResolutionResult.success(with: accType)])
}
func resolvePin(for intent:RewardsIntent, with completion: #escaping ([INIntegerResolutionResult]) -> Void) {
guard let verifyPin = intent.pin else {
completion([INIntegerResolutionResult.needsValue()])
return
}
completion([INIntegerResolutionResult.confirmationRequired(with: verifyPin as? Int)])
}
func confirm(intent: RewardsIntent, completion: #escaping (RewardsIntentResponse) -> Void) {
completion(RewardsIntentResponse.init(code: RewardsIntentResponseCode.ready, userActivity: nil))
}
func handle(intent: RewardsIntent, completion: #escaping (RewardsIntentResponse) -> Void) {
guard intent.accType != nil else {
completion(RewardsIntentResponse.init(code: RewardsIntentResponseCode.continueInApp, userActivity: nil))
return
}
guard intent.pin != nil else {
completion(RewardsIntentResponse.init(code: RewardsIntentResponseCode.continueInApp, userActivity: nil))
return
}
let response = RewardsIntentResponse.success(rewardPoints: NSNumber(value: 3453))
completion(response)
}
}
Modified the IntentHandler to return rewards handler for rewards intent
//
// IntentHandler.swift
// SiriIntentExt
//
import Intents
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
if intent is RewardsIntent {
return RewardsIntentHandler()
}
return self
}
}
Donated the intent on view load as below.
//
// ViewController.swift
// Shortcuts
//
import UIKit
import Intents
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
siriAuthorisarion()
donateRewardIntent()
}
func siriAuthorisarion() {
INPreferences.requestSiriAuthorization { (status) in
print("Siri Authorization Status - ", status)
}
}
func donateRewardIntent() {
let rewardsIntent = RewardsIntent()
rewardsIntent.suggestedInvocationPhrase = "Reward Points"
rewardsIntent.accType = "test account"
let interaction = INInteraction(intent: rewardsIntent, response: nil)
interaction.donate { error in
if let error = error {
print("Donating intent failed with error \(error)")
}
DispatchQueue.main.async {
let alert = UIAlertController.init(title: ((error != nil) ? "Error" : "Success"), message: ((error != nil) ? "Oops!!! Error occured on donating intent." : "Intent donated succussfully!!!"), preferredStyle: .alert)
alert.addAction(UIAlertAction.init(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
}
}
I'm facing problem from the above code base. Siri is not requesting for pin and not able to get the exact reward points for the account.
Have following questions.
Can we add the intents programmatically to Siri instead adding from shortcuts app or settings. So that user can directly use the functionality once installing the application.
Once intent is added using Shortcuts app, I’m trying the ask Siri for reward points. Its immediately requesting for my app shortcuts defined. Once we say 'yes' to request, I need to be asked for pin. But Siri replies with some problem with my app. What to be done for asking for next param value.
In the handler file, I have added the resolve methods for each parameters. I feel, resolve methods are not getting called to validate the values. Do we need to handle anything to make resolve methods work?
How can I debug the handler implementation using breakpoint within resolve/handle/confirm methods.
Thanks in advance.
Find my analysis for the above questions.
Can we add the intents programmatically to Siri instead adding from shortcuts app or settings. So that user can directly use the functionality once installing the application.
By default, intents are provided for specific domains such as messaging, payments, photos, workout, etc. No need to explicitly add intents through shortcuts for theses specific domains. Apart from these domains if we are creating custom intent, we are in need to donate and add the intents to Siri using shortcut/settings application.
Once intent is added using Shortcuts app, I’m trying the ask Siri for reward points. Its immediately requesting for my app shortcuts defined. Once we say 'yes' to request, I need to be asked for pin. But Siri replies with some problem with my app. What to be done for asking for next param value.
From iOS13, Apple has added Siri parameters and Siri suggestion for custom intents to request the missing parameters. Till iOS12, we don't have parameters option for custom intents.
In the handler file, I have added the resolve methods for each parameters. I feel, resolve methods are not getting called to validate the values. Do we need to handle anything to make resolve methods work?
In iOS12, we cannot add resolve methods for parameters in custom intents. Resolve methods handled only for specific domains provided within Intents extensions as mentioned in question 1. From iOS13, we can have resolve methods for custom intents based on the parameters.
How can I debug the handler implementation using breakpoint within resolve/handle/confirm methods.
We can add breakpoints and debug intent handler methods.
Thanks.

siri replay's like "Sorry, there was a problem with the app"

I was just trying to integrating custom siri intent into my app.i have done code for intent handler and i can able to create shortcut but when i run my shortcut. i'm unable to open my app. see this image https://i.stack.imgur.com/m2fby.png
A possible solution is to make sure that your Intent does not take too much memory (~<20Mb) and respond in reasonable time (<5s)
If you missed handling intent then also you might come across this error. Check file IntentHandler.swift
class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling {
override func handler(for intent: INIntent) -> Any {
return MyIntetHandler() //Here, If you are returning self that means you have not handled it.
}
......
Sample intent handler code, the intent name I have created is My, protocol MyIntentHandling is autogenerated you just need to confirm it.
class MyIntetHandler: NSObject, MyIntentHandling
{
func handle(intent: MyIntent, completion: #escaping (MyIntentResponse) -> Void) {
completion(MyIntentResponse(code: .success, userActivity: nil))
}
}

How do I avoid callback hell on swift functions that I didn't write?

I don't want this to be misconstrued as a duplicate. I want to deal with callback hell arising from API calls from Firestore (database by Firebase, a Google platform). I can't modify their function declarations, so I assume I will have to wrap their functions with some of my code.
For example, in the code below, the function eventCreatedSuccessfully() can only be called after the asynchronous function completes. eventCreatedSuccessfully() also contains a function call to firebase that has a closure, which another function relies on and etc...Though this isn't causing me problems right now, it probably will as my App grows larger and larger. I researched online and found solutions like Futures and Streams from third-party frameworks, but I didn't know how to integrate them into code I have no control over (the API calls).
batch.commit { (error) in
self.dismiss(animated: true) {
if error == nil {
self.eventCreatedSuccessfully()
print("Event created successfully")
} else {
print(error!.localizedDescription)
}
}
}
Wrap the calls in promises. Any of the popular libraries will do the trick. The one that comes to mind is PromiseKit, available (at the time of this writing) at https://github.com/mxcl/PromiseKit.
Here is code I wrote for a work project (it's open source) which wraps a function that takes a completion, and returns a Promise which will signal with the result when the completion is called. It's using an internal Promise implementation, but the process can be adapted to other implementations.
public func promise<Return>(_ task: (#escaping (Return?, Error?) -> ()) -> ()) -> Promise<Return> {
let p = Promise<Return>()
task { (value: Return?, error: Error?) -> Void in
if let error = error {
p.signal(error)
}
if let value = value {
p.signal(value)
}
}
return p
}
The completion is expected to be called with a result of some kind, or an error. Adapt as required for your use-case.
An example usage follows.
public typealias BalanceCompletion = (Balance?, Error?) -> Void
func balance(completion: #escaping BalanceCompletion) {
guard deleted == false else {
completion(nil, KinError.accountDeleted)
return
}
Stellar.balance(account: stellarAccount.publicKey!, asset: asset, node: node)
.then { balance -> Void in
completion(balance, nil)
}
.error { error in
completion(nil, KinError.balanceQueryFailed(error))
}
}
func balance() -> Promise<Balance> {
return promise(balance)
}
I researched online and found solutions like Futures and Streams (...)
In most cases futures and streams is all about PromiseKit and RxSwift frameworks.
If you have only big amount of closures try to use PMK. It's very simple and easy to use. PMK also has nice documentation section on github.
RxSwift is more advanced level, because it requires you to write code fully to paradigm of itself - starts from server/firebase request and ends with ui. Also, there is a good note at PMK github about difference of these two.
Also, should be note, that google also has nice library called promises. According to their benchmarks google's library is leader in almost all nominations.

How to init INStartWorkoutIntent properly in Swift 3?

I know that there's a built-in template for it.
I go to the File menu and choose New > Target
Select iOS > Application extensions from the left-hand pane.
Now choose Intents extension.
That will create two new groups: YourExtension and YourExtensionUI. If you open the YourExtension group you'll see IntentHandler.swift, which contains some sample code for handling workouts.
Here's a much simpler example to get you started:
class IntentHandler: INExtension, INSendMessageIntentHandling {
override func handler(for intent: INIntent) -> AnyObject {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
return self
}
func handle(sendMessage intent: INSendMessageIntent, completion: (INSendMessageIntentResponse) -> Void) {
print("Send message: " + (intent.content ?? "No message"))
let response = INSendMessageIntentResponse(code: .success, userActivity: nil)
completion(response)
}
}
I did that, it's OK.
Now my issue is about using INStart​Workout​Intent instead of INSendMessageIntent, how am I supposed to? Is there a built-in template for this intents too?
Finally, I solved the question by myself.
When you want to use INStartWorkoutIntent properly, you have just to remove all the built-in template content.
You have also to replace INSendMessageIntentHandling by INStartWorkoutIntent Handling.
public func handle(startWorkout intent: INStartWorkoutIntent, completion: #escaping (INStartWorkoutIntentResponse) -> Swift.Void) {
let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartWorkoutIntent.self))
let response = INStartWorkoutIntentResponse(code: .continueInApp, userActivity: userActivity)
completion(response)
}
DO NOT FORGET:
To your newly created Intents target, fully expand the NSExtension dictionary to study its contents. The dictionary describes in more detail which intents your extension supports and if you want to allow the user to invoke an intent while the device is locked.
Insert the most relevant intents at the top if you want to support more than one. Siri uses this order to figure out which one the user wants to use in case of ambiguity.
We now need to define which intents we want to support.
For example, I want to build an extension that supports the payment intent. Modify the Info.plist file to match the following picture.
Here we specify that we want to handle the INSendPaymentIntent and that we require the device to be unlocked. We don't want strangers to send payments when the device is lost or stolen!
Last thing just to set in target your Intent at the running and it's done.

Resources