在小部件中获取 CoreData

Fetch CoreData in a Widget

我正在尝试从我的核心数据中获取实体和属性以显示在小部件中,但确实很困难。目前我可以获取实体的 ItemCount,但这不是我要显示的内容。我希望能够显示我保存在 Core Data 中的字符串和图像。我查看了大量网站,包括 ,但没有成功。

我当前的设置有一个用于核心数据的工作应用程序组,该应用程序组共享给主应用程序和小部件。

这是用于显示实体的 ItemCount 的 Widget.swift 代码:

import WidgetKit
import SwiftUI
import CoreData


private struct Provider: TimelineProvider {
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }

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

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        let midnight = Calendar.current.startOfDay(for: Date())
        let nextMidnight = Calendar.current.date(byAdding: .day, value: 1, to: midnight)!
        let entries = [SimpleEntry(date: midnight)]
        let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
        completion(timeline)
    }
}

private struct SimpleEntry: TimelineEntry {
    let date: Date
}

private struct MyAppWidgetEntryView: View {
    var entry: Provider.Entry
    let moc = PersistenceController.shared.container.viewContext
    let predicate = NSPredicate(format: "favorite == true")
    let request = NSFetchRequest<Project>(entityName: "Project")
    
    
    var body: some View {
        // let result = try moc.fetch(request)
        
        VStack {
            Image("icon")
                .resizable()
                .aspectRatio(contentMode: .fit)
            Text("Item Count: \(itemsCount)")
                .padding(.bottom)
                .foregroundColor(.secondary)
        }
    }

    var itemsCount: Int {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Project")
        do {
            return try PersistenceController.shared.container.viewContext.count(for: request)

        } catch {
            print(error.localizedDescription)
            return 0
        }
    }
}
@main
struct MyAppWidget: Widget {
    let kind: String = "MyAppWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            MyAppWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("MyApp Widget")
        .description("Track your project count")
        .supportedFamilies([.systemSmall])
    }
}

但是,我真正希望能够做的是从实体访问属性 bodyTextimage1,就像我的主应用程序访问它的方式一样。例如,

VStack {
        Image(uiImage: UIImage(data: project.image1 ?? self.projectImage1)!)
             .resizable()
             .aspectRatio(contentMode: .fill)
 
        Text("\(project.bodyText!)")
    }

这是 Widget.swift 代码,它似乎更接近我要完成的目标,但它无法获得 project.bodyText 并出现错误 Value of type 'FetchedResults<Project>' has no member 'bodyText' -- 我想不通为什么它没有看到我的实体 Project

的属性
// MARK: Core Data
var managedObjectContext: NSManagedObjectContext {
    return persistentContainer.viewContext
}

var workingContext: NSManagedObjectContext {
    let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    context.parent = managedObjectContext
    return context
}

var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "MyAppApp")

    let storeURL = URL.storeURL(for: "group.com.me.MyAppapp", databaseName: "MyAppApp")
    let description = NSPersistentStoreDescription(url: storeURL)

    container.loadPersistentStores(completionHandler: { storeDescription, error in
        if let error = error as NSError? {
            print(error)
        }
    })

    container.viewContext.automaticallyMergesChangesFromParent = true
    container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy

    return container
}()


// MARK: WIDGET
struct Provider: TimelineProvider {

// CORE DATA
var moc = managedObjectContext

    init(context : NSManagedObjectContext) {
        self.moc = context
    }

// WIDGET
func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date())
}

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

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var entries: [SimpleEntry] = []

    // Generate a timeline consisting of five entries an hour apart, starting from the current date.
    let currentDate = Date()
    for hourOffset in 0 ..< 5 {
        let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
        let entry = SimpleEntry(date: entryDate)
        entries.append(entry)
    }

    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
}
}

struct SimpleEntry: TimelineEntry {
let date: Date

}

struct MyAppWidgetEntryView : View {
var entry: Provider.Entry

@FetchRequest(entity: Project.entity(), sortDescriptors: []) var project: FetchedResults<Project>

var body: some View {

        Text("\(project.bodyText)").  <--------------------- ERROR HAPPENS HERE

}
}

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

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

struct MyAppWidget_Previews: PreviewProvider {
static var previews: some View {
    MyAppWidgetEntryView(entry: SimpleEntry(date: Date()))
        .previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

我接近了吗?你有没有碰巧得到类似的工作?非常感谢!

您可以删除这些行

let storeURL = URL.storeURL(for: "group.com.me.MyAppapp", databaseName: "MyAppApp")
let description = NSPersistentStoreDescription(url: storeURL)

而是像这样覆盖 defaultDirectoryURL()

override public class func defaultDirectoryURL() -> URL {
    let storeURL = FileManager.default.containerURL(
        forSecurityApplicationGroupIdentifier: "group.com.me.MyAppapp"
    )
    return storeURL!
}

编辑:
刚刚看到您没有 NSPersistentCloudKitContainer 的子类,您应该有一个可以覆盖 defaultDirectoryURL() 并提供持久存储的默认位置的子类。

此外,拥有一个 single/shared NSPersistentCloudKitContainer 实例也是一个好习惯,这样您就不会在每次使用 NSPersistentCloudKitContainer 变量时都加载商店。

据我在互联网上的了解,@NSFetchRequest 在 Widgets 中不起作用。

我为小部件创建了一个 CoreDataManager class,它将从“getTimeline”获取数据

我的代码有点复杂,因为我的小部件是可配置的,但也许它可以帮助您了解如何让它工作。

首先是在主应用程序和小部件之间共享的 DataController。

    class DataController: ObservableObject {
    let container: NSPersistentCloudKitContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "Main", managedObjectModel: Self.model)
        let storeURL = URL.storeURL(for: "MyGroup", databaseName: "Main")
        let storeDescription = NSPersistentStoreDescription(url: storeURL)
        storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
            containerIdentifier: "Identifier"
        )
        container.persistentStoreDescriptions = [storeDescription]

