3

I have a widget that shows me core data stuff data I modify in the app. All I want the widget to do is to refresh whenever the core data database in the main app refreshes.

Here is my widget

//
//  RemindMeWidget.swift
//  RemindMeWidget
//
//  Created by Charel Felten on 05/07/2021.
//

import WidgetKit
import SwiftUI
import Intents
import CoreData



struct Provider: IntentTimelineProvider {
  func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(
      date: Date(),
      configuration: ConfigurationIntent(),
      notes: Note.previewNotes,
      realFamily: context.family
    )
  }
  
  func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    let entry = SimpleEntry(
      date: Date(),
      configuration: configuration,
      notes: Note.previewNotes,
      realFamily: context.family
    )
    completion(entry)
  }
  
  func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
    print("get timeline is called")
    
    var notes: [Note] = []
    
    let containerURL = PersistenceController.containerURL
    let storeURL = containerURL.appendingPathComponent(PersistenceController.SQLiteStoreAppendix)
    let description = NSPersistentStoreDescription(url: storeURL)
    let container = NSPersistentCloudKitContainer(name: PersistenceController.containerName)
    
    container.persistentStoreDescriptions = [description]
    
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
    
    let viewContext = PersistenceController.shared.container.viewContext
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Note")
    do {
      let result = try viewContext.fetch(request)
      // is there a nicer way to do it?
      if let tmp = result as? [Note] {
        notes = tmp
      }
    } catch {
      let nsError = error as NSError
      fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
    }
    
    notes = notes.sorted { (lhs, rhs) in
      if lhs.int16priority == rhs.int16priority {
        return lhs.timestamp! > rhs.timestamp!
      }
      return lhs.int16priority > rhs.int16priority
    }
    
    let entry = SimpleEntry(
      date: Date(),
      configuration: configuration,
      notes: notes,
      realFamily: context.family
    )
    print("hey")
    let timeline = Timeline(entries: [entry], policy: .atEnd)
    completion(timeline)
  }
}



struct SimpleEntry: TimelineEntry {
  let date: Date
  let configuration: ConfigurationIntent
  var notes: [Note]
  var realFamily: WidgetFamily
}



struct RemindMeWidgetEntryView : View {
  var entry: Provider.Entry
  
  var body: some View {
    WidgetView(notes: entry.notes, realFamily: entry.realFamily)
  }
}



@main
struct RemindMeWidget: Widget {
  let kind: String = "RemindMeWidget"
  
  var body: some WidgetConfiguration {
    IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
      RemindMeWidgetEntryView(entry: entry)
    }
    .configurationDisplayName("My Widget")
    .description("This is an example widget.")
  }
}



struct RemindMeWidget_Previews: PreviewProvider {
  static var previews: some View {
    RemindMeWidgetEntryView(
      entry: SimpleEntry(
        date: Date(),
        configuration: ConfigurationIntent(),
        notes: Note.previewNotes,
        realFamily: .systemSmall
      )
    ).previewContext(WidgetPreviewContext(family: .systemSmall))
  }
}

In the app, I have a settings page where I call a manual widget refresh (for debugging for now) like this:

Button("Reload Widget") {
            print("calling widgetcenter")
            WidgetCenter.shared.reloadAllTimelines()
            WidgetCenter.shared.getCurrentConfigurations({result in print(result)})
          }

Full code:

//
//  SettingsView.swift
//  RemindMe
//
//  Created by Charel Felten on 02/07/2021.
//

import SwiftUI
import WidgetKit

struct SettingsView: View {
  
  @Environment(\.colorScheme) var colorScheme
  @ObservedObject var config: Config
  
  var body: some View {
    NavigationView {
      Form {
        ForEach(Priority.allRegularCases) { priority in
          PriorityView(config: config, priority: priority)
        }
        Section(header: Text("Theme")) {
          Picker(selection: $config.colorTheme, label: Text("Theme")) {
            ForEach(ColorTheme.allCases, id: \.self) { value in
              Text(value.rawValue).tag(value)
            }
          }
        }
        Section(header: Text("Additional Info"), footer: Text("Toggle which additional information to show below each note in the list.")) {
          Toggle(isOn: $config.showNotificationTime) {
            Text("Show reminders")
          }
          Toggle(isOn: $config.showCreationTime) {
            Text("Show date")
          }
          Button("Reload Widget") {
            print("calling widgetcenter")
            WidgetCenter.shared.reloadAllTimelines()
            WidgetCenter.shared.getCurrentConfigurations({result in print(result)})
          }
        }
      }
      .navigationTitle("Settings")
    }
    // save the settings if we leave the app (goes away from foreground)
    .onReceive(
      NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification),
      perform: { _ in config.save() }
    )
    .onDisappear(perform: Note.updateAllNotes)
  }
}

