Виджет SwiftUI не обновляется при изменении значения @AppStorage

Я пытаюсь обновить виджет SwiftUI по команде при изменении значения @AppStorage. Когда я загружаю симулятор, виджет обновляется до правильного значения из @AppStorage, но не обновляется снова, что бы я ни пытался. Чтобы отобразить новое значение в виджете, симулятор необходимо закрыть и снова открыть.

Посмотреть в приложении:

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

    }
}

Файл виджета:

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

person Jon T    schedule 21.01.2021    source источник


Ответы (4)


AppStorage должен быть в поле зрения, все остальное остается в поле зрения

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)
        }
    }
}
person Asperi    schedule 21.01.2021
comment
Это помогает, но я почти уверен, что проблема связана с тем, что сохраненное значение @AppStorage не обновляется. При использовании операторов печати на шкале времени каждый раз при запуске он печатает то же сообщение, что и при запуске приложения, даже если значение должно быть обновлено. - person Jon T; 21.01.2021

При использовании подхода AppStorage + Widget возникают две основные проблемы:

  1. Вы не можете использовать @AppStorage вне SwiftUI View - особенно в IntentTimelineProvider.
  2. Представления виджетов статичны - даже если вы используете @AppStorage в представлении виджетов, значение будет прочитано только один раз (побеждая точку @AppStorage).

Вместо этого вам нужно вручную прочитать значение из 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)
    }
}
person pawello2222    schedule 21.01.2021
comment
Спасибо за помощь. Я применил ваши предложения и убедился, что приложение правильно сохраняет и считывает новые значения. Однако виджет по-прежнему читает только один раз при запуске, и каждое обновление имеет старое значение. - person Jon T; 21.01.2021
comment
Разве ограничение @AppStorage представлением не представляет собой еще один источник истины, который противоречит модели SwiftUI? По крайней мере, теперь возникает необходимость хранить правдивую информацию исключительно в представлении, а затем передавать ее другим вашим источникам правды в соответствии с шаблоном SwiftUI. - person promacuser; 19.02.2021

Вам необходимо убедиться, что новое значение записано на диск перед вызовом WidgetCenter.shared.reloadAllTimelines(), и я не думаю, что у @AppStorage есть способ сделать это. В своем приложении попробуйте установить параметры UserDefaults напрямую, а затем вызовите synchronize() перед перезагрузкой виджетов:

Button(action: {
   let userDefaults = UserDefaults(suiteName: "group.com.suiteName")!
   userDefaults.set("new message", forKey: "message")
   userDefaults.synchronize()
   WidgetCenter.shared.reloadAllTimelines()
}, label: { Text("button") })

Я знаю, что претензия к документации synchronize() больше не нужна, но это единственное, что у меня сработало. ¯ \ _ (ツ) _ / ¯

Также может помочь использование UserDefaults вместо @AppStorage в вашем виджете.

person Adam    schedule 21.01.2021
comment
Спасибо за предложение, но виджет все еще не обновляется. Я просмотрел кучу примеров в Интернете, и, похоже, у всех это работает одинаково. Не уверен, что мне не хватает. - person Jon T; 22.01.2021

Убедитесь, что вы добавили группу приложений к обеим целям в разделе «Подписание и возможности».

person user7196970    schedule 26.01.2021