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!