Jon T
Jon T

Reputation: 81

SwiftUI widget not updating when @AppStorage value is changed

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

Answers (5)

mirzabek
mirzabek

Reputation: 1

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

WidgetCenter.shared.reloadAllTimelines()

Upvotes: 0

user7196970
user7196970

Reputation: 61

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

Upvotes: 4

Adam
Adam

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

pawello2222
pawello2222

Reputation: 54516

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)
    }
}

Upvotes: 0

Asperi
Asperi

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

Related Questions