        guard let description = container.persistentStoreDescriptions.first else {
            Log.shared.add(.coreData, .error, "###\(#function): Failed to retrieve a persistent store description.")
            fatalError("###\(#function): Failed to retrieve a persistent store description.")
        }

        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        if inMemory {
            container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
        }

        container.loadPersistentStores { (_, error) in
            if let error = error {
                Log.shared.add(.cloudKit, .fault, "Fatal error loading store \(error)")
                fatalError("Fatal error loading store \(error.localizedDescription)")
            }
        }

        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }

    static let model: NSManagedObjectModel = {
        guard let url = Bundle.main.url(forResource: "Main", withExtension: "momd") else {
            fatalError("Failed to locate model file.")
        }

        guard let managedObjectModel = NSManagedObjectModel(contentsOf: url) else {
            fatalError("Failed to load model file.")
        }

        return managedObjectModel
    }()
}

Widget 的 CoreDataManager

class CoreDataManager {
    var vehicleArray = [Vehicle]()
    let dataController: DataController

    private var observers = [NSObjectProtocol]()

    init(_ dataController: DataController) {
        self.dataController = dataController
        fetchVehicles()

        /// Add Observer to observe CoreData changes and reload data
        observers.append(
            NotificationCenter.default.addObserver(forName: .NSPersistentStoreRemoteChange, object: nil, queue: .main) { _ in //swiftlint:disable:this line_length discarded_notification_center_observer
                self.fetchVehicles()
            }
        )
    }

    deinit {
        /// Remove Observer when CoreDataManager is de-initialized
        observers.forEach(NotificationCenter.default.removeObserver)
    }

    /// Fetches all Vehicles from CoreData
    func fetchVehicles() {
        defer {
            WidgetCenter.shared.reloadAllTimelines()
        }

        dataController.container.viewContext.refreshAllObjects()
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Vehicle")

        do {
            guard let vehicleArray = try dataController.container.viewContext.fetch(fetchRequest) as? [Vehicle] else {
                return
            }
            self.vehicleArray = vehicleArray
        } catch {
            print("Failed to fetch: \(error)")
        }
    }
}

我确实将我的 CoreData 模型“车辆”添加到 WidgetEntry

struct WidgetEntry: TimelineEntry {
    let date: Date
    let configuration: SelectedVehicleIntent
    var vehicle: Vehicle?
}

Timeline Provider 在重新加载时间线时获取所有 CoreData 并将我的核心数据模型注入 WidgetEntry

struct Provider: IntentTimelineProvider {

    /// Access to CoreDataManger
    let dataController = DataController()
    let coreDataManager: CoreDataManager

    init() {
        coreDataManager = CoreDataManager(dataController)
    }

    /// Placeholder for Widget
    func placeholder(in context: Context) -> WidgetEntry {
        WidgetEntry(date: Date(), configuration: SelectedVehicleIntent(), vehicle: Vehicle.example)
    }

    /// Provides a timeline entry representing the current time and state of a widget.
    func getSnapshot(for configuration: SelectedVehicleIntent, in context: Context, completion: @escaping (WidgetEntry) -> Void) { //swiftlint:disable:this line_length
        vehicleForWidget(for: configuration) { selectedVehicle in
            let entry = WidgetEntry(
                date: Date(),
                configuration: configuration,
                vehicle: selectedVehicle
            )
            completion(entry)
        }
    }

    /// Provides an array of timeline entries for the current time and, optionally, any future times to update a widget.
    func getTimeline(for configuration: SelectedVehicleIntent, in context: Context, completion: @escaping (Timeline<WidgetEntry>) -> Void) { //swiftlint:disable:this line_length
        coreDataManager.fetchVehicles()

        /// Fetches the vehicle selected in the configuration
        vehicleForWidget(for: configuration) { selectedVehicle in
            let currentDate = Date()
            var entries: [WidgetEntry] = []

            // Create a date that's 60 minutes in the future.
            let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 60, to: currentDate)!

            // Generate an Entry
            let entry = WidgetEntry(date: currentDate, configuration: configuration, vehicle: selectedVehicle)
            entries.append(entry)

            let timeline = Timeline(entries: entries, policy: .after(nextUpdateDate))
            completion(timeline)
        }
    }

    /// Fetches the Vehicle defined in the Configuration
    /// - Parameters:
    ///   - configuration: Intent Configuration
    ///   - completion: completion handler returning the selected Vehicle
    func vehicleForWidget(for configuration: SelectedVehicleIntent, completion: @escaping (Vehicle?) -> Void) {
        var selectedVehicle: Vehicle?
        defer {
            completion(selectedVehicle)
        }

        for vehicle in coreDataManager.vehicleArray where vehicle.uuid?.uuidString == configuration.favoriteVehicle?.identifier { //swiftlint:disable:this line_length
            selectedVehicle = vehicle
        }
    }
}

最后是 WidgetView

struct WidgetView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text("CoreData: \(entry.vehicle?.fuel?.count ?? 99999)")
            Text("Name: \(entry.vehicle?.brand ?? "No Vehicle found")")
            Divider()
            Text("Refreshed: \(entry.date, style: .relative)")
                .font(.caption)
        }
        .padding(.all)
    }
}

确保在文件检查器的目标成员列表中选中核心数据模型。