Error while running widgetkit extension target on physical device - ios

Added a WidgetBuilder to my extension so I could include a second type of widget for my app. I keep getting this error in a popup when building it onto my device.
Details
SendProcessControlEvent:toPid: encountered an error: Error Domain=com.apple.dt.deviceprocesscontrolservice Code=8 "Failed to show Widget 'com.identifier' error: Error Domain=SBAvocadoDebuggingControllerErrorDomain Code=2 "Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'" UserInfo={NSLocalizedDescription=Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'}." UserInfo={NSLocalizedDescription=Failed to show Widget 'com.identifiert' error: Error Domain=SBAvocadoDebuggingControllerErrorDomain Code=2 "Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'" UserInfo={NSLocalizedDescription=Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'}., NSUnderlyingError=0x12f915290 {Error Domain=SBAvocadoDebuggingControllerErrorDomain Code=2 "Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'" UserInfo={NSLocalizedDescription=Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'}}}
Domain: DTXMessage
Code: 1
--
Heres the before and after code.
Before adding the second widget:
import WidgetKit
import SwiftUI
import Foundation
let testBook = Book(id: UUID(), name: "Name", author: "Author", genre: "Error", page: "0", total: "77")
public enum AppGroup: String {
case Livre = "group.com.idetifier"
public var containerURL: URL {
switch self {
case .Livre:
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: self.rawValue)!
}
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), book: testBook)
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), book: testBook)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
let books: [Book] = self.load("list")
let entry = SimpleEntry(date: currentDate, book: books.randomElement() ?? testBook)
let timeline = Timeline(entries: [entry], policy: .after(entryDate))
completion(timeline)
}
func load<T: Decodable>(_ filename: String) -> T {
let groupDirectory = AppGroup.Livre.containerURL
let groupURL = groupDirectory
.appendingPathComponent(filename)
.appendingPathExtension("json")
return try! JSONDecoder().decode(T.self, from: Data(contentsOf: groupURL))
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let book: Book
}
struct PlaceholderView: View {
var body: some View {
RandomWidgetView(book: testBook)
.redacted(reason: .placeholder)
}
}
struct LivreWidgetEntryView : View {
#Environment(\.widgetFamily) var family
var entry: Provider.Entry
#ViewBuilder
var body: some View {
switch family {
case .systemSmall:
RandomWidgetView(book: entry.book, size: .small)
case .systemMedium:
RandomWidgetView(book: entry.book, size: .medium)
case .systemLarge:
RandomWidgetView(book: entry.book, size: .large)
default:
RandomWidgetView(book: entry.book, size: .small)
}
}
}
#main
struct LivreWidget: Widget {
let kind: String = "LivreWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
LivreWidgetEntryView(entry: entry)
}
.configurationDisplayName("Progress of a random book")
.description("This widget shows the progress from a random book in your Library")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
and after adding the second widget:
import WidgetKit
import SwiftUI
import Foundation
#main
struct LivreWidgetBuilder: WidgetBundle {
init() {}
#WidgetBundleBuilder
var body: some Widget {
RandomWidget()
QuoteWidget()
}
}
let testBook = Book(id: UUID(), name: "Name", author: "Author", genre: "Error", page: "0", total: "77", quotes: ["Quote"])
public enum AppGroup: String {
case Livre = "group.com.identifier"
public var containerURL: URL {
switch self {
case .Livre:
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: self.rawValue)!
}
}
}
//MARK: Random book widget
struct RandomProvider: TimelineProvider {
func placeholder(in context: Context) -> RandomSimpleEntry {
RandomSimpleEntry(date: Date(), book: testBook)
}
func getSnapshot(in context: Context, completion: #escaping (RandomSimpleEntry) -> ()) {
let entry = RandomSimpleEntry(date: Date(), book: testBook)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
let books: [Book] = self.load("list")
let entry = RandomSimpleEntry(date: currentDate, book: books.randomElement() ?? testBook)
let timeline = Timeline(entries: [entry], policy: .after(entryDate))
completion(timeline)
}
func load<T: Decodable>(_ filename: String) -> T {
let groupDirectory = AppGroup.Livre.containerURL
let groupURL = groupDirectory
.appendingPathComponent(filename)
.appendingPathExtension("json")
return try! JSONDecoder().decode(T.self, from: Data(contentsOf: groupURL))
}
}
struct RandomSimpleEntry: TimelineEntry {
let date: Date
let book: Book
}
struct RandomPlaceholderView: View {
var body: some View {
RandomWidgetView(book: testBook)
.redacted(reason: .placeholder)
}
}
struct RandomWidgetEntryView : View {
#Environment(\.widgetFamily) var family
var entry: RandomProvider.Entry
#ViewBuilder
var body: some View {
switch family {
case .systemSmall:
RandomWidgetView(book: entry.book, size: .small)
case .systemMedium:
RandomWidgetView(book: entry.book, size: .medium)
case .systemLarge:
RandomWidgetView(book: entry.book, size: .large)
default:
RandomWidgetView(book: entry.book, size: .small)
}
}
}
struct RandomWidget: Widget {
let kind: String = "RandomWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: RandomProvider()) { entry in
RandomWidgetEntryView(entry: entry)
}
.configurationDisplayName("Random book progress")
.description("This widget shows the progress from a random book in your Library")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
//MARK: Quote Widget
struct QuoteProvider: TimelineProvider {
func placeholder(in context: Context) -> QuoteSimpleEntry {
QuoteSimpleEntry(date: Date(), book: testBook)
}
func getSnapshot(in context: Context, completion: #escaping (QuoteSimpleEntry) -> ()) {
let entry = QuoteSimpleEntry(date: Date(), book: testBook)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
let books: [Book] = self.load("list")
let entry = QuoteSimpleEntry(date: currentDate, book: books.randomElement() ?? testBook)
let timeline = Timeline(entries: [entry], policy: .after(entryDate))
completion(timeline)
}
func load<T: Decodable>(_ filename: String) -> T {
let groupDirectory = AppGroup.Livre.containerURL
let groupURL = groupDirectory
.appendingPathComponent(filename)
.appendingPathExtension("json")
return try! JSONDecoder().decode(T.self, from: Data(contentsOf: groupURL))
}
}
struct QuoteSimpleEntry: TimelineEntry {
let date: Date
let book: Book
}
struct QuotePlaceholderView: View {
var body: some View {
QuoteWidgetView(book: testBook)
.redacted(reason: .placeholder)
}
}
struct QuoteWidgetEntryView : View {
#Environment(\.widgetFamily) var family
var entry: QuoteProvider.Entry
var body: some View {
QuoteWidgetView(book: entry.book, size: .small)
}
}
struct QuoteWidget: Widget {
let kind: String = "QuoteWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: QuoteProvider()) { entry in
QuoteWidgetEntryView(entry: entry)
}
.configurationDisplayName("Quote widget")
.description("This widget shows a random quote from a random book in your Library")
.supportedFamilies([.systemMedium])
}
}
The widgets seem to work, but I'm trying to find out why i'm getting this popup error.

