Search code examples
swiftcore-datawidget

SwiftUI attempting to read CoreData inside widget always return empty?


I am trying to read my app CoreData file from my app Widget extension .

The read was success but the data fetch is always return empty while my app is reading the data successfully .

here is Prayers Entity : ( Target membership is selected to widget and app ) enter image description here

and this is my widget class :

import WidgetKit
import SwiftUI
import CoreData

class CoreDataStack {
    static let shared = CoreDataStack()

    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "Prayers")
        let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.xxx.test.xxx.test")!.appendingPathComponent("Prayers.sqlite")
        let description = NSPersistentStoreDescription(url: storeURL)
            description.shouldMigrateStoreAutomatically = true
            description.shouldInferMappingModelAutomatically = true
        
        container.persistentStoreDescriptions = [description]
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error {
                fatalError("Unresolved error \(error)")
            }
        })
        return container
    }()
}

struct widgetsEntryView : View {
    
    var entry: Provider.Entry
    @State private var lineY:CGFloat = 0
    @State var zz:CDPrayer!
    @State var zzzzzzzz:String =  ":))"
    
    var body: some View {
        
        ZStack{
            
            GeometryReader { proxy in
                
                HStack(alignment: .center, spacing: 0){
                    
                    if zz != nil {
                        prayerBiteVertical(prayer:zz,proxy:proxy,lineY : $lineY)
                    }else{
                        Text(zzzzzzzz)
                            .font(.system(size: 9))
                    }
                }
                
            } .coordinateSpace(name: "_geo")
                .onChange(of: entry.date) { oldValue, newValue in
                loadDate()
            }.onAppear(){
                loadDate()
            }
            
        }
        
    }
    
    func loadDate(){
        
        let managedObjectContext = CoreDataStack.shared.persistentContainer.viewContext

        let fetchRequest  = CDPrayer.fetchRequest()
        
        do{
            let prayers =  try managedObjectContext.fetch(fetchRequest) 
        
            zzzzzzzz = "ok \(prayers)"
            
            //They r 5 prayers we r good
            if prayers.count == 5 {
                zz = prayers.filter{$0.prayer == "Fajr"}[0]
            }
        
        } catch let error as NSError {
            zzzzzzzz = "[PrayerTimes][WIDGET] Could not fetch. \(error), \(error.userInfo)"
        }
       
    }
   
}


struct prayerBiteVertical : View  {
    
    @State var prayer:CDPrayer
    @State var proxy:GeometryProxy!
    @Binding var lineY:CGFloat
    @State var btn:btton_themes = .normal
    
    //Keys
    @State var key_prayer:String = ""
    
    var body: some View {
       
        ZStack{
            
            Color("highlightBg")
            
            GeometryReader{ g in
        
                Color.clear.onAppear(){
                    lineY = g.frame(in: .named("_geo")).minY
                }
                
            }.frame(height :0.1)
            
            VStack{
                
                Text(key_prayer)
                    .font(.poppins(.Regular,size: 15))
                    .minimumScaleFactor(0.85)
                
                Image(systemName: "sunset.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width : 25)
                    .frame(height : 20)
               
                Text("11:11")
                    .font(.poppins(.Regular,size: 18))
                    .minimumScaleFactor(0.85)
                
                Spacer()
            }
            
          
        }.frame(width : proxy.frame(in: .local).width * 0.2)
        .onChange(of: prayer) { oldValue, newValue in
            
            key_prayer = prayer.prayer!
            
        }
        
    }
    
}


struct widget_Medium: Widget {
    
    let kind: String = "widgets"
    var body: some WidgetConfiguration {
        
        AppIntentConfiguration(kind: kind, provider: Provider()) 
        { entry in
            
            widgetsEntryView(entry: entry)
                .containerBackground( .clear, for: .widget)
         
        }.supportedFamilies([.systemMedium])
         .containerBackgroundRemovable(true)
         .configurationDisplayName("My Widget")
         .description("This is an example widget.")
         
        
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationAppIntent
}


struct Provider: AppIntentTimelineProvider {
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
    }

    func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: configuration)
    }
 
    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry>
    {
        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 ..< 60 
        {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
                entries.append(entry)
        }

        return Timeline(entries: entries, policy: .atEnd)
    }
}

#Preview(as: .systemMedium) {
    widget_Medium()
} timeline: {
    SimpleEntry(date: .now, configuration: ConfigurationAppIntent())
}

PS : I've followed almost every topic about his issue in SOF and apple community and I've tried almost every solution of trying to read CoreData from app widget all my attempts failed with empty results [] from the context . when I am 100% sure the database is full of records .


Solution

  • Setting the Target Membership is not enough. This will only expose the entities to the Widget code. It doesn´t share the underlying database with the Widget extension.

    You need to set up an app group to be the same for both, the application and the extension. Then create the database url for the appgroup and use this database in both widget and app.

    e.g.:

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "TestCoreData")
        let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "app.group.identifier")?.appendingPathComponent("databasename.sqlite")
        let storeDescription = NSPersistentStoreDescription(url: url!)
        container.persistentStoreDescriptions = [storeDescription]
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
    

    But be aware: This database will be empty. If you already have data in your previous database you will have to migrate.