struct PriorityView: View {
  @Environment(\.colorScheme) var colorScheme
  @ObservedObject var config: Config
  var priority: Priority
  
  var body: some View {
    Section(header: Text(priority.getDescription()).foregroundColor(Color.secondary)) {
      Picker(selection: $config.priorityIntervals[priority.getIndex()], label: Text("Notification interval")) {
        ForEach(Interval.allCases, id: \.self) { value in
          Text(value.rawValue).tag(value)
        }
      }
      switch config.priorityIntervals[priority.getIndex()] {
      case .ten_minutes, .never:
        EmptyView()
      default:
        DatePicker("Reminders at", selection: $config.priorityDates[priority.getIndex()], displayedComponents: [.hourAndMinute])
      }
    }
    .foregroundColor(Colors.getColor(for: priority, in: .primary))
    .listRowBackground(
      ZStack {
        colorScheme == .dark ? Color.black : Color.white
        Colors.getColor(for: priority, in: .background)
      }
    )
  }
}

struct SettingsView_Previews: PreviewProvider {
  static var previews: some View {
    SettingsView(
      config: Config()
    )
  }
}

Clicking this button prints the following to my console:

calling widgetcenter
success([WidgetInfo:
- configuration: <ConfigurationIntent: 0x282e51dd0> {
}
- family: systemSmall
- kind: RemindMeWidget, WidgetInfo:
- configuration: <ConfigurationIntent: 0x282e51e60> {
}
- family: systemLarge
- kind: RemindMeWidget, WidgetInfo:
- configuration: <ConfigurationIntent: 0x282e51ef0> {
}
- family: systemMedium
- kind: RemindMeWidget])

The widget however is not refreshed. It keeps looking like this:

enter image description here

Sometimes I did refresh, but then when I made changes in the app, the widget did not update. None of the print statements that I put in getTimeline are being printed. I am really exhausted and not sure what I did wrong...

I am not sure how to supply a smaller reproducible example, but I just have not idea whatsoever where the issue could come from.

I have read multiple related questions but none of them have helped:

If you want to take a look at the code, here it is:

https://github.com/charelF/RemindMeApp/tree/main/RemindMeWidget


Update

It's also not working in the simulator.

I pushed all my changes to GitHub, see https://github.com/charelF/RemindMeApp

enter image description here

halfer
  • 19,824
  • 17
  • 99
  • 186
charelf
  • 3,103
  • 4
  • 29
  • 51
  • Make sure that core data is being stored in a common location such as an app group – lorem ipsum Mar 05 '23 at 14:07
  • I dont think thats the issue. Followed these instructions here: https://stackoverflow.com/a/63945538/9439097 And the widget used to work. I think i just misconfigured something with the intents. Thanks for the help though, I appreciate it! – charelf Mar 05 '23 at 14:36
  • My only other suggestion would be to stop ignoring errors and checking line by line. You can use “Result” to actually display errors. https://stackoverflow.com/questions/75236895/how-do-i-use-weatherkit-swift-framework-to-fetch-the-weather-inside-a-widget-ext/75237499#75237499 – lorem ipsum Mar 05 '23 at 15:08

3 Answers3

1

Your code looks good to me, with one exception:
In your RemindMeWidget.intentdefinition, you define a custom intent "Configuration", but this intent has no parameters.
According to the docu, Xcode generates then the following custom intent:

public class ConfigurationIntent: INIntent {
}  

Since your intent has no parameters, this class does not have any @NSManaged public var.

The docu continues:

When WidgetKit calls getSnapshot(for:in:completion:) or getTimeline(for:in:completion:), it passes an instance of your custom INIntent, configured with the user-selected details. The game widget provider accesses the properties of the intent and includes them in the TimelineEntry. WidgetKit then invokes the widget configuration’s content closure, passing the timeline entry to allow the views to access the user-configured properties.

My wild guess is, that WidgetKit is confused because it does not find any parameter to pass.

