Hello everyone 👋
I'm currently implementing the new interactive widget for iOS 17 but there is a behaviour I don't understand.
My Goal :
The widget I want to create is pretty simple : I have a Store
where I can find a list of MyObject
, and in the widget I want to display 2 items randomly every X hours. For each item, there is a button to like/unlike the item.
The Issue :
When I tap the like button, the function func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ())
of the widget is called again and so, two items are get randomly (so the items I liked disappear).
What I want :
When two items are got randomly and I tap a like button, I want my item isLiked
property to be toggled without refreshing my timeline.
My current code :
(The code is simplified for the demo, but if you copy/paste all of this code in a new widget, it should compile and run)
MyObject
final class MyObject {
var isLiked: Bool
let name: String
init(isLiked: Bool, name: String) {
self.isLiked = isLiked
self.name = name
}
}
MyStore
final class MyStore {
static let shared: MyStore = .init()
private init() { }
var myObjects: [MyObject] = [
.init(isLiked: false, name: "Test 1"),
.init(isLiked: true, name: "Test 2"),
.init(isLiked: false, name: "Test 3"),
.init(isLiked: false, name: "Test 4"),
.init(isLiked: false, name: "Test 5"),
.init(isLiked: true, name: "Test 6"),
.init(isLiked: false, name: "Test 7"),
.init(isLiked: false, name: "Test 8"),
.init(isLiked: false, name: "Test 9"),
.init(isLiked: true, name: "Test 10"),
.init(isLiked: false, name: "Test 11"),
.init(isLiked: false, name: "Test 12"),
.init(isLiked: true, name: "Test 13"),
.init(isLiked: false, name: "Test 14"),
]
func getRandom(_ number: Int) -> [MyObject] {
guard !myObjects.isEmpty else { return [] }
var random: [MyObject] = []
for _ in 0 ... number - 1 {
let randomIndex: Int = Int.random(in: 0...myObjects.count - 1)
random.append(myObjects[randomIndex])
}
return random
}
}
My action intent
import AppIntents
struct AddOrRemoveFromFavoriteAppIntent: AppIntent {
static let title: LocalizedStringResource = "My title"
static let description: IntentDescription = .init("My description")
@Parameter(title: "name")
var name: String
init() { }
init(name: String) {
self.name = name
}
func perform() async throws -> some IntentResult {
MyStore.shared.myObjects.first(where: { $0.name == name })?.isLiked.toggle()
return .result()
}
}
My widget
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
My widget view
struct MyWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
if entry.objects.count >= 2 {
HStack {
HStack {
Text(entry.objects[0].name)
Button(intent: AddOrRemoveFromFavoriteAppIntent(name: entry.objects[0].name)) {
Text(entry.objects[0].isLiked ? "Unlike" : "Like")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
HStack {
Text(entry.objects[1].name)
Button(intent: AddOrRemoveFromFavoriteAppIntent(name: entry.objects[1].name)) {
Text(entry.objects[1].isLiked ? "Unlike" : "Like")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
}
} else {
Text("No data")
}
}
}
My widget model
struct SimpleEntry: TimelineEntry {
let date: Date
let objects: [MyObject]
}
My widget provider
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), objects: [])
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), objects: [])
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, objects: MyStore.shared.getRandom(2))
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
What am I missing ? Are widgets not designed for this use case ?
Thanks 🙏
Alexandre