NSUserDefaultsDidChangeNotification and Today Extensions
Asked Answered
Z

1

16

I am developing an iPhone app with a Today Extension. The app has a Model module that loads from/saves toNSUserDefaults. Since I want this information to be available to both the main app and the extension, I use an app group:

let storage = NSUserDefaults(suiteName: "group.etc.etc.etc...")

Both the app and the extension can access the information without any problem.

The main app occasionally might create a local notification to present to the user. That notification has two actions associated with it (UIUserNotificationAction). One of those actions triggers some code run on the background on the main app. That code changes the NSUserDefaults information and triggers a synchronization. My code is something like this:

func application(application: UIApplication, handleActionWithIdentifier id: String?, forLocalNotification not: UILocalNotification, completionHandler: () -> ()) {
    // Interact with model here
    // New information gets saved to NSUserDefaults
    userDefaultsStorage.synchronize()
    completionHandler()
}

Now, on the Today Ext. I naturally observe any changes made to the information on NSUserDefaults so that I can reload the interface on the widget:

override func viewDidLoad() {
    super.viewDidLoad()

    // ...

    NSNotificationCenter.defaultCenter().addObserverForName(NSUserDefaultsDidChangeNotification, object: nil, queue: NSOperationQueue.mainQueue()) { _ in
        self.reload()
    }
}

Now, here's my issue:

  1. The main app schedules a UILocalNotification. I open the today view and look at my today widget.

  2. When the notification fires, a banner appears on the top of the screen.

  3. I slide down on that banner to reveal the two actions and I select the one that I mentioned earlier (the today widget is still live and on screen).

I know for a fact that the action runs correctly in the background, and that the changes are being made to the information on NSUserDefaults.

However, even though the today widget has been active and on screen all this time, no reload action is triggered. After further investigation, I can confirm that the NSUserDefaultsDidChangeNotification is not being fired (I placed a breakpoint and it did not trigger, and did some other checks as well).

I know the changes are being made by the notification action because if I force a reload of the widget (by closing and opening the today view) the widget updates correctly.

I have seen various tutorials online where the first thing they say is to listen to this notification and update the widget so that "the widget is in sync with NSUserDefaults". But the thing is that AFAICT this notification is absolutely useless! How come??


Note 1: When I change the information on NSUserDefaults from within the today widget the notification fires correctly.

Note 2: Debugging a today widget is absolutely horrible, btw. It is always necessary to tell Xcode to "Attach to process by name..." before it can react to breakpoints and crashes. And iOS is constantly creating a new process for the widget so I have to constantly tell Xcode to attach again.

Zuber answered 2/2, 2015 at 19:12 Comment(5)
Why did you put the notification registration in viewDidLoad and not the initializer?Rancho
@Rancho Well, for no specific reason; it's just where I usually do it. But in this case it doesn't matter because the notification action happens way after viewDidLoad has been called on the today widget's view controller.Zuber
Why do you want to manually reload the widget? Whenever the widget is being shown to the screen, the OS is requesting an update from the widget via the widgetPerformUpdateWithCompletionHandler, so you could just get the new values from the user defaults there and update there. Also you could try if KVO worksRingnecked
Maybe off topic, but if you want to communicate between extension and its containing app, take a look at this project: github.com/cxa/AppExtensionCommunicator. Disclaimer: I created this project.Illgotten
@Illgotten i've downloaded it but didn't workContract
R
14

From doc here:

Cocoa includes two types of notification centers: The NSNotificationCenter class manages notifications within a single process. The NSDistributedNotificationCenter class manages notifications across multiple processes on a single computer.

Apparently the containing app and today extension are different processes, since when you debug today extension you want to attach containing app process, but NSNotificationCenter only work within a single process.

In order to communicate between containing app and extensions, you can use Darwin Notify Center CFNotificationCenterthat works like NSDistributedNotificationCenter, which is only available for osx.

The idea is use a file inside the group folder that they share. in containing app, you write the data you want to send into the file, then post a CFNotification, which will be received by today extension.

In today extension, use CFNotificationCenterAddObserver to observer the CFNotification, upon receiving it, callback will be called, in which a NSNotification has to be posted due to callback is a C style function and "userInfo" cannot be passed in the CFNotification, after receiving this NSNotification object, it starts to read data from the file, which is used to update the today extension view in Notification center.

You can use this github code to implement force loading the today extension view. It works for me.
Here is a great post on this. http://www.atomicbird.com/blog/sharing-with-app-extensions

Another option is to use setHasContent function. When you schedule a local identifier, set has content to false to hide the view, in handleActionWithIdentifier set it to true to show the view. This way, when you stay in notification center, you will not see the view for a moment, but when you see it, it will be the updated data.

let widgetController = NCWidgetController.widgetController()
widgetController.setHasContent(false, forWidgetWithBundleIdentifier: "YourTodayWidgetBundleIdentifier") 

But I think the whole problem is a rare case, which doesn't need to be fixed since you can get the updated data reloading the notification center or switch to notification tab and switch back to today tab.

Rolypoly answered 10/2, 2015 at 15:56 Comment(5)
I believe he is referring to iOS, since it is tagged as such and he is running in a simulator.Toxemia
@JeremyPope, yes, I think so, the solution and the code in the link are all for iOS.Rolypoly
So basically NSUserDefaultsDidChangeNotification does not fire because the app and the extension are two separate processes. It does not surprise me, but at the same time I was expecting this to "just work"... I tried the last idea (using setHasContent) and it did not seem to work either, though I'll have to investigate it more thoroughly. Thanks!Zuber
They both worked for me,for the last idea, I stayed in the notification center, the widget was not there for a moment and then reappeared with the updated value. :).Rolypoly
If you need listener and data sharing library for both Watch and Today extensions, use this neat lib: github.com/mutualmobile/MMWormholeRiegel

© 2022 - 2024 — McMap. All rights reserved.