0

I am new in iOS development.

What I am trying to do: I have an app that stores photos as a data in CoreData. I have host app and widget extension. I fetch photos in host app and it works fine. However, when I try it to be accessed with widget I see no result. I've created a separate project to test this feature. Right now I save timestamps and names to be fetched.

I've seen similar questions on the web and even step by step tutorial how to fetch data from CoreData and display it in your widget. Here is the answer I'm referring to: https://stackoverflow.com/a/63945538/16649677 Despite the fact that everything is described quite understandable, I cannot get how to access my data.

Please kindly advice what am I missing? Maybe you would see what I cannot. You'll see my code below. Thank you in advance!

Content View

import SwiftUI
import CoreData
import WidgetKit

struct ContentView: View {
    @State private var textField = ""
    @FetchRequest(entity: Item.entity(), sortDescriptors: []) var items: FetchedResults<Item>
    @Environment(\.managedObjectContext) var moc  
  var body: some View {
        Group {
            VStack {
                TextField("text", text: $textField)
                Button("Submit") {

                    items.forEach { item in
                        item.timestamp = Date()
                        item.name = textField
                    }
                    
                    do {
                        try moc.save()
                        print("Saved")
                    } catch {
                        print(error.localizedDescription)
                    }
                    
                    WidgetCenter.shared.reloadAllTimelines()
                }
            }
        }
    }
}

WidgetTestProjectApp

 import SwiftUI
    
    @main
    struct WidgetTestProjectApp: App {
        let persistenceController = PersistenceController.shared
    
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environment(\.managedObjectContext, persistenceController.container.viewContext)
            }
        }
    }

CoreDataStack

import CoreData

class CoreDataStack {
    static let shared = CoreDataStack()
    
    private init() {}
    private let persistentContainer: NSPersistentContainer = {
        let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.name.widgetcache")!.appendingPathComponent("WidgetTestProject.sqlite")
        let container = NSPersistentContainer(name: "WidgetTestProject")
        
        var defaultURL: URL?
        if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
            defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
        }
        
        if defaultURL == nil {
            container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
        }
        container.loadPersistentStores(completionHandler: { [unowned container] (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
            
            if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
                let coordinator = container.persistentStoreCoordinator
                if let oldStore = coordinator.persistentStore(for: url) {
                    do {
                        try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
                    } catch {
                        print(error.localizedDescription)
                    }
                    
                    // delete old store
                    let fileCoordinator = NSFileCoordinator(filePresenter: nil)
                    fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
                        do {
                            try FileManager.default.removeItem(at: url)
                        } catch {
                            print(error.localizedDescription)
                        }
                    })
                }
            }
        })
        return container
    }()
}

// MARK: - Main context

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

    func saveContext() {
        managedObjectContext.performAndWait {
            if managedObjectContext.hasChanges {
                do {
                    try managedObjectContext.save()
                } catch {
                    print(error.localizedDescription)
                }
            }
        }
    }
}

// MARK: - Working context

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

    func saveWorkingContext(context: NSManagedObjectContext) {
        do {
            try context.save()
            saveContext()
        } catch {
            print(error.localizedDescription)
        }
    }
}

/// Taken from: https://stackoverflow.com/a/60266079/8697793
extension NSManagedObjectContext {
    /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
    ///
    /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
    /// - Throws: An error if anything went wrong executing the batch deletion.
    public func executeAndMergeChanges(_ batchDeleteRequest: NSBatchDeleteRequest) throws {
        batchDeleteRequest.resultType = .resultTypeObjectIDs
        let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
        let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
        NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
    }
}

WidgetExtension

import WidgetKit
import SwiftUI
import Intents
import CoreData

struct Provider: IntentTimelineProvider {
    let moc: NSManagedObjectContext
    init(moc: NSManagedObjectContext) {
        self.moc = CoreDataStack.shared.managedObjectContext
    }
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), text: "", configuration: ConfigurationIntent())
    }

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

    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        
        let request = NSFetchRequest<Item>(entityName: "Item")
        let results: [Item] = { do { return try moc.fetch(request)} catch { return [Item(context: moc)]} }()
        
        results.forEach { item in
        let entry = SimpleEntry(date: item.timestamp, text: item.name, configuration: configuration)
            entries.append(entry)
        let timeline = Timeline(entries: entries, policy: .never)
        completion(timeline)
            print(item.timestamp)
            print(item.name)
        }
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let text: String
    let configuration: ConfigurationIntent
}

struct PhotoShownEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        ZStack {
            Image("background")
                .resizable()
                .scaledToFill()
            VStack {
                Text(entry.text)
                Text(entry.date, style: .time)
            }
        }
    }
}

@main
struct PhotoShown: Widget {
    private let kind: String = "PhotoShown"
    public var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self,
                            provider: Provider(moc: moc)) { entry in
            PhotoShownEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
    var moc: NSManagedObjectContext = {
        let moc = CoreDataStack.shared.managedObjectContext
        return moc
    }()
}

struct PhotoShown_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            PhotoShownEntryView(entry: SimpleEntry(date: Date(), text: "Hello", configuration: ConfigurationIntent()))
                .previewContext(WidgetPreviewContext(family: .systemSmall))
        }
    }
}

In assets I have one random image just for background purposes and I changed my group name. PersistenceController is initialised and has only saveContext method (I haven't changed anything). Entity is called Item and has two non-optional properties: timestamp: Date and name: String.

Any advice would be helpful! Thank you again!

0 Answers0