如何在 IntentConfiguration Widget view iOS 14 中显示与 CoreData 中同一行相关的数据?

How to display data related to the same row in CoreData at IntentConfiguration Widget view iOS 14?

我目前正在使用 SwiftUI 开发应用程序并尝试让 widget ios 14 用户可以使用 IntentConfiguration

选择数据来检查其详细信息

我想在这个应用程序中做的大纲如下:

  1. 用户在添加视图中添加了一些数据(id:UUID,任务:String,状态:String,还有一些...)到 CoreData 在主机应用中
  2. 用户选择了用户想要在编辑小部件中查看其详细信息的数据
  3. 用户可以在小部件视图中查看简要详细信息
  4. 如果用户点击小部件视图,可以在宿主应用程序的详细视图中查看详细数据

在我的代码中,我几乎可以实现上面解释的所有功能。

但我不知道如何在编辑小部件中显示任务数据...

到目前为止,我将CoreData中的UUID数据指定为配置中的参数。因为 WidgetEntryView 需要 UUID(或某些唯一值)来过滤哪些 row 请求 CoreData 并使 URLDeepLink 进行详细查看在主机应用程序中。

因此小部件中的列表视图显示 UUID 数据,如下所示。

但我想在该列表视图中显示任务数据而不是 UUID,同时将 UUID 数据也提供给 Widget 视图。

如果我将 CoreData 中的任务数据指定为配置中的参数,小部件会将任务显示为列表。但是在那种情况下,Coredata (NSPredicate(format: "id == %@") 和 widgetURL() 请求数据的过滤器不起作用...

我该如何实现?


代码如下:

TimerIntentWidget.swift

import Foundation
import WidgetKit
import SwiftUI
import CoreData

struct Provider: IntentTimelineProvider {
    
    typealias Intent = ConfigurationIntent
    
    var moc = PersistenceController.shared.managedObjectContext
    
    init(context : NSManagedObjectContext) {
        self.moc = context
    }
    
    func placeholder(in context: Context) -> SimpleEntry {
        
        var timerEntity:TimerEntity?
        let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
       
        do{
            let result = try moc.fetch(request)
            timerEntity = result.first
        }
        catch let error as NSError{
            print("Could not fetch.\(error.userInfo)")
        }
        
        return SimpleEntry(configuration: ConfigurationIntent(), date: Date(), timerEntity: timerEntity!)
    }
    
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        var timerEntity:TimerEntity?
        let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
      
        do{
            let result = try moc.fetch(request)
            timerEntity = result.first
        }
        catch let error as NSError{
            print("Could not fetch.\(error.userInfo)")
        }
        
        let entry = SimpleEntry(configuration: configuration, date: Date(), timerEntity: timerEntity!)
        completion(entry)
    }
    
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        
        var timerEntity:TimerEntity?
        let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
        
        request.predicate = NSPredicate(format: "id == %@", UUID(uuidString: configuration.UUID!)! as CVarArg)
        do{
            let result = try moc.fetch(request)
            timerEntity = result.first
        }
        catch let error as NSError{
            print("Could not fetch.\(error.userInfo)")
        }
        
        var entries: [SimpleEntry] = []
        
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(configuration: configuration, date: entryDate, timerEntity: timerEntity!)
            entries.append(entry)
        }
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let configuration: ConfigurationIntent
    let date: Date
    let timerEntity:TimerEntity?

}

struct TimerIntentWidgetEntryView : View{
    var entry: Provider.Entry
    
    var body: some View {
        VStack{
            Text(entry.timerEntity!.id!.uuidString)
            Divider()
            Text(entry.timerEntity!.task!)
            Divider()
            Text(entry.timerEntity!.status!)
            Divider()
            Text(entry.date, style: .time)
        }
        .widgetURL(makeURLScheme(id: entry.timerEntity!.id!))
    }
}

@main
struct TimerIntentWidget: Widget {
    let kind: String = "TimerIntentWidget"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider(context: PersistenceController.shared.managedObjectContext)) { entry in
            TimerIntentWidgetEntryView(entry: entry)
                .environment(\.managedObjectContext, PersistenceController.shared.managedObjectContext)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

func makeURLScheme(id: UUID) -> URL? {
    guard let url = URL(string: "timerlist://detail") else {
        return nil
    }
    var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)
    urlComponents?.queryItems = [URLQueryItem(name: "id", value: id.uuidString)]
    return urlComponents?.url
}

IntentHandler.swift

import WidgetKit
import SwiftUI
import CoreData
import Intents

class IntentHandler: INExtension,ConfigurationIntentHandling {
    
    var moc = PersistenceController.shared.managedObjectContext
    
    func provideUUIDOptionsCollection(for intent: ConfigurationIntent, with completion: @escaping (INObjectCollection<NSString>?, Error?) -> Void) {
        
        let request = NSFetchRequest<TimerEntity>(entityName: "TimerEntity")
                var nameIdentifiers:[NSString] = []
        
                do{
                    let results = try moc.fetch(request)
        
                    for result in results{
                        nameIdentifiers.append(NSString(string: result.id?.uuidString ?? ""))
                    }
                }
                catch let error as NSError{
                    print("Could not fetch.\(error.userInfo)")
                }
        
                let allNameIdentifiers = INObjectCollection(items: nameIdentifiers)
                completion(allNameIdentifiers,nil)
    }
    
    override func handler(for intent: INIntent) -> Any {
        return self
    }
}

TimerIntentWidget.intentdefinition

Persistence.swift(主机应用)

import CoreData

class PersistenceController {
    static let shared = PersistenceController()

    private init() {}

    private let persistentContainer: NSPersistentContainer = {
        let storeURL = FileManager.appGroupContainerURL.appendingPathComponent("TimerEntity")

        let container = NSPersistentContainer(name: "ListTimer")
        container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
        container.loadPersistentStores(completionHandler: { storeDescription, error in
            if let error = error as NSError? {
                print(error.localizedDescription)
            }
        })
        return container
    }()
}

extension PersistenceController {
    var managedObjectContext: NSManagedObjectContext {
        persistentContainer.viewContext
    }
}

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

import Foundation

extension FileManager {
    static let appGroupContainerURL = FileManager.default
        .containerURL(forSecurityApplicationGroupIdentifier: "group.com.sample.ListTimer")!
}

Xcode: 版本 12.0.1

iOS: 14.0

生命周期:SwiftUI 应用程序

您可以为配置参数创建自定义类型。目前您使用的是 String,这限制了您只能使用一个值。

创建一个自定义类型,我们称之为 Item:

现在您的 Item 具有 identifierdisplayString 属性,可以映射到模型的 UUIDtask 属性。

然后,在 IntentHandler 而不是 INObjectCollection<NSString>? 中,您需要在完成中提供 INObjectCollection<Item>?

假设您已经从核心数据中获取了 results,您只需要将它们映射到 Item 对象:

let results = try moc.fetch(request)
let items = results.map {
    Item(identifier: [=10=].id.uuidString, display: [=10=].task)
}
completion(items, nil)

这样你可以使用 display 属性 向用户显示 可读的 信息,但也有 identifier 属性 稍后可用于检索核心数据模型。