Related

Widget element disappears in Widget Gallery - Swift

I have a weird issue I'm not sure how to resolve. I have a weather app that displays Celsius and Fahrenheit at the same time, as well as an SF symbol for the weather condition.
Everything in the widget works perfectly on it's own. However, in the widget gallery, the SF symbol will disappear from the widget after about half a second and I have no idea why.
Here's all the code from the widget. It's my first attempt at one, so it's not very clean:
import WidgetKit
import SwiftUI
let otherUserDefaults = UserDefaults(suiteName: "group.DualTemp")
let temperatureCelsius = otherUserDefaults?.value(forKey: "CTempWidget") ?? 0
let temperatureFahrenheit = otherUserDefaults?.value(forKey: "FTempWidget")
let name = otherUserDefaults?.value(forKey: "SearchedCityWidget")
let weatherID = otherUserDefaults?.string(forKey: "WidgetIcon")
let weather = UIImage(systemName: weatherID!)
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), name: "", temperatureCelsius: "0°C", temperatureFahrenheit: "0°F", weatherName: "Cloud", weatherIcon: weather!)
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), name: name as! String , temperatureCelsius: temperatureCelsius as! String, temperatureFahrenheit: temperatureFahrenheit as! String, weatherName: "sun", weatherIcon: weather!)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let userDefaults = UserDefaults(suiteName: "group.DualTemp")
let temperatureCelsius = userDefaults?.value(forKey: "CTempWidget") ?? 0
let temperatureFahrenheit = userDefaults?.value(forKey: "FTempWidget")
let name = userDefaults?.value(forKey: "SearchedCityWidget")
let weatherName = userDefaults?.string(forKey: "WidgetIcon")
let weatherImage = UIImage(systemName: weatherName!)
//MARK: - refresh info
let totalCountdown = 30
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for offset in 0 ..< totalCountdown {
let entryDate = Calendar.current.date(byAdding: .minute, value: offset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, name: name as? String ?? "No location", temperatureCelsius: temperatureCelsius as? String ?? "0°C", temperatureFahrenheit: temperatureFahrenheit as? String ?? "0°F", weatherName: weatherName!,
weatherIcon: weatherImage!)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let name: String
let temperatureCelsius: String
let temperatureFahrenheit: String
let weatherName: String
let weatherIcon: UIImage
}
struct DualTempWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
ZStack{
Color("WidgetBackground")
VStack(spacing: 0.5) {
if #available(iOSApplicationExtension 16.0, *) {
Text(entry.name)
.padding(6)
.bold()
.font(.system(size: 27))
.minimumScaleFactor(0.01)
.lineLimit(1)
} else {
Text(entry.name)
.padding(6)
.font(.system(size: 29))
.minimumScaleFactor(0.01)
.lineLimit(1)
// Fallback on earlier versions
}
Image(systemName: entry.weatherName)
.resizable()
.symbolRenderingMode(.palette)
.scaledToFit()
.frame(width: 70, height: 70)
HStack{
Text(entry.temperatureCelsius)
.font(.system(size: 22))
Text("|")
Text(entry.temperatureFahrenheit)
.font(.system(size: 22))
}
}
}
}
}
struct DualTempWidget: Widget {
let kind: String = "DualTempWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
DualTempWidgetEntryView(entry: entry)
}
.configurationDisplayName("Dual Temp")
.description("See the current temperature, in celsius and fahrenheit, and weather conditions for your location.")
.supportedFamilies([.systemSmall])
}
}
struct DualTempWidget_Previews: PreviewProvider {
static var previews: some View {
DualTempWidgetEntryView(entry: SimpleEntry(date: Date(), name: name as! String , temperatureCelsius: temperatureCelsius as! String, temperatureFahrenheit: temperatureFahrenheit as! String, weatherName: weatherID!, weatherIcon: weather!))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
Everything looks as it should be as everything in the widget updates correctly when you search for a new place in the app itself. Weird.

Implementing In App Purchases in IOS Widget

I'm currently stuck trying to implement in app purchases within my IOS Widget. My widget works, the in-app purchase works, and the app works for the baseline functions.
I am unable to make the purchases available within the widget. My current strategy is to use if else statements to turn change with parts of code to point to.
Could anyone tell me how to implement in app purchases within my Widget?
Here is my code:
code
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
let userDefaults = UserDefaults(suiteName: "group.QuotePackage")
func placeholder(in context: Context) -> SimpleEntry {
_ = Quote.self
if UserDefaults.standard.bool(forKey: "premiumQuotes") == true {
// remove adds
return SimpleEntry(date: Date(), quoteDetails: QuoteProvider.random())
} else {
return SimpleEntry(date: Date(), quoteDetails: QuoteProvider2.random2())
}
}
//let userDefaults = UserDefaults(suiteName: "group.QuotePackage")
public typealias Entry = SimpleEntry
public func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
if UserDefaults.standard.bool(forKey: "premiumQuotes") == true {
// remove adds
let entry = SimpleEntry(date: Date(), quoteDetails: QuoteProvider.random())
} else {
let entry = SimpleEntry(date: Date(), quoteDetails: QuoteProvider2.random2())
}
completion(entry)
}
public func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> Void) {
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 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
if UserDefaults.standard.bool(forKey: "premiumQuotes") == true {
// remove adds
let entry = SimpleEntry(date: entryDate, quoteDetails: QuoteProvider.random())
} else {
let entry = SimpleEntry(date: entryDate, quoteDetails: QuoteProvider2.random2())
}
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct Quote_WidgetEntryView: View {
var entry: Provider.Entry
var body: some View {
QuoteWidgetEntryView(quoteDetails: entry.quoteDetails)
}
}
struct SimpleEntry: TimelineEntry {
public let date: Date
public let quoteDetails: QuoteDetails
}
struct PlaceholderView : View {
var body: some View {
Text("Inspo")
}
}
struct QuoteWidgetEntryView : View {
let quoteDetails: QuoteDetails
var body: some View {
ZStack {
Image("background")
.resizable()
.aspectRatio(contentMode: .fill)
VStack (alignment: .center, spacing: 15) {
Text("\"\(quoteDetails.texts)\"")
.font(Font.custom("Georgia", size: 20))
.fontWeight(.light)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.lineLimit(5)
.padding(.leading, 20)
.padding(.trailing, 20)
Text("\(quoteDetails.authors)")
.font(Font.custom("Georgia", size: 15))
.fontWeight(.light)
.foregroundColor(Color.white)
}
}
}
}
#main
struct Quote2_Widget: Widget {
private let kind: String = "Quote2_Widget"
public var body: some WidgetConfiguration {
if UserDefaults.standard.bool(forKey: "premiumQuotes") == true {
// remove adds
StaticConfiguration(kind: kind, provider: Provider()) { entry in
QuoteWidgetEntryView(quoteDetails: QuoteProvider.random())
} else {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
QuoteWidgetEntryView(quoteDetails: QuoteProvider.random())
}
}
.configurationDisplayName("Inspo")
.description("Add Inspo Quotes to your homescreen.")
.supportedFamilies([.systemMedium])
}
}
}
struct QuoteWidgetView_Previews: PreviewProvider {
static var previews: some View {
QuoteWidgetEntryView(quoteDetails: QuoteProvider.random())
.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}

Pass data entered in a UIView to a Widget

I try to implement a simple pattern on Widgets:
get a textField in the app;
have the Widget updated when textField is changed.
App is UIKit (not SwiftUI).
I've read here that I could pass it through UserDefaults,
How to pass uiviewcontroller data to swiftui widget class
and also through a shared singleton. I tried but couldn't make it work.
What I tried:
Create a singleton to hold the data to pass:
class Util {
class var shared : Util {
struct Singleton {
static let instance = Util()
}
return Singleton.instance;
}
var globalToPass = "Hello"
}
shared the file between the 2 targets App and WidgetExtension
In VC, update the singleton when textField is changed and ask for widget to reload timeline
#IBAction func updateMessage(_ sender: UITextField) {
Util.shared.globalToPass = valueToPassLabel.text ?? "--"
WidgetCenter.shared.reloadTimelines(ofKind: "WidgetForTest")
WidgetCenter.shared.reloadAllTimelines()
}
Problem : Widget never updated its message field
Here is the full widget code at this time:
import WidgetKit
import SwiftUI
struct LoadStatusProvider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), loadEntry: 0, message: Util.shared.globalToPass)
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), loadEntry: 0, message: Util.shared.globalToPass)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for minuteOffset in 0 ..< 2 {
let entryDate = Calendar.current.date(byAdding: .minute, value: 5*minuteOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, loadEntry: minuteOffset, message: Util.shared.globalToPass)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let loadEntry: Int
let message: String
}
struct WidgetForTestNoIntentEntryView : View {
var entry: LoadStatusProvider.Entry
var body: some View {
let formatter = DateFormatter()
formatter.timeStyle = .medium
let dateString = formatter.string(from: entry.date)
return
VStack {
Text(String(entry.message))
HStack {
Text("Started")
Text(entry.date, style: .time)
}
HStack {
Text("Now")
Text(dateString)
}
HStack {
Text("Loaded")
Text(String(entry.loadEntry))
}
}
}
}
#main
struct WidgetForTestNoIntent: Widget {
let kind: String = "WidgetForTestNoIntent"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: LoadStatusProvider()) { entry in
WidgetForTestNoIntentEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct WidgetForTestNoIntent_Previews: PreviewProvider {
static var previews: some View {
WidgetForTestNoIntentEntryView(entry: SimpleEntry(date: Date(), loadEntry: 0, message: "-"))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

iOS 14 Widget not update

I'm trying to build an IOS 14 Widget, that updates every two minutes. This is the widget code:
import WidgetKit
import SwiftUI
import Intents
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let entry1 = SimpleEntry(date: Date(), configuration: configuration)
let timeline = Timeline(entries: [entry1], policy: .after(Date().addingTimeInterval(2*60.0)))
print("timeline: \(timeline)")
completion(timeline)
}
}
struct TeamWidgetEntryView : View {
#Environment(\.widgetFamily) var family
var entry: Provider.Entry
#ViewBuilder
var body: some View {
switch family {
case .systemSmall:
SmallView(date: Date())
case .systemMedium:
MediumView()
case .systemLarge:
LargeView()
default:
Text("Some other WidgetFamily in the future.")
}
}
}
#main
struct TeamWidget: Widget {
let kind: String = "TeamWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
TeamWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall,.systemMedium,.systemLarge])
}
}
This is the SmallView class:
struct SmallView: View {
var date: Date
static let taskDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter
}()
var body: some View {
VStack {
Text("Small View")
Text("\(Date(), formatter: Self.taskDateFormat)")
}
}
}
I want that the Widget will update every 2 minutes but it's not happening, any idea what is the problem?
I was also facing the same problem and tried every update time from 10 seconds to 10 minutes and it seems that it starts updating the content only from 5 minutes
this is what I use to update my widget every minute
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries = [SimpleEntry]()
let currentDate = Date()
let midnight = Calendar.current.startOfDay(for: currentDate)
let nextMidnight = Calendar.current.date(byAdding: .day, value: 1, to: midnight)!
for offset in 0 ..< 60 * 24 {
let entryDate = Calendar.current.date(byAdding: .minute, value: offset, to: midnight)!
entries.append(SimpleEntry(date: entryDate))
}
let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
completion(timeline)
}

