ios 14 widget: How to reload timeline after UserDefaults changed - ios

I have a ios 14 widget that refresh every 5 minutes
let timeline = Timeline(entries: entries, policy: .atEnd)
The entries depends on the configuration on my MainApp.
I use UserDefaults to share data between MainApp and Widget.
#AppStorage("FollowingCatalog", store: UserDefaults(suiteName: "group.vn.f19.com"))
var catalogItemsData: Data = Data()
I've successfully mirrored the widget content base on UserDefaults data. BUT my problem is the my widget refresh the UI only after .atEnd policy, every 5 minutes
That cause a bad UX
How can I refresh widget content immediately right after my configuration in UserDefaults was changed?
Thanks for your supports.

To clarify the whole workflow, I add some note here:
You can call WidgetCenter not only from widget but from your main app.
WidgetCenter.shared.reloadAllTimelines()
WidgetCenter.shared.reloadTimelines(ofKind: "com.myApp.myWidget........")
So the flow is:
User change something on MainApp, eg: I rearrange the stock items in the following list
MainApp save the data to UserDefaults, eg: save the list item order
MainApp trigger reload widget by:
WidgetCenter.shared.reloadTimelines(ofKind: "mone24h_widget")
You can get the kind by looking into your widget entry file:
#main
struct mone24h_widget: Widget {
let kind: String = "mone24h_widget"
Widget will reload the timeline, I will get the shared data from UserDefaults here. Done the work. Eg: Re-render my stocks list base on the arrangement passed from MainApp

In Keeping a Widget Up to Date by Apple, to inform your widgets to update its timeline and its content, you call:
WidgetCenter.shared.reloadAllTimelines()
This reloads all widgets that your app has. If you want to reload a specific widget (in the case your app has multiple different widget types), use WidgetCenter.shared.reloadTimelines(ofKind: "com.myApp.myWidget") instead.

BTW, if you're using react-native, I've already created a module to support this issue:
react-native-widget-bridge

Related

How to clear installed widgets on iOS app

In last version of iOS app we have implemented widget feature, but in the coming release we have decided to remove the widget extension.
If the user has already added widget to his Home Screen and when ever user updates his/her application with latest code that does not contain widget extension, still user is able to view blank widget.
Is there any way to clear/remove widgets programatically from main app.
If you set the supportedFamilies modifier on the WidgetConfiguration to [] then the widget no longer shows in the widget gallery. That might be an option.
public var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: Provider()
) { entry in
widget_mainEntryView(entry: entry)
}
.configurationDisplayName("Foo")
.description("Foo")
.supportedFamilies([])
}

Sharing UserDefaults between main app and Widget in iOS 14

