7

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")
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
Jon T
  • 71
  • 1
  • 4

4 Answers4

4

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

user7196970
  • 61
  • 1
  • 5
  • 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. – MikeMilzz Feb 02 '22 at 18:12
3

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.

Adam
  • 4,405
  • 16
  • 23
  • 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. – Jon T Jan 22 '21 at 00:51
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)
        }
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 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. – Jon T Jan 21 '21 at 18:32
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)
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • 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. – Jon T Jan 21 '21 at 20:32
  • 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. – promacuser Feb 19 '21 at 18:05