How to ensure WidgetKit view shows correct results from #FetchRequest?

I have an app that uses Core Data with CloudKit. Changes are synced between devices. The main target has Background Modes capability with checked Remote notifications. Main target and widget target both have the same App Group, and both have iCloud capability with Services set to CloudKit and same container in Containers checked.
My goal is to display actual Core Data entries in SwiftUI WidgetKit view.
My widget target file:
import WidgetKit
import SwiftUI
import CoreData
// MARK: For Core Data
public extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}
var managedObjectContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
var workingContext: NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = managedObjectContext
return context
}
var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Countdowns")
let storeURL = URL.storeURL(for: "group.app-group-countdowns", databaseName: "Countdowns")
let description = NSPersistentStoreDescription(url: storeURL)
container.loadPersistentStores(completionHandler: { storeDescription, error in
if let error = error as NSError? {
print(error)
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return container
}()
// MARK: For Widget
struct Provider: TimelineProvider {
var moc = managedObjectContext
init(context : NSManagedObjectContext) {
self.moc = context
}
func placeholder(in context: Context) -> SimpleEntry {
return SimpleEntry(date: Date())
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
return completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct CountdownsWidgetEntryView : View {
var entry: Provider.Entry
#FetchRequest(entity: Countdown.entity(), sortDescriptors: []) var countdowns: FetchedResults<Countdown>
var body: some View {
return (
VStack {
ForEach(countdowns, id: \.self) { (memoryItem: Countdown) in
Text(memoryItem.title ?? "Default title")
}.environment(\.managedObjectContext, managedObjectContext)
Text(entry.date, style: .time)
}
)
}
}
#main
struct CountdownsWidget: Widget {
let kind: String = "CountdownsWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(context: managedObjectContext)) { entry in
CountdownsWidgetEntryView(entry: entry)
.environment(\.managedObjectContext, managedObjectContext)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct CountdownsWidget_Previews: PreviewProvider {
static var previews: some View {
CountdownsWidgetEntryView(entry: SimpleEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
But I have a problem: let's say I have 3 Countdown records in the main app:
At the start widget view shows 3 records as expected in preview (UI for adding a widget). But after I add a widget to the home screen, it does not show Countdown rows, only entry.date, style: .time. When timeline entry changes, rows not visible, too. I made a picture to illustrate this better:
Or:
At the start widget view shows 3 records as expected, but after a minute or so, if I delete or add Countdown records in the main app, widget still shows initial 3 values, but I want it to show the actual number of values (to reflect changes). Timeline entry.date, style .time changes, reflected in the widget, but not entries from request.
Is there any way to ensure my widget shows correct fetch request results? Thanks.
Widget views don't observe anything. They're just provided with TimelineEntry data. Which means #FetchRequest, #ObservedObject etc. will not work here.
Enable remote notifications for your container:
let container = NSPersistentContainer(name: "DataModel")
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
Update your CoreDataManager to observe remote notifications:
class CoreDataManager {
var itemCount: Int?
private var observers = [NSObjectProtocol]()
init() {
fetchData()
observers.append(
NotificationCenter.default.addObserver(forName: .NSPersistentStoreRemoteChange, object: nil, queue: .main) { _ in
// make sure you don't call this too often - notifications may be posted in very short time frames
self.fetchData()
}
)
}
deinit {
observers.forEach(NotificationCenter.default.removeObserver)
}
func fetchData() {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Item")
do {
self.itemCount = try CoreDataStack.shared.managedObjectContext.count(for: fetchRequest)
WidgetCenter.shared.reloadAllTimelines()
} catch {
print("Failed to fetch: \(error)")
}
}
}
Add another field in the Entry:
struct SimpleEntry: TimelineEntry {
let date: Date
let itemCount: Int?
}
Use it all in the Provider:
struct Provider: TimelineProvider {
let coreDataManager = CoreDataManager()
...
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> Void) {
let entries = [
SimpleEntry(date: Date(), itemCount: coreDataManager.itemCount),
]
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
}
Now you can display your entry in the view:
struct WidgetExtEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.date, style: .time)
Text("Count: \(String(describing: entry.itemCount))")
}
}
}

Resources