Как обновить цвет фона виджета iOS 14 из приложения?

Для моего виджета я хочу, чтобы пользователи могли выбирать цвет фона виджета по своему выбору, но я не могу заставить его работать.

Я пытаюсь сделать это с помощью ColorPicker и @AppStorage

ColorPicker("Background Color", selection: Binding(get: {
                    bgColor
                }, set: { newValue in
                    backgroundColor = self.updateColorInAppStorage(color: newValue)
                    bgColor = newValue
                }))
                .frame(width: 200, height: 50)
                .font(.headline)

backgroundColor - это @AppStorage переменная, которая содержит значения RGB.

В моем классе расширения Widget я использую эту переменную для установки цвета фона виджета в структуре @main:

@main
struct MyWidget: Widget {
    @AppStorage("backgroundColor", store: UserDefaults(suiteName: "group.com.MyWidget")) var backgroundColor = ""
    @State private var bgColor = Color(UIColor.systemBackground)
    let kind: String = "Count"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            SubscriberCountEntryView(entry: entry)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(bgColor)
                .onAppear {
                    if (backgroundColor != "") {
                        let rgbArray = backgroundColor.components(separatedBy: ",")
                        if let red = Double(rgbArray[0]), let green = Double(rgbArray[1]), let blue = Double(rgbArray[2]), let alpha = Double(rgbArray[3]) {
                            bgColor = Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
                        }
                    }
                }
        }
        .configurationDisplayName("Count")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

Кажется, что это работает, только когда приложение впервые установлено на устройстве. После этого, сколько бы раз я ни выбирал цвет и не обновлял виджет, цвет фона не изменится. Если я удалю и снова установлю приложение, цвет изменится.

Есть идеи, как это сделать правильно?


person Arjun    schedule 07.11.2020    source источник


Ответы (1)


Вы получаете доступ к UserDefaults в своем виджете, а не в провайдере временной шкалы виджета. Вы также излишне сложным образом сохраняете свой цвет.

Вот простой пример, который показывает, как сохранить UIColor в UserDefaults и получить к нему доступ в своем виджете. Хотя вы используете Color, цветовые структуры можно создавать из UIColor. Однако ColorPicker позволяет создавать привязку с CGColor или Color, а CGColor можно легко преобразовать в UIColor.

ContentView

В моем ContentView я создал простое приложение, использующее ColorPicker с привязкой CGColor. Когда цвет выбран, мы передаем его как UIColor в функцию сохранения. Он использует NSKeyedArchiver для преобразования UIColor в данные, которые можно легко сохранить в UserDefaults. Я использую AppStorage для хранения данных, созданных из UIColor.

Затем мы вызываем WidgetCenter.shared.reloadAllTimelines(), чтобы убедиться, что WidgetCenter знает, что мы хотим обновить виджеты.

import SwiftUI
import WidgetKit

struct ContentView: View {

    @AppStorage("color", store: UserDefaults(suiteName: "group.com.my.app.identifier"))
    var colorData: Data = Data()

    @State private var bgColor: CGColor = UIColor.systemBackground.cgColor

    var body: some View {

        ColorPicker("Color", selection: Binding(get: {
            bgColor
        }, set: { newValue in
            save(color: UIColor(cgColor: newValue))
            bgColor = newValue
        }))
    }

    func save(color: UIColor) {
        do {
            colorData = try NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: false)
            WidgetCenter.shared.reloadAllTimelines()
        } catch let error {
            print("error color key data not saved \(error.localizedDescription)")
        }
    }
}

MyWidget

Затем в Provider мы используем оболочку свойств AppStorage для доступа к данным, которые мы сохранили для цвета. У нас нет доступа к AppStorage внутри MyWidgetEntryView или внутрь MyWidget, так как там это не работает.

Внутри моего провайдера я создал вычисляемое свойство, которое получает UIColor из данных о цвете, которые мы сохранили в AppStorage. Затем этот цвет передается каждой записи при ее создании. Это ключ к использованию AppStorage с вашими виджетами, значения должны быть переданы при создании записи.

MyWidgetEntryView очень прост, он просто показывает дату его создания и цвет фона.

import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {

    @AppStorage("color", store: UserDefaults(suiteName: "group.com.my.app.identifier"))
    var colorData: Data = Data()

    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(color: color)
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(color: color)
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let entry = SimpleEntry(color: color)
        let timeline = Timeline(entries: [entry], policy: .atEnd)
        completion(timeline)
    }

    var color: UIColor {

        var color: UIColor?

        do {
            color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: colorData)
        } catch let error {
            print("color error \(error.localizedDescription)")

        }
        return color ?? .systemBlue
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date = Date()
    let color: UIColor
}

struct MyWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        ZStack {
            Color(entry.color)
            Text(entry.date, style: .time)
        }
   }
}

@main
struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

Вот он работает ????

Виджет с палитрой цветов

person Andrew    schedule 07.11.2020
comment
Спасибо! Сработало отлично. Однако есть одно предложение: перетаскивание через палитру цветов приводит к тому, что функция сохранения вызывается сотни раз, без необходимости перезагружая виджет. Поэтому вместо этого я настраиваю издателя центра уведомлений, чтобы проверять, когда приложение переходит в фоновый режим, а затем перезагружаю виджет. - person Arjun; 08.11.2020
comment
Вы также можете использовать .onReceive с ObservableObject, отслеживающим выбранный цвет, и добавляя к нему дебаунс, ограничивая количество вызовов функции сохранения. Это потребует немного дополнительных настроек, но избавит от необходимости отправлять уведомление. Существует множество опций, которые можно использовать для уменьшения количества совершаемых вызовов. - person Andrew; 09.11.2020
comment
@Andrew: Это также работает с виджетами mac os? Не могли бы вы опубликовать репозиторий? - person Paul; 13.07.2021
comment
@Paul Я не пробовал на macOS. О каком репозитории вы говорите? - person Andrew; 13.07.2021