Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSUserDefaultsDidChangeNotification and Today Extensions

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.

like image 730
Alex Avatar asked Feb 02 '15 19:02

Alex


1 Answers

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.

like image 180
gabbler Avatar answered Oct 12 '22 12:10

gabbler