0

Recently, I wanted to add a widget to my app that fetches some data from Core Data in the main app. Based on my search, I wrote the following code, but I encountered the following three problems:


The first problem is that I added and modified data in the app, but the widget did not update.


The second problem is that when I run the widget directly on my phone, it displays blank with no error reported.


The third problem is that the original data disappears.


How can I solve these problems?
 Below is the code used to update the widget:

@main
struct MyMainApp: App {  
  @Environment(\.scenePhase) private var scenePhase

  var body: some Scene {
    WindowGroup {
      MyMainAppTabView()
        .environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
        .onChange(of: scenePhase) { newScenePhase in
          if newScenePhase == .background {
            WidgetCenter.shared.reloadAllTimelines()
          }
       }
    }
  }
}

The following is my PersistenceController, mainly intended to migrate Core Data to app groups.

struct PersistenceController {
  static let shared = PersistenceController()
  
  static var preview: PersistenceController = {
    let result = PersistenceController(inMemory: true)
    let viewContext = result.container.viewContext
    
    return result
  }()
  
  let container: NSPersistentCloudKitContainer
  
  init(inMemory: Bool = false) {
    container = NSPersistentCloudKitContainer(name: "Database")
    
    let storeURL = URL.storeURL(for: "group.main.data", databaseName: "Database")
    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 {
      let storeDescription = NSPersistentStoreDescription(url: storeURL)
      storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.MyMainApp")
      container.persistentStoreDescriptions = [storeDescription]
    }
    
    if inMemory {
      container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    }
    
    container.viewContext.automaticallyMergesChangesFromParent = true
    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, withType: NSSQLiteStoreType)
          } catch {
            print("migration store failed: \(error.localizedDescription)")
          }
          
          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("delete old store failed: \(error.localizedDescription)")
            }
          })
        }
      }
    })
  }
}

The following is the code related to the widget:

struct Provider: TimelineProvider {
  
  let moc = PersistenceController.shared.container.viewContext
  
  func placeholder(in context: Context) -> SimpleEntry {
    return SimpleEntry(date: Date(), items: [])
  }
  
  func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    let entry = SimpleEntry(date: Date(), items: [])
    completion(entry)
  }
  
  func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var entity = SimpleEntry(date: Date(), items: [])
    do {
      let result = try moc.fetch(Item.fetchNearExpirationItems())
      entity = SimpleEntry(date: Date(), items: result)

    } catch {
      print("fetch near Expiration items failed: \(error.localizedDescription)")
    }

    
    let timeline = Timeline(entries: [entity], policy: .atEnd)
    completion(timeline)
  }
}

struct SimpleEntry: TimelineEntry {
  let date: Date
  let items: [Item]
}

struct NearExpirationEntryView : View {
  var entry: Provider.Entry
    
  var body: some View {
    VStack {
      HStack(spacing: .zero) {
        Icon(systemName: "hourglass.tophalf.filled",size: .small)
          .foregroundColor(Color("primary"))
        Text("Expiration")
        
        Spacer(minLength: .zero)
        
        Text("\(entry.items.count)")
          .foregroundColor(Color("onPrimary"))
          .padding(UIConstants.padding4)
          .background(ContainerRelativeShape().fill(Color("primary")))
      }
      .font(UIConstants.font13Medium)

      ForEach(entry.items){ item in
        ItemCellView(name: item.name, count: item.count ?? 0)
      }
      
      Spacer(minLength: .zero)
    }
    .padding(.all)
  }
}

struct NearExpiration: Widget {
  let kind: String = "NearExpiration"
  let contenxt = PersistenceController.shared.container.viewContext

  var body: some WidgetConfiguration {
    StaticConfiguration(kind: kind, provider: Provider()) { entry in
      NearExpirationEntryView(entry: entry)
        .environment(\.managedObjectContext, contenxt)
    }
    .configurationDisplayName("My Widget")
    .description("This is an example widget.")
    .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
  }
}
IDTIMW
  • 217
  • 2
  • 14

0 Answers0