Reputation: 81
I am trying to get a SwiftUI widget to update on command when the value of the @AppStorage changes. When I load the simulator the widget updates to the correct value from @AppStorage but does not update again no matter what I try. To display a new value in the widget, the simulator needs to be closed and reopened.
View in app:
import SwiftUI
import WidgetKit
struct MessageView: View {
@AppStorage("message", store: UserDefaults(suiteName: "group.com.suiteName"))
var widgetMessage: String = ""
var body: some View {
VStack {
Button(action: {
self.widgetMessage = "new message"
WidgetCenter.shared.reloadAllTimelines()
}, label: { Text("button") })
}
}
}
Widget file:
import WidgetKit
import SwiftUI
import Intents
struct Provider: IntentTimelineProvider {
@AppStorage("message", store: UserDefaults(suiteName: "group.com.suiteName"))
var widgetMessage: String = ""
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), message: "Have a great day!", configuration: ConfigurationIntent())
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), message: "Have a great day!", configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let entry = SimpleEntry(date: Date(), message: widgetMessage, configuration: configuration)
let timeline = Timeline(entries: [entry], policy: .never)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let message: String
let configuration: ConfigurationIntent
}
struct statusWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack{
Text(entry.message)
}
}
}
@main
struct statusWidget: Widget {
let kind: String = "StatusWidget"
var body: some WidgetConfiguration {
IntentConfiguration(
kind: kind,
intent: ConfigurationIntent.self,
provider: Provider()
) { entry in
statusWidgetEntryView(entry: entry)
}
.configurationDisplayName("Note Widget")
.description("Display note from a friend or group")
}
}
Upvotes: 8
Views: 4195
Reputation: 1
Try to update widget once you change one of appStorage/default values
WidgetCenter.shared.reloadAllTimelines()
Upvotes: 0
Reputation: 61
Make sure you add your app group to both targets in Signing & Capabilities
Upvotes: 4
Reputation: 5115
You need to ensure the new value is written to disk before you call WidgetCenter.shared.reloadAllTimelines()
, and I don't think @AppStorage
has a way to do that. In your app, try setting UserDefaults directly and then call synchronize()
before reloading the widgets:
Button(action: {
let userDefaults = UserDefaults(suiteName: "group.com.suiteName")!
userDefaults.set("new message", forKey: "message")
userDefaults.synchronize()
WidgetCenter.shared.reloadAllTimelines()
}, label: { Text("button") })
I know the docs claim synchronize()
is no longer necessary, but it was the only thing that worked for me. ¯\_(ツ)_/¯
It may help to use UserDefaults
instead of @AppStorage
in your widget too.
Upvotes: 6
Reputation: 54516
There are two basic issues with the AppStorage+Widget approach:
@AppStorage
outside the SwiftUI View
- especially not in the IntentTimelineProvider
.@AppStorage
in the widget view, the value will only be read once (defeating the point of @AppStorage
).Instead, you need to manually read the value from UserDefaults
:
struct Provider: IntentTimelineProvider {
let userDefaults = UserDefaults(suiteName: "group.com.suiteName")!
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), message: "Have a great day!", configuration: ConfigurationIntent())
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), message: "Have a great day!", configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// read on every timeline refresh
let widgetMessage = userDefaults.string(forKey: "message") ?? ""
let entry = SimpleEntry(date: Date(), message: widgetMessage, configuration: configuration)
let timeline = Timeline(entries: [entry], policy: .never)
completion(timeline)
}
}
Upvotes: 0
Reputation: 257779
The AppStorage
should be in view, everything else keep in entry
struct statusWidgetEntryView : View {
@AppStorage("message", store: UserDefaults(suiteName: "group.com.suiteName"))
var widgetMessage: String = ""
var entry: Provider.Entry
var body: some View {
VStack{
Text(widgetMessage)
}
}
}
Upvotes: 0