5

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.

udondan
  • 57,263
  • 20
  • 190
  • 175

4 Answers4

5

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.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Thanks Tom. You're right, it doesn't matter for the lifecycle if you return `.NoData` or `.NewData`. But this might be new since Xcode 6.2, I recently updated and now I can not reproduce the original issue any longer. This is for OS X, not iOS, so things might be slightly different, e.g. I also tested the PID thingy and it always is a new PID as long as you close the notification center and give it some seconds before you re-open it. – udondan Mar 18 '15 at 07:43
  • 1
    I found [this answer](http://stackoverflow.com/a/28639621/2753241) by @bencallis and it sounds like the snapshot is only used for animations, e.g. when you remove/add the widget or on iOS when you pull down the notification center. I tested this on OS X and it seems to be correct. When you send `.NewData` a snapshot is created, which appears to be an image which is used for animating the widget. If you return `.NoData` no snapshot is created and this would lead to animating an outdated representation of the widget. – udondan Mar 18 '15 at 08:00
2

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.

  • Thank you for your answer. It's not targeting the problem I described though. I know when my data changed. The problem was that when my data did not change and I called the `completionHandler` with `.NoData` the widget would end up completely empty in some cases. I just tried to create a small dummy application to demonstrate the problem, though it's gone now - I can not reproduce the behavior I experienced when I wrote this question. – udondan Mar 18 '15 at 07:12
  • There was an Xcode update (6.2) in the meantime, maybe Apple has changed something in the libraries. Or it might be I simply did something stupid before in my project. The current behavior seems to be it doesn't matter if one returns `.NoData` or `.NewData` at all. – udondan Mar 18 '15 at 07:12
  • @Danilo Torrisi: isn't already synchronized the access to the shared user defaults and thread safe? – valeCocoa Sep 21 '17 at 09:19
1

The simplest way to do this is to cache your current widgets state in user defaults. Then when the widget loads (in viewDidLoad) fetch your cached data from user defaults and set it as a property on your widget view controller.

Then when widgetPerformUpdateWithCompletionHandler is called you have your previous updated and you can decide if you need to do a network request of if your data is fresh enough. If you do a web request you can then compare it to your cache to determine if you have new data or no update.

bencallis
  • 3,478
  • 4
  • 32
  • 58
  • Thank you for your answer. It's not targeting the problem I described though. I know when my data changed. The problem was that when my data did not change and I called the `completionHandler` with `.NoData` the widget would end up completely empty in some cases. I just tried to create a small dummy application to demonstrate the problem, though it's gone now - I can not reproduce the behavior I experienced when I wrote this question. – udondan Mar 18 '15 at 07:12
  • There was an Xcode update (6.2) in the meantime, maybe Apple has changed something in the libraries. Or it might be I simply did something stupid before in my project. The current behavior seems to be it doesn't matter if one returns `.NoData` or `.NewData` at all. – udondan Mar 18 '15 at 07:13
  • [Another answer of yours](http://stackoverflow.com/a/28639621/2753241) though helped me to understand what snapshots really are. I think the issue I experienced can actually not be related to whatever was passed to `completionHandler`. I must have done something else terribly wrong before to get that behavior. That other answer is so good I wished I had placed a bounty on that question. Maybe you want to update here. :) See as well my comments on Toms answer here. – udondan Mar 18 '15 at 08:04
0

No need to persist any data in UserDefaults or even Core Storage. Keep it simple: Just declare the variable(s) you use to store any calculated contents or data you use to check whether something changed to be static. Since the widget runs in the same process (as shown here) the static data will be available the next time your widget is activated.

    static NSDate *lastDate;

    - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
        NSDate * currentDate = [NSDate date];
        if (!lastDate ||
            [[NSCalendar currentCalendar]compareDate:lastDate toDate:currentDate toUnitGranularity:NSCalendarUnitDay] != NSOrderedSame) {
            // Date changed 
            lastDate = currentDate;
            ... do whatever needs to be done ...
            completionHandler(NCUpdateResultNewData);
        } else {
            completionHandler(NCUpdateResultNoData);
        }
   }
Community
  • 1
  • 1
vilmoskörte
  • 291
  • 1
  • 10
  • So how in this way the container application will be able to signal that it eventually has made changes to data? The most simple implementation I've found was to store a flag in the shared user defaults: if it's set to YES (or nil for first launch), then I'll set it to NO and call the completion handler with NCUpdateResultNewData (updating the widget view prior calling the completion handler). Otherwise if the flag is NO than I'll just call the completion handler with NCUpdateResultNoData. – valeCocoa Sep 01 '17 at 14:04