Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OS X Today Widget - How to detect in NCWidgetProviding.widgetPerformUpdateWithCompletionHandler if content has changed?

widgetPerformUpdateWithCompletionHandler() gives us the possibility to let the Notification Center know if the content of a Today Extension has changed. For example:

func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
    // Refresh the widget's contents in preparation for a snapshot.
    // Call the completion handler block after the widget's contents have been
    // refreshed. Pass NCUpdateResultNoData to indicate that nothing has changed
    // or NCUpdateResultNewData to indicate that there is new data since the
    // last invocation of this method.

    if(has_new_data()) { // There was an update
        completionHandler(.NewData)
    }
    else { // nothing changed!
        completionHandler(.NoData)
    }
}

But how would we know if the content has changed? On every snapshot the widget is instantiated from scratch. It is a complete new process with new PID. So you can not store any property in your instance. How would one compare the current widget content with the content of the previous snapshot?

I used Core Data to store the current content for later comparison. This is obvious and works. But then another problem crashes in: What if there is no previous snapshot? Let's say the user removed the widget just to re-add it again. Or the user rebooted. There might be more reasons why there is no previous snapshot that I can not think of now. Either way - there still is content stored in Core Data. If the comparison between this old content and the current content detects there are no changes and I return .NoData the widget would end up empty because the Notification Center would not redraw the content.

You might wonder why It is so important to me to call the completionHandler with a correct state and not simply always return .NewData. That's because I am experiencing a little flicker when there is no change and still return .NewData. I have some images in my widget and when redrawing the widget the whole content gets invisible for a millisecond - long enough to notice.

Is there something I am missing? It seems strange to me that Apple provides a way to give us the option to respond with different states but then makes it impossible to detect which state we should respond.

like image 618
udondan Avatar asked Jan 27 '15 03:01

udondan


2 Answers

In theory you would use this to check whether there was any new content since the last call, and pass back the appropriate value. I suppose in theory it might be the same instance of the view controller on more than one call, but that's clearly not how things work right now. Checking whether content has changed depends on the nature of the app and the extension. Since you're using a shared Core Data store to share data, you might do something like:

  1. Any time the app changes data, save the current date in shared user defaults.
  2. Any time the today extension reads data, save the current date in shared user defaults.
  3. When widgetPerformUpdateWithCompletionHandler is called, look up both of those dates. If the data has changed more recently than the last time the extension read that data, return NCUpdateResultNewData. If not, return NCUpdateResultNoData.

You could also save these dates in the metadata on the persistent store instead of in shared user defaults. If it was the same view controller each time, you might keep the value from step 2 in memory instead of saving it to a file. But again that's not how it works now, and it's not clear when or if that will change.

Apps that save data in some other way might need to use different checks, the details of which would depend on how their app and extension worked.

In practice with iOS 8.2 it really doesn't matter because the extension environment doesn't seem to care what value you send back. I tried returning NCUpdateResultNewData and compared it to returning NCUpdateResultNoData every time. There was absolutely no effect on the life cycle of the today extension view controller or its views.

As an aside, it's not always a different process. I tried putting this line in the today extension's viewDidLoad:

NSLog(@"viewDidLoad pid=%d self=%@", getpid(), self);

Then I ran the today extension, scrolled up and down in the notification center a couple of times, closed the notification center, reopened it, and got the following:

2015-03-17 15:19:01.203 DemoToday[3484:903442] viewDidLoad pid=3484 self=<TodayViewController: 0x12d508cc0>
2015-03-17 15:19:14.441 DemoToday[3484:903442] viewDidLoad pid=3484 self=<TodayViewController: 0x12d50ade0>
2015-03-17 15:19:23.784 DemoToday[3484:903442] viewDidLoad pid=3484 self=<TodayViewController: 0x12d619c40>
2015-03-17 15:19:29.015 DemoToday[3484:903442] viewDidLoad pid=3484 self=<TodayViewController: 0x12d50abe0>

Although it's a different instance of the view controller each time, it's the same pid in each of these cases.

like image 67
Tom Harrington Avatar answered Sep 23 '22 18:09

Tom Harrington


You can use any storage API ( Core Data, NSCoding, SQLite ) as long as you enable data sharing with your host app.

https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html

Even if the user removes the widget or reboots the device, the data will be stored on your shared container. Whenever iOS launches your widget, you will be able to read and write the same shared container that you have previously used.

Be aware that your host app and your widget may read or write concurrently so you have to coordinate any reading and writing operation.

like image 24
Danilo Torrisi Avatar answered Sep 21 '22 18:09

Danilo Torrisi