【问题标题】:Fetch CoreData in a Widget在小部件中获取 CoreData
【发布时间】:2021-06-13 01:38:03
【问题描述】:

我正在尝试从我的核心数据中获取实体和属性以显示在一个小部件中并且真的很挣扎。目前我可以获取实体的 ItemCount,但这不是我想要显示的。我希望能够显示我保存在 Core Data 中的字符串和图像。我查看了很多网站,包括 this post,但都没有成功。

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

这是用于显示实体的 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&lt;Project&gt;' has no member 'bodyText' - 我无法弄清楚为什么它没有看到我的实体项目

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

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

【问题讨论】:

    标签: ios swift core-data swiftui widgetkit


    【解决方案1】:

    据我在互联网上看到的,@NSFetchRequest 在小部件中不起作用。

    我为 Widget 创建了一个 CoreDataManager 类,它将从“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
        }()
    }
    

    小部件的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 模型“Vehicle”添加到了 WidgetEntry

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

    时间线提供程序在重新加载时间线并将我的核心数据模型注入到 WidgetEntry 时获取所有 CoreData

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

    【讨论】:

    • 感谢分享!这确实帮助我更接近。我在 WidgetEntry 中添加了var project: Project?,还与 Widget 共享了 Project+CoreDataProperties.swift 和相关的属性文件。这至少允许小部件找到项目属性(!!!)它实际上并没有在小部件中显示任何内容,但没有错误!哈哈。谢谢
    【解决方案2】:

    您可以删除这些行

    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() 并提供持久存储的默认位置的子类。

    另外,拥有 NSPersistentCloudKitContainer 的单个/共享实例也是一个好习惯,这样您就不必在每次使用 NSPersistentCloudKitContainer 变量时都加载存储。

    【讨论】:

    • 非常感谢@Stamenkovski 的回复!提前道歉,因为我对这一切仍然很陌生。我把你的覆盖放在哪里?它是在 Widget.swift 中还是在我的 Persistence.swift 中?
    • 持久性需要从 NSPersistentCloudKitContainer 继承,然后你可以在那里覆盖它
    • 这是我的 Persistence.swift:pastebin.com/T11UtLKm 我在其中定义了 NSPersistentCloudKitContainer。你的覆盖会进入那里吗?
    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 2021-01-04
      • 1970-01-01
      • 2011-11-08
      • 2021-06-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-04
      • 1970-01-01
      相关资源
      最近更新 更多