The way of iOS to implement Diffable Data Source obviously has a major flaw.
It is not able to detect, when the content changed and position moved happen at the same time on an item.
If we look how Android implements their version of "Diffable Data Source" - DiffUtil. It comes with 2 functions
areContentsTheSame(int oldItemPosition, int newItemPosition) - Called by the DiffUtil when it wants to check whether two items have the same data.
and
areItemsTheSame(int oldItemPosition, int newItemPosition) - Called by the DiffUtil to decide whether two object represent the same Item.
As pointed out by yetanotherme , besides the ability to detect content change, iOS's Diffable Data Source need a way to distinguish whether 2 items are having same identify.
Same analogy is... Yesterday my hair color is black, today my hair color is white (Content change. In Android, this is detected using areContentsTheSame)
But, Yesterday me and today me are still referring to the same me (Same identify. In Android, this is detected using areItemsTheSame)
Post
Replies
Boosts
Views
Activity
It seems that this issue is resolved in Xcode 13 beta - https://bugs.swift.org/browse/SR-14715
For additional information,
Testing environment
XCode Version 14.0 (14A309)
macOS Monterey Version 12.6 (M1)
Simulator iPhone 13, iOS 15
Hi DaleOne, thank you for sharing such an important information. For iOS 15, do you think the mythology described in https://www.avanderlee.com/swift/diffable-data-sources-core-data/ still accurate? Do we still need to call reloadItems explicitly? Recently, we have encountered classic NSInternalInconsistencyException for some clients, by using classic performBatchUpdates. We was thinking migrate to NSDiffableDataSourceSnapshotReference. But, we aren't sure it will solve the prob. Thank you!
I have discovered the following solution. It works great both for Taiwan and Germany as per testing. Hopefully, it works fine for other locales as well. If not, please let me know the mistake. Thank you.
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .short
let amSymbol = dateFormatter.amSymbol
let pmSymbol = dateFormatter.pmSymbol
if amSymbol != "" || pmSymbol != "" {
print("12-hour format")
} else {
print("24-hour format")
}
I have discovered the following solution. It works great both for Taiwan and Germany as per testing. Hopefully, it works fine for other locales as well. If not, please let me know the mistake. Thank you.
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .short
let amSymbol: String? = dateFormatter.amSymbol
let pmSymbol: String? = dateFormatter.pmSymbol
guard let amSymbol = amSymbol, let pmSymbol = pmSymbol else {
print("24-hour format")
}
let dateAsString = dateFormatter.string(from: Date())
if dateAsString.contains(amSymbol) || dateAsString.contains(pmSymbol) {
print("12-hour format")
} else {
print("24-hour format")
}
This is the video recording to show interactive widget is not working after an 8 hours overnight.
This is the video recording to show interactive widget is working again, after launching and closing the main app. The main app however will call WidgetCenter.shared.reloadAllTimelines(). Hence, I am not sure whether main app relaunching is causing widget to work again, or WidgetCenter.shared.reloadAllTimelines() is causing widget to work again.
I am using iOS version 17.1.1, iPhone SE to perform testing.
To avoid complications when dealing with CloudKit, here is the strategy I am using:
In the main app, I use a CoreDataStack with CloudKit enabled. The StoreDescription.cloudKitContainerOptions is not nil.
In the widget extension, I use a CoreDataStack with CloudKit disabled. The StoreDescription.cloudKitContainerOptions is nil.
As a result, synchronization across multiple devices only occurs when the user launches the main app. If the user doesn't launch the main app, synchronization will not happen.
When there's a crash in WidgetKit, it often goes unnoticed by developers and users because there's no clear indication within the widget itself. In contrast, a crash in the main app is more apparent because it causes the app to close suddenly.
One theory, as discussed in this https://developer.apple.com/forums/thread/668649, is that the main app and the widget are attempting to perform a migration simultaneously.
However, I'm skeptical of this theory because our last CoreData migration began last year. Therefore, it's surprising to see a high number of crashes after a year.
Here's where fatal error occurs. It is a pretty standard CoreData stack used in widget.
private static func setupNSPersistentContainer() -> NSPersistentContainer {
precondition(Thread.isMainThread)
let container = NSPersistentCloudKitContainer(name: "***", managedObjectModel: NSManagedObjectModel.***)
// Integrate AppGroup with CoreData. If we are assigning a new store description to CoreData, it is important
// to finish all store description initialization, before assigning. If not, CloudKit will not work.
let storeURL = AppGroup.wenote.containerURL.appendingPathComponent(Constants.WENOTE_SQLITE)
let newStoreDescription = NSPersistentStoreDescription(url: storeURL)
// Turn on persistent history tracking.
newStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
newStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
newStoreDescription.cloudKitContainerOptions = nil
// Finalize store description assigning.
container.persistentStoreDescriptions = [newStoreDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
Here's the info from XCode's Organizer.
Here's the crash log
Thread 0 name:
Thread 0 Crashed:
0 libswiftCore.dylib 0x000000018d371870 _assertionFailure(_:_:file:line:flags:) + 264 (AssertCommon.swift:144)
1 sticky-note-widgetExtension 0x0000000104dcb890 closure #1 in static CoreDataStack.setupNSPersistentContainer() + 376 (CoreDataStack.swift:76)
2 sticky-note-widgetExtension 0x0000000104dcb8e4 thunk for @escaping @callee_guaranteed (@guaranteed NSPersistentStoreDescription, @guaranteed Error?) -> () + 80 (<compiler-generated>:0)
3 CoreData 0x000000019c14a0ec -[NSPersistentStoreCoordinator _doAddPersistentStoreWithDescription:privateCopy:completeOnMainThread:withHandler:] + 668 (NSPersistentStoreCoordinator.m:1402)
4 CoreData 0x000000019c149df8 -[NSPersistentStoreCoordinator addPersistentStoreWithDescription:completionHandler:] + 228 (NSPersistentStoreCoordinator.m:1448)
5 CoreData 0x000000019c149bf4 -[NSPersistentContainer _loadStoreDescriptions:withCompletionHandler:] + 184 (NSPersistentContainer.m:291)
6 CoreData 0x000000019c219ff0 -[NSPersistentCloudKitContainer _loadStoreDescriptions:withCompletionHandler:] + 536 (NSPersistentCloudKitContainer.m:163)
7 CoreData 0x000000019c0b852c -[NSPersistentContainer loadPersistentStoresWithCompletionHandler:] + 248 (NSPersistentContainer.m:274)
8 sticky-note-widgetExtension 0x0000000104dcbdb0 specialized static CoreDataStack.setupNSPersistentContainer() + 992 (CoreDataStack.swift:73)
9 sticky-note-widgetExtension 0x0000000104dcb5f8 static CoreDataStack.setupNSPersistentContainer() + 4 (CoreDataStack.swift:23)
10 sticky-note-widgetExtension 0x0000000104dcb5f8 closure #1 in CoreDataStack.persistentContainer.getter + 4 (<compiler-generated>:0)
11 sticky-note-widgetExtension 0x0000000104dcb5f8 CoreDataStack.persistentContainer.getter + 36
12 sticky-note-widgetExtension 0x0000000104dcb5a0 CoreDataStack.viewContext.getter + 28
13 sticky-note-widgetExtension 0x0000000104db2058 specialized static NSPlainNoteRepository.getFirstStickyNoteWithoutTrash() + 68 (NSPlainNoteRepository.swift:54)
14 sticky-note-widgetExtension 0x0000000104da4828 static NSPlainNoteRepository.getFirstStickyNoteWithoutTrash() + 8 (NSPlainNoteRepository.swift:22)
15 sticky-note-widgetExtension 0x0000000104da4828 static NSPlainNoteRepository.getStickyNote(_:) + 8 (sticky_note_widget.swift:15)
16 sticky-note-widgetExtension 0x0000000104da4828 Provider.placeholder(in:) + 64 (<compiler-generated>:0)
17 sticky-note-widgetExtension 0x0000000104da4828 protocol witness for IntentTimelineProvider.placeholder(in:) in conformance Provider + 104
18 WidgetKit 0x00000001a2c65a50 IntentTimelineEntryProvider.placeholder(for:with:) + 528 (IntentConfiguration.swift:258)
19 WidgetKit 0x00000001a2c6a2a0 protocol witness for EntryProviding.placeholder(for:with:) in conformance IntentTimelineEntryProvider<A, B> + 24 (<compiler-generated>:0)
20 WidgetKit 0x00000001a2b56ef4 closure #1 in closure #2 in WidgetExtensionXPCServer.ExportedObject.getPlaceholders(requests:completion:) + 2172 (WidgetExtensionXPCServer.swift:250)
21 WidgetKit 0x00000001a2b5659c closure #2 in WidgetExtensionXPCServer.ExportedObject.getPlaceholders(requests:completion:) + 164 (WidgetExtensionXPCServer.swift:219)
22 WidgetKit 0x00000001a2c64054 thunk for @escaping @callee_guaranteed () -> () + 36 (<compiler-generated>:0)
So far, we are not able to reproduce such a crash locally.
We manage to have a temporary workaround, by changing "never refresh" timeline
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
let nsTodoList = NSTodoListRepository.INSTANCE.getNSTodoList(configuration)
let entry = SimpleEntry(
date: currentDate,
nsTodoList: nsTodoList,
configurationIntent: configuration
)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
to "refresh every 1 hour"
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
let nextUpdateDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
let nsTodoList = NSTodoListRepository.INSTANCE.getNSTodoList(configuration)
let entry = SimpleEntry(
date: currentDate,
nsTodoList: nsTodoList,
configurationIntent: configuration
)
entries.append(entry)
// Provide data entry snapshot for "now". This getTimeline function will be triggered again after 1 hour.
let timeline = Timeline(entries: entries, policy: .after(nextUpdateDate))
completion(timeline)
}
However, this seems like a bug to me because an interactive widget is supposed to function properly even if it isn't refreshed for an extended period.
Does anyone know where I can report this issue to Apple? Thank you.
I would recommend using an image loading library - https://github.com/onevcat/Kingfisher , which is designed to work with images in an optimized way.