I suggest that you test your code first without intents, and if this works, add a dummy parameter to your intent.
Maybe this helps...

Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116
  • I just deleted this intent but that seems to not have been the right choice, now my code is complaining that the intent is missing. Unfortuntely im not sure if i will have to fix this and see if your idea works before the bounty expires. What do you suggest I do, regarding the bounty? – charelf Mar 16 '23 at 14:56
  • Instead of the `IntentTimelineProvider`, just use a `TimelineProvider` and its functions `placeholder(in context: Context)`, `getSnapshot(in context: Context, completion: @escaping (ToBuyItemsTimelineEntry)`, and `getTimeline(in context: Context, completion: @escaping (Timeline)`. Then, no intents are used. You should do this anyway, if you don't need intents. Good luck! – Reinhard Männer Mar 16 '23 at 15:32
  • I have to admit, i did not know this was a thing - that is, a timelineprovider without intents. I implemented it, unfortunately it is still not working. Probably a good catch nonetheless, i dont seem to need intents so probably better to work without them. – charelf Mar 16 '23 at 16:04
  • Hmmm... Since you have your project on GitHub, I will try to look into it when I have time. – Reinhard Männer Mar 16 '23 at 16:30
  • Unfortunately, it is not so easy to get your code running (correct provisioning profiles..., etc.). What I did not mention in my previous comment is that you also have to replace in your `WidgetConfiguration` the `IntentConfiguration` by a `StaticConfiguration`. If you do so, does your project build without errors? If not, which errors are reported? Or does it run? If so, is anything interesting in the logs? – Reinhard Männer Mar 16 '23 at 16:48
  • I did that, it runs without errors, can check the logs if i see anyhing in there – charelf Mar 17 '23 at 07:20
0

It is as intent. In Apple's document, it says:

Reloading widgets consumes system resources and causes battery drain due to additional networking and processing. To reduce this performance impact and maintain all-day battery life, limit the frequency and number of updates you request to what’s necessary.

To manage system load, WidgetKit uses a budget to distribute widget reloads over the course of the day. The budget allocation is dynamic and takes many factors into account

A widget’s budget applies to a 24-hour period. WidgetKit tunes the 24-hour window to the user’s daily usage pattern, which means the daily budget doesn’t necessarily reset at exactly midnight. For a widget the user frequently views, a daily budget typically includes from 40 to 70 refreshes. This rate roughly translates to widget reloads every 15 to 60 minutes, but it’s common for these intervals to vary due to the many factors involved.

Keeping a Widget Up To Date

Owen Zhao
  • 3,205
  • 1
  • 26
  • 43
  • Thanks, but I do understand how widgets work (to some degree at least). I get that you can not update your widgets all the time. However, various sources online say that whenever your app is in the foreground, reloading the widget does **not** count against your allocated quota of reloads. But even if, it surely is not apples intent that your widget never ever loads and displays data for the first time, so surely something is wrong with my specific implementation and I want to know what. – charelf Mar 14 '23 at 09:13
  • 1
    I think your issue is not update the widget. It is if the widget is correctly implemented. I ran your app in my SE 3 and the widget shows empty list when the main app has items. So it is not an updating issue, the widget doesn't read the item at all. – Owen Zhao Mar 15 '23 at 22:22
  • I am starting to think it may be an issue with the way i am sharing my core data between app and widget, i will look more into this. – charelf Mar 16 '23 at 14:45
0

I managed to get it working by refactoring the code with ChatGPT a bit. I still don't know what the exact error was.

What I changed

  • I switched to a static intents, thanks Reinhard
  • I removed my core data retrieval logic from the timelineprovider to a separate function, that is then called by the timelineprovider, and also the getSnapchot function.
  • some changes to my widget view

What I think was my bug (or at least one bug, seems to be the one that Owen reported, i.e. my widget showing an empty list always):

In my widgetview, I only want to show the first 4 notes since I can not fit more in the widget. I used this code for that:

SystemWidgetView(notes: Array(notes[..<4]))

ChatGPT instead recommended this code to only show 4 notes:

SystemWidgetView(notes: Array(notes.prefix(4)))

I'm still not sure why one is wrong and the other isn't, but that seemed to prevent my widgets from showing. However, I managed to get them to show sometimes even with my old way of doing it, so I think there were additional bugs. ChatGPT also recommended to pass around the entry object instead of the notes list, not sure if that has an impact.

Another bug may have been that I had code like this in my view file:

extension WidgetFamily: EnvironmentKey {
    public static var defaultValue: WidgetFamily = .systemMedium
}



extension EnvironmentValues {
  var widgetFamily: WidgetFamily {
    get { self[WidgetFamily.self] }
    set { self[WidgetFamily.self] = newValue }
  }
}

I'm not exactly sure what it did, but I think it may have had an impact as well. I removed it.


Anyway, the widget is working. Below is the working code:

RemindMeWidget.swift

//
//  RemindMeWidget.swift
//  RemindMeWidget
//
//  Created by Charel Felten on 05/07/2021.
//

import WidgetKit
import SwiftUI
import CoreData

struct MyWidgetProvider: TimelineProvider {
  typealias Entry = MyWidgetEntry
  