I am writing a widget for iOS, seems UserDefaults is not accessible in the widget, I added app group as following, still it's nil:
let prefs = UserDefaults(suiteName:"group.myco.myapp")
Still when I try to read something from prefs which I set in the main app, it's nil here.
You'll need to add the AppGroup capability to the widget target. Select the project, then select the widget target, go to the Signing & Capabilities tab, and click the + Capability button. Then choose AppGroup and configure it with your group.
As #Ilya Muradymov commented blow
You need to set data for corresponding group UserDefault first if you want to get the data between widget and container app:
//object-C
[[NSUserDefaults.alloc initWithSuiteName:#"group.com.your.groupname"] setObject:mObject forKey:#"xxxxxxxx"];
//swift
UserDefaults(suiteName:"group.com.your.groupname").set(mObject, forKey: "xxxxxxxx")

NSFetchedResultsController not seeing inserts made by extension

I have an iOS app structured like this
Main Application (the main iOS app)
Intents Extension (Siri integration)
Shared Framework (shared library for interacting with Core Data. This allows both the main application and the intents extension to use the same Core Data store)
My issue is that when I insert something into Core Data using the Intents Extension, it doesn't appear in the Main Application's UITableView until I manually refresh the fetchedResultsController like this:
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: "myCache")
try? fetchedResultsController.performFetch()
tableView.reloadData()
Is there a way to make the fetchedResultsController see the changes without having to manually refresh everything?
Note: If I insert something into core data from the Main Application, the fetchedResultsController automatically sees the change and updates the table (like expected)
To share a database between an app and extension you need to implement Persistent History Tracking. For an introduction see WWDC 2017 What's New in Core Data at 20:49 and for sample code see the documentation Consuming Relevant Store Changes.
The basic idea is to enable the store option NSPersistentHistoryTrackingKey, observe NSPersistentStoreRemoteChangeNotification, upon being notified you should fetch the changes using NSPersistentHistoryChangeRequest and then merge into the context using mergeChangesFromContextDidSaveNotification and transaction.objectIDNotification. Your NSFetchedResultsController will then update accordingly.
This is normal because the application extension and the main application are not working in the same process.
There are some ways to update the data in the main application
NSPersistentStoreRemoteChangeNotification
UserDefaults(suitename:)
Darwin Notifications
I'm using UserDefaults and refreshAllObjects function for the viewContext.
Example:
func sceneDidBecomeActive(_ scene: UIScene) {
let defaults = UserDefaults(suiteName:"your app group name")
let hasChange = defaults?.bool(forKey: "changes")
if hasChange ?? false {
refreshAllObjects()
defaults?.set(false, forKey: "changes")
}
}
refresh all objects function is like this:
viewContext.perform {
viewContext.stalenessInterval = 0.0
viewContext.refreshAllObjects()
viewContext.stalenessInterval = -1
}

CoreStore how to observe changes in database

I need to observe changes of an Entity after import occurred.
Currently I have next logic:
Save Entity with temp identifier (NSManagedObject.objectId) to local core data storage.
Send Entity to the server via Alamofire POST request.
Server generates JSON and reply with the almost the same Entity details but with modified identifier which was NSManagedObject.objectId previously. So the local one Entity id will be updated with server id.
Now when I received new JSON I do transaction.importUniqueObjects.
At this step I want to inform my datasource about changes. And refetch data with updated identifiers.
So my DataSource has some Entities in an array, and while I use this datasource to show data it's still static information in that array which I fetched before, but as you see on the step number 4 I already updated core data storage via CoreStore import and want DataSource's array to be updated too.
I found some information regarding ListMonitor in CoreStore and tried to use it. As I can see this method works when update comes
func listMonitorDidChange(_ monitor: ListMonitor)
but I try to refetch data somehow. Looks like monitor already contains some most up to date info.
but when I do this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
let entities = try? CoreStore.fetchAll(
From<MyEntity>()
.orderBy(.ascending(\.name))
) // THERE IS STILL old information in database, but monitor instance shows new info.
}
And then code became like this:
func listMonitorDidChange(_ monitor: ListMonitor<MyEntity>) {
var myEntitiesFromMonitor = [MyEntity]()
for index in 0...monitor.numberOfObjects() {
myEntitiesFromMonitor.append(monitor[index])
}
if myEntitiesFromMonitor.count > 0 {
// HERE we update DataSource
updateData(with: myEntitiesFromMonitor)
}
}
not sure if I am on the right way.
Please correct me if I am wrong:
As I understood each time core data gets updated with new changes, monitor gets updated as well. I have not dive deep into it how this was made, via some CoreData context notification or whatever but after you do something via CoreStore transaction, such as create or update or delete object or whatever you want, monitor gets update. Also it has callback functions that you need to implement in your class where you want to observe any changes with data model:
Your classes such as datasource or some service or even some view controller (if you don't use any MVVP or VIPER or other design patterns) need to conform to ListObserver protocol in case you want to listen not to just one object.
here are that functions:
func listMonitorDidChange(monitor: ListMonitor<MyPersonEntity>) {
// Here I reload my tableview and this monitor already has all needed info about sections and rows depend how you setup monitor.
// So you classVariableMonitor which I provide below already has up to date state after any changes with data.
}
func listMonitorDidRefetch(monitor: ListMonitor<MyPersonEntity>) {
// Not sure for which purposes it. I have not received this call yet
}
typealias ListEntityType = ExerciseEntity
let classVariableMonitor = CoreStore.monitorSectionedList(
From<ListEntityType>()
.sectionBy(#keyPath(ListEntityType.muscle.name)) { (sectionName) -> String? in
"\(String(describing: sectionName)) years old"
}
.orderBy(.ascending(\.name))
.where(
format: "%K == %#",
#keyPath(ListEntityType.name),
"Search string")
)
All other thing documented here so you can find info how to extract info from monitor in your tableview datasource function.
Thanks #MartinM for suggestion!

Determine if the widget is enabled

Is there any way to determine if my Today Widget is already added to Notification Centre by user? I need to know so I can change some Labels in host app accordingly.
There is no API for that, but you could have your today widget write something to the shared container that you can read from your app to determine if it's been displayed. The main problems with that are that it won't happen until the widget has been displayed at least once, and you can't relly tell if they've installed and then removed it.
func widgetHasRun() {
if let sharedContainer = NSUserDefaults(suiteName: "group.com.my.app") {
sharedContainer.setBool(true, forKey: "today widget installed")
sharedContainer.synchronize()
}
}
We use this technique to determine whether we should prompt new users to install our widget.

Resources