Why don't interactive buttons in iOS 17 widget call AppIntent perform() when the app running?
Asked Answered
M

3

8

Inside a widget, there is a button,

Button(intent: AnAppIntent()) {
  // Button's label.
}
// It seems this modifier does not add any value.
.invalidatableContent()

connected to an AppIntent.

struct AnAppIntent: AppIntent {
  static var title: LocalizedStringResource = "An AppIntent"

  init() {
    // AppIntent required init.
  }

  func perform() async throws -> some IntentResult {

    // Never called when the app is running.

    return .result()
  }
}

The button calls AppIntent's perform() when tapped, and it consequently updates the widget UI (with or without the modifier .invalidatableContent()) only when the app is closed completely.

If the app is alive in the background, perform() is not called, and the widget UI never updates.

The user must explicitly dismiss the app to make the widget work as expected.

The problem may be in the timeline used.

struct SimpleEntry: TimelineEntry {
  let date: Date
}

struct Provider: TimelineProvider {
  func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date())
  }

  func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    completion(SimpleEntry(date: Date()))
  }

  func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var entries: [SimpleEntry] = []

    // Generate a timeline 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, to: currentDate)!
      entries.append(SimpleEntry(date: entryDate))
    }

    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
  }
}

However, if the problem were the timeline, the widget would not work with the app dismissed.

Malicious answered 27/6, 2023 at 15:20 Comment(3)
developer.apple.com/forums/thread/732771Malicious
You found any solution? I'm literally having the same problemMechling
I'm facing a similar issue but on my side the "perform" method is executed (I can see it in the logs) but the UI of the widget is not refreshed, the timeline is not called to retrieve new timeline entries. If the app is completely closed, the widget is working fine and is refreshed on each tap.Tessellation
G
5

The file including the Intent needs to have a dual target membership: It needs to be part of both the widget extension target as well as the app target.

So make sure to tick those checkboxes.

Grasmere answered 11/7, 2023 at 20:22 Comment(2)
I have dual target but I keep having this issue, to make sure I understood, the dual target should be set to the file where the intent is defined?Negrete
Have you found a solution to this problem?Spotted
M
0

I found that my widget, which had dual target membership (it was in both my app & my widget target) still had this issue: it was looking in the wrong location for data.

I was using UserDefaults to store data about the current widget state, but since the widget code runs in different processes when the app is running (with different state & "well-known" locations, assumedly), my calls to UserDefaults were reading a different file when the app was open vs closed.

If you add the AppIntent to both your app and widget extension targets, the intent will run in your app process if your app is running (even if suspended in the background) and in the widget process when your app is not running. If you want it to always run in your app's process, you can implement openAppWhenRun which will cause your app to come to foreground, or you implement a different intent protocol that runs in the app's process in the background: AudioPlaybackIntent, LiveActivityIntent, or ForegroundContinuableIntent.

(via answer linked above)


My fix

I mitigated this by storing my data in a custom app group. This answer offers different ways to use the AppGroup, depending on your data.

In my case, I simply migrated from UserDefaults to using my SQLite DB I was already storing in the shared app group.

Misfire answered 7/1 at 4:12 Comment(0)
G
0

I've lost a few hours on this before figuring out that perform() won't be called if you miss to set a property in the init.

So for everyone out there with the same issue - make sure you're giving a value to all of the properties in the init:

@Parameter(title: "Hero ID")
var heroID: String
@Parameter(title: "Chanel")
var channel: Int

init() {
    
}

init(heroID: String, channel: Int) {
    self.heroID = heroID // if this is missing the perform() won't be called
    self.channel = channel // if this is missing the perform() won't be called
}

Tested on iOS 18.

Gobbledygook answered 10/9 at 5:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.