  func placeholder(in context: Context) -> MyWidgetEntry {
    MyWidgetEntry(date: Date(), notes: [])
  }
  
  func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) -> Void) {
    let entry = MyWidgetEntry(date: Date(), notes: fetchNotes())
    completion(entry)
  }
  
  func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) -> Void) {
    let entry = MyWidgetEntry(date: Date(), notes: fetchNotes())
    let timeline = Timeline(entries: [entry], policy: .atEnd)
    completion(timeline)
  }
  
  private func fetchNotes() -> [Note] {
    var notes: [Note] = []
    let containerURL = PersistenceController.containerURL
    let storeURL = containerURL.appendingPathComponent(PersistenceController.SQLiteStoreAppendix)
    let description = NSPersistentStoreDescription(url: storeURL)
    let container = NSPersistentCloudKitContainer(name: PersistenceController.containerName)
    container.persistentStoreDescriptions = [description]
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
    let viewContext = PersistenceController.shared.container.viewContext
    let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Note")
    do {
      let result = try viewContext.fetch(request)
      if let tmp = result as? [Note] {
        notes = tmp
      }
    } catch {
      let nsError = error as NSError
      fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
    }
    notes = notes.sorted { (lhs, rhs) in
      if lhs.int16priority == rhs.int16priority {
        return lhs.timestamp! > rhs.timestamp!
      }
      return lhs.int16priority > rhs.int16priority
    }
    return notes
  }
}

struct MyWidgetEntry: TimelineEntry {
    let date: Date
    let notes: [Note]
}

@main
struct MyWidget: Widget {
    let kind: String = "RemindMeWidget"
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: MyWidgetProvider()) { entry in
          WidgetView(entry: entry)
        }
        .configurationDisplayName("RemindMe Widget")
        .description("Your most important reminders at a glance.")
        .supportedFamilies([
          .systemSmall,
          .systemMedium,
          .systemLarge,
          .systemExtraLarge,
          .accessoryCircular,
          .accessoryRectangular,
          .accessoryInline,
      ])
    }
}

WidgetView.swift

//
//  WidgetView.swift
//  RemindMe
//
//  Created by Charel Felten on 06/07/2021.
//

import SwiftUI
import WidgetKit

struct WidgetView: View {
  var entry: MyWidgetProvider.Entry
  
  @Environment(\.colorScheme) var colorScheme
  @Environment(\.widgetFamily) var widgetFamily
  
  var body: some View {
    switch widgetFamily {
    case .systemSmall, .systemMedium:
      SystemWidgetView(notes: Array(entry.notes.prefix(4)))
    case .systemLarge, .systemExtraLarge:
      SystemWidgetView(notes: Array(entry.notes.prefix(4)))
    default:
      Text(String(entry.notes.count))
    }
  }
}

struct SystemWidgetView: View {
  var notes: [Note]
  @Environment(\.colorScheme) var colorScheme
  
  var body: some View {
    ZStack {
      colorScheme == .dark ? Color.black : Color.white
      Rectangle()
        .fill(colorScheme == .dark ? Color.black : Color.white)
        .overlay {
          VStack(alignment: .leading, spacing: 5) {
            ForEach(notes, id: \.self) { note in
              Text(note.content!)
                .fontWeight(.medium)
                .font(.callout)
                .foregroundColor(note.getPrimaryColor())
                .frame(maxWidth:.infinity, maxHeight: .infinity, alignment: .leading)
                .padding(.vertical, 5)
                .padding(.horizontal, 10)
                .background(note.getWidgetBackgroundColor())
                .cornerRadius(5)
            }
          }
        }
        .cornerRadius(15)
        .padding(5)
    }
  }
}

struct WidgetView_Previews: PreviewProvider {
  // NOTE: if it crahes its this bug: https://www.appsloveworld.com/coding/xcode/66/fail-to-preview-widget-in-xcode-with-swiftui-preview
  // FIX: the membership of this class must
  // be only the widgeet target, no other target
  static var previews: some View {
    WidgetView(
      entry: MyWidgetEntry(
        date: Date(),
        notes: Note.previewNotes
      )
    )
    .previewContext(WidgetPreviewContext(family: .systemSmall))
  }
}

Full code:

https://github.com/charelF/RemindMeApp/tree/ace8ed293bf1e7f3af673e16a1e3076c5edd64c8


Lastly, I would like to thank @Owen Zhao and @Reinhold Männer. I appreciate your answers. Thanks for your contribution!

halfer
  • 19,824
  • 17
  • 99
  • 186
charelf
  • 3,103
  • 4
  • 29
  • 51