Widget is cleared if the intent identifier changes: Easy fix

I just experienced this problem, and thought it might help someone else out later.

User adds a widget to their home screen. The widget has dynamic options, so they edit the widget and select a value. In my case, these are events, so the user selects their "Paris Trip". The widget shows the Paris Trip data and all is well.

The user then edits the event in the main app and renames it to "Paris Vacation".

The widget on the home screen clears because the identifier that that widget was created for (i.e. the name of that event) no longer exists.

Here's my fix...

The default settings for a dynamic selection intent's Type is to have two properties: identifier and displayString. The settings for these two are greyed-out (because they shouldn't be changed).

But, you can add a new property into there, so I added a unique identifier for the event, "uniqueId".

Now, here's a gotcha. This doesn't show up anywhere until the supporting files for the intent are recreated in the background. The easiest way I found of forcing this was to quit and restart Xcode.

Here's my provideEventOptionsCollection() func in IntentHandler.swift:
Code Block swift
func provideEventOptionsCollection(for intent: DynamicEventSelectionIntent, with completion: @escaping (INObjectCollection<Event>?, Error?) -> Void) {
let events: [Event] = EventDetail.availableEvents.map { event in
let returnEvent = Event(
identifier: event.name,
display: event.name
)
returnEvent.uniqueId = event.uniqueId. /* <-- THIS IS THE UPDATE */
return returnEvent
}
let collection = INObjectCollection(items: events)
completion(collection, nil)
}

As you can see, you can access the uniqueId property of the event object and add it to the event you're returning.

In my IntentTimelineProvider the event func is now this:
Code Block swift
if let uniqueId = configuration.event?.uniqueId {
if let event = EventDetail.eventFromUniqueId(uniqueId: uniqueId) {
return event
}
}

and the eventFromUniqueId() func in EventDetail:
Code Block swift
static func eventFromUniqueId(uniqueId: String) -> EventDetail? {
return (availableEvents).first(where: { (event) -> Bool in
return event.uniqueId == uniqueId
})
}


Hope this helps someone out there.
Answered by darkpaw in 639267022
My widget was based on the EmojiRangerWidget sample code, which contained this:
Code Block swift
func character(for configuration: DynamicCharacterSelectionIntent) -> CharacterDetail {
if let name = configuration.hero?.identifier, let character = CharacterDetail.characterFromName(name: name) {
/* Save the last selected character to our App Group. */
CharacterDetail.setLastSelectedCharacter(heroName: name)
return character
}
return .panda
}

My event() function is just based on that.

You don't need it; it's just the way I get the event for my case. Since I get the event in two places it saves duplicating the code.

In getSnapshot() and getTimeline():
Code Block swift
let selectedEvent = event(for: configuration)

That passes the intent configuration to the event() function, and returns the first event where its uniqueId matches the one provided by the intent. So, the user still selects the event's name when they edit the widget (as that's way more user-friendly), but what we're actually basing the widget on is the uniqueId instead. Does that make sense?
Cool, many thanks, this might help me, too, but I have no idea where you put

Code Block
if let uniqueId = configuration.event?.uniqueId {
if let event = EventDetail.eventFromUniqueId(uniqueId: uniqueId) {
return event
}
}

I have no func inside my IntentTimelineProvider that could hold that code. Thanks for your help!
Accepted Answer
My widget was based on the EmojiRangerWidget sample code, which contained this:
Code Block swift
func character(for configuration: DynamicCharacterSelectionIntent) -> CharacterDetail {
if let name = configuration.hero?.identifier, let character = CharacterDetail.characterFromName(name: name) {
/* Save the last selected character to our App Group. */
CharacterDetail.setLastSelectedCharacter(heroName: name)
return character
}
return .panda
}

My event() function is just based on that.

You don't need it; it's just the way I get the event for my case. Since I get the event in two places it saves duplicating the code.

In getSnapshot() and getTimeline():
Code Block swift
let selectedEvent = event(for: configuration)

That passes the intent configuration to the event() function, and returns the first event where its uniqueId matches the one provided by the intent. So, the user still selects the event's name when they edit the widget (as that's way more user-friendly), but what we're actually basing the widget on is the uniqueId instead. Does that make sense?
Yep, that makes sense now. Many thanks! :o)
Widget is cleared if the intent identifier changes: Easy fix
 
 
Q