SwiftUI widget not updating when @AppStorage value is changed
Asked Answered
F

5

8

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")
    }
}
Fork answered 21/1, 2021 at 18:10 Comment(0)
V
6

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.

Vanden answered 21/1, 2021 at 20:57 Comment(1)
Thanks for the suggestion, widget still not updating though. I've been looking through a bunch of examples online and everyone seems to be getting it to work a similar way. Not sure what I'm missing.Fork
K
4

Make sure you add your app group to both targets in Signing & Capabilities

Klepht answered 26/1, 2021 at 23:18 Comment(1)
THIS! Thank you for this answer. I totally forgot that I set this up in a previous app when I copied over the code. The group user defaults appeared to be working in my app, but weren't actually storing them in the shared space. Once I added an App Groups section under Signing and Certificates for each Target it started working as expected.Highbinder
P
0

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)
        }
    }
}
Philipines answered 21/1, 2021 at 18:16 Comment(1)
That helps, but I'm pretty sure the issue comes from the @AppStorage stored value not updating. When using print statements in the timeline, each time it runs it prints the same message from when the app launched, even though the value should be updated.Fork
G
0

There are two basic issues with the AppStorage+Widget approach:

  1. You can't use @AppStorage outside the SwiftUI View - especially not in the IntentTimelineProvider.
  2. Widget views are static - even if you use @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)
    }
}
Gerta answered 21/1, 2021 at 18:58 Comment(2)
Thanks for the help. I applied your suggestions and I verified the app was correctly saving and reading new values. The widget however is still only reading once at launch and every update has the old value.Fork
Doesn't limiting @AppStorage to the View introduce another source of truth, which is antithetical to the SwiftUI model? At the very least, it now makes it necessary to store truth information solely in a View and then pass it around to your other sources of truth, against the SwiftUI pattern.Dulcie
A
0

Try to update widget once you change one of appStorage/default values

WidgetCenter.shared.reloadAllTimelines()
Anikaanil answered 7/5, 2024 at 5:23 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.