Tio
Tio

Reputation: 1036

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

I'm currently developing an application using SwiftUI and trying to make a widget ios 14 user can choose a data to check its detail using IntentConfiguration

An outline I want to do in this App is like below:

  1. A user adds some data at an adding view (id: UUID, task: String, status: String, and some...) to CoreData in Host App
  2. The user chooses data the user wants to check its detail in the edit widget
  3. The user can check a brief detail in the widget view
  4. If the user taps the widget view, can check detailed data in a detailed view in the host App

In my codes, I could implement almost all the functions I explain above.

But I don't know How can I display task data in the edit widget...

So far, I specify UUID data in CoreData as a Parameter in Configuration. Because WidgetEntryView needs UUID(or something unique value) to filter which row requests to CoreData and to make URL for DeepLink to detail view in host App.

So the list view in the widget displays UUID data like below. enter image description here

But I want to display task data instead of UUID in that list view keeping to give UUID data to the Widget view as well.

If I specify task data in CoreData as a Parameter in Configuration, the widget displays tasks as a list. But in that case a filter for request data to Coredata (NSPredicate(format: "id == %@") and widgetURL() don't work...

How could I implement this?


Here are the codes:

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 enter image description here

Persistence.swift (Host App)

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: Version 12.0.1

iOS: 14.0

Life Cycle: SwiftUI App

Upvotes: 1

Views: 612

Answers (1)

pawello2222
pawello2222

Reputation: 54591

You can create a custom type for your configuration parameter. Currently you're using String which limits you to one value only.

Instead create a custom type, let's call it Item:

enter image description here

Now you have the identifier and displayString properties for your Item which can be mapped to the UUID and task properties of your model.

Then, in the IntentHandler instead of INObjectCollection<NSString>? you need to provide INObjectCollection<Item>? in the completion.

Assuming you already have your results fetched from Core Data, you only need to map them to the Item objects:

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

This way you can use the display property to show readable information to the user but also have the identifier property which can be later used to retrieve the Core Data model.

Upvotes: 1

Related Questions