I am trying to save a data that comes with push notification payload. It works well when the app is running but not when the app is closed.
how can I save data from push notification to sqlite db when the app is completely closed and not in the background .
i need to do this code while the application is closed and receives a push notification
- (void) application:(UIApplication *)application didReceiveRemoteNotification:NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
//save data
NSLog(#" Received remote notifcation: %#", userInfo);
for (NSString *key in [userInfo allKeys])
{
NSString *data = [userInfo objectForKey:key];
NSLog(#"inside did register for notification .... %# ---- > %#",key,data);
}
query = [NSString stringWithFormat:#"INSERT INTO accounts(email_address) VALUES ('%#')",data;
sqlite3_stmt *compiledStatement;
if(sqlite3_prepare_v2(mydb, [query UTF8String], -1, &compiledStatement, NULL) == SQLITE_OK)
{
if(SQLITE_DONE != sqlite3_step(compiledStatement))
{
NSLog( #"Error while inserting data: '%s'", sqlite3_errmsg(mydb));
}
else {
NSLog(#"New data inserted");
isneed=#"yes";
}
sqlite3_reset(compiledStatement);
}
else
{
NSLog( #"Error while inserting '%s'", sqlite3_errmsg(mydb));
}
}
The following is taken from the apple documentation...
Use this method to process incoming remote notifications for your app. Unlike the application:didReceiveRemoteNotification: method, which is called only when your app is running in the foreground, the system calls this method when your app is running in the foreground or background. In addition, if you enabled the remote notifications background mode, the system launches your app (or wakes it from the suspended state) and puts it in the background state when a push notification arrives. However, the system does not automatically launch your app if the user has force-quit it. In that situation, the user must relaunch your app or restart the device before the system attempts to launch your app automatically again.
Note the highlighted text, the app will not be started if it is completely closed
I don't save them into CoreData but I save them into NSUserDefaults. This solution was recommended by Apple Team Support and it is only for iOS 10, I implemented it and it works well! I leave the link:
https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ModifyingNotifications.html
This is the procedure:
Create a new Notification Service Extension for your app. Open your project and press File - New - Target and select on iOS section, the option "Notification Service Extension". Put name of extension and the info that is required. After that, Xcode will added two files with Objective C language: the .h and .m file.
NOTE: Consider that you will use three identifiers:
1) Identifier for your app (you already have)
2) Identifier for your extension (you will create)
3) Identifier for App Group. (you will create)
Is necessary to enable App Groups to create a resource where you can save and read info for your app and the extension. Click on "Show Project Navigator". Into the list of targets select your main project. Then, click on "Capabilities" and switch on the option named "App Groups". Please add the identifier for App Group to identify share resources. You should do the same step for the target of extension that you created (Select target of extension - Capabilities - Switch on at "App Groups" - Add the same identifier for App Group)
You should to add the Identifier for your extension into Apple Developer Site for identify the Notification Service Extension, also you should to make new Provisional Profiles (Development, AdHoc and/or Production) and associate it with the new Provisional Profiles.
On both identifiers (App and Extension) you should edit them and enable "App Groups" Service in both of them. You should to add the Identifier for App group into the App Groups Services.
NOTE: Identifier for App and Identifier for Extension SHOULD HAVE THE
SAME IDENTIFIER FOR APP GROUP.
Download the new Provisional Profile on Xcode and associate them with your Notification Service Extension. Please ensure you everything is ok with them.
After that, into "Capabilities" of your app and extension, open "App Groups" section and update them. The three steps - 1)Add the App Groups entitlements to your entitlements file, 2) App the App Groups feature to your App ID and 3) Add App Groups to your App ID - should be checked.
Come back to Project navigator and select the folder of you extension. Open the .m file. You will see a method called didReceiveNotificationRequest:(UNNotificationRequest *)request. Into this method you will create a different NSUserDefaults with SuiteName exactly equal to the Identifier for app group like this:
NSUserDefaults *defaultsGroup = [[NSUserDefaults alloc]
initWithSuiteName: #"identifier for app group"];
Into this same method, get the body of the notification and save it into a NSMutableArray, then save it in the share resources. Like this:
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler{
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
NSMutableArray *notifications = [NSMutableArray new];
NSUserDefaults *defaultsGroup = [[NSUserDefaults alloc] initWithSuiteName: #"Identifier for app group"];
notifications = [[defaultsGroup objectForKey:#"notifications"] mutableCopy];
if (notifications != nil){
[notifications addObject:self.bestAttemptContent.userInfo];
}else{
notifications = [NSMutableArray new];
[notifications addObject:self.bestAttemptContent.userInfo];
}
[defaultsGroup setObject:notifications forKey:#"notifications"];
}
Finally, into the App Delegate of your main project, in your didFinishLaunchingWithOptions method, recover the Array into the share resources with this code:
NSUserDefaults *defaultsGroup = [[NSUserDefaults alloc] initWithSuiteName: #"Identifier for app group"];
NSMutableArray *notifications = [[defaultsGroup objectForKey:#"notifications"] mutableCopy];
Enjoy it!
I hope to be clear with each step. I going to write a post to implement this solution with images in my page. Another point that you should consider is it doesn't work with Silent Push Notification.
Related
Assume a following situation:
User restarts his/her iPhone.
User lets device locked, does
not unlock it.
Server sends a (silent) push notification on the
device (or anything happens that wakes the application up on the
background, such as Apple Watch extension requests data, etc.).
App wakes up and tries to access Keychain items stored with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly accessibility.
Now, the Keychain items should not be accessible, since device was not unlocked yet. How do I correctly check for this situation?
Note: In my case, the presence of an item stored in the keychain determines if the app is "active" or not, so I would need something to stop this check soon enough, otherwise my app will assume it's not active (value cannot be read) and perform init steps...
I've come across the same situation in my app and here's how I'm checking if the keychain is available (objective-c code):
+ (BOOL)isKeychainAvailable {
NSString *testVal = #"testVal";
NSString *testKey = #"testKey";
[JNKeychain saveValue:testVal forKey:testKey];
NSString *validatedValue = [JNKeychain loadValueForKey:testKey];
[JNKeychain deleteValueForKey:testKey];
return (validatedValue == testVal);
}
I basically save a value in the keychain and try to read it again. If it's not the same as I've just written it means the keychain is unavailable, which also means the phone hasn't been through the first unlock since the keychain should be available after the first unlock thanks to the option kSecAttrAccessibleAfterFirstUnlock.
What I ended up doing in this situation is terminating the app if it was started in the background and the keychain is unavailable:
- (void) methodStartedInBackgroundThatNeedsKeychain {
if (!JNKeychain.isKeychainAvailable && [UIApplication sharedApplication].applicationState != UIApplicationStateActive) {
exit(0);
}
}
ATTENTION! please be aware that Apple strongly discourages the usage of exit(0) when the app is in foreground mode, thats why I'm making sure to only call it in background with [UIApplication sharedApplication].applicationState != UIApplicationStateActive. Here's Apple's QA discussion on the subject: https://developer.apple.com/library/archive/qa/qa1561/_index.html
I have a simple project where the user taps a button on the Apple Watch and some audio plays on the iPhone, this is easy enough to do with the openParentApplication method and having the handleWatchKitExtensionRequest code in AppDelegate. However, while this works in the simulator, it will NOT work on the actual devices if the iPhone app is not already open. I'm trying to find if it's possible to use other methods, that will work even if the iPhone app isn't already open.
I've read on a stackoverflow answer here that it is possible to use Handoff to (partially) bring the phone app to the foreground, using WKInterfaceController updateUserActivity:userInfo:webpageURL: and UIApplicationDelegate application:continueUserActivity:restorationHandler. However, as a new developer I'm struggling to work out how to do this properly without any examples. Can anyone give some example code of how this would work, where both these are used together to run some code on the iphone app?
Register your activity type names in your iphone application's plist. Add a row named NSUserActivityTypes and make it an array. Eg:
Include the continueUserActivity: method in your AppDelegate. Eg:
-(BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler
{
// Extract the payload
NSString *type = [userActivity activityType];
NSDictionary *userInfo = [userActivity userInfo];
// Assume the app delegate has a text field to display the activity information
NSLog(#"User activity is of type %#, and user info %#", type, userInfo);
restorationHandler(#[self.window.rootViewController]);
return YES;
}
In your watchkit controller's awakeWithContext, add the updateUserActivity method.
[self updateUserActivity:#"com.co.YourApp.watchkitextension.activity" userInfo:#{#"yo": #"dawg"} webpageURL:nil];
You should now see the app icon after opening the selected viewcontroller in your watch app.
I was wondering if there is any other way besides MMWormhole to pass basic data between iPhone and Apple Watch. Do you know if any existing official Apple framework allows this?
It is possible.
Looking at: The WatchKit Doc's
There is a paragraph on sharing data between the watch app and the extension on the iPhone.
To quote the first paragraph.
Sharing Data with Your Containing iOS App
If your iOS app and WatchKit extension rely on the same data, use a shared app group to store that data. An app group is a secure container that multiple processes can access. Because your WatchKit extension and iOS app run in separate sandbox environments, they normally do not share files or communicate directly with one another. An app group lets the two processes share files or user defaults information between them.
From what I understand MMWormhole is handy for as close to realtime data changes between the 2 binaries. Whereas this method allows for accessing data used saved by the iPhone app that can be read by the Watch App and Vice Versa.
We can pass the data between iPhone & iWatch using groups.
Basically We can share data using the NSUserDefaults.
But for that you need to enable that see steps below:
1)open capabilities section in both your project target
2)open App Groups from that section
3)add container by click on + button with name group.test.demo
sample code to achieve that.
In your iphone app code
NSUserDefaults *myDefaults = [[NSUserDefaults alloc]initWithSuiteName:#"group.test.demo"];
[myDefaults setObject:#"tejas" forKey:#"name"];
now value "tejas" is set for key "name"
code to retrieve that
NSUserDefaults *myDefaults = [[NSUserDefaults alloc]initWithSuiteName:#"group.test.demo"];
[myDefaults objectForKey:#"name"];
best of luck :)
If you check the docs for WKInterfaceController, you'll find a method called openParentApplication:reply: that allows you to communicate with your host app in the background.
As stated above, I have used a shared app group and placed the core data files in that group. Using this technique, both the phone app and the watch can read and write the same data and all is good when they are run discretely. As each process is running in a separate sandbox, you run into the classic distributed database problem of potentially overwriting data from different sources.
To overcome this, you need to put data observers in place. I resorted to using the NSDistributedNotificationCenter to pass some custom messages between the app and the watch extension, but there may be a more elegant solution. Any ideas from others?
Use watch Connectivity.
// 1. In .m viewcontroller on phone & interface controller on iwatch
#import <WatchConnectivity/WatchConnectivity.h>
#interface IController()<WCSessionDelegate>
#end
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];}
// 2. Send Message - Phone or iWatch:
[[WCSession defaultSession] sendMessage:response
replyHandler:^(NSDictionary *reply) {
NSLog(#"%#",reply);
}
errorHandler:^(NSError *error) {
NSLog(#"%#",error);
}
];
// 3. Receive Message - Phone or iWatch
- (void)session:(WCSession *)session didReceiveMessage: (NSDictionary<NSString *, id> *)message
{
}
I'm creating a Watch app just to display a value on the watch when the user taps on a table view in the iPhone/host app.
I would like to get a notification the value changes on a shared UserDefault. It is shared between WatchKit app and the iOS (host) app, so when the user makes any changes in the host app I'm expecting to get the notification. I've done the following:
When user do some action in application (host app):
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:#"group.app"];
id object = [self.plantsArray objectAtIndex:[self.plantsTable indexPathForSelectedRow].row];
[shared setObject:object forKey:#"data"];
[shared synchronize];
In the Watchkit extension have registered for the notification:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(print) name:NSUserDefaultsDidChangeNotification object:nil];
but unfortunately I'm not getting any notification, has anyone a solution?
I don't think iOS has capability of distributed notifications between app and extension, notifications will not work between both, instead you need to find a way in which both can monitor changes. For example files.
As you already have created group, you can keep a file in the group folder and add a filewatcher in extension, update the file from app, and filewatcher will catch the change, and your work is done.
For filewatcher see code here.
Hope it helps.
Cheers.
Update
Find File watcher Swift version here. Thanks #rivera for adding.
You can try MMWormHole which provides :
a channel between the iOS device and the watch which enables you to send data back and forth between them.
Also provides a way to do Notifications without having to handle the file monitoring by urself.
Using it ,That will be all the code needed to do notifications in ur app
[self.wormhole passMessageObject:#{#"buttonNumber" : #(1)} identifier:#"button"];
[self.wormhole listenForMessageWithIdentifier:#"button"
listener:^(id messageObject) {
self.numberLabel.text = [messageObject[#"buttonNumber"] stringValue];
}];
Is it possible to intercept if the user switches from on to off the iCloud support under Settings -> iCloud -> Document & Data?
Obviously when he does this the app has already resigned active and entered the background. I am targeting iOS7 and I would like to keep in sync the UIManagedDocument, otherwise it would be like having two different UIDocuments: one with iCloud support and all the data created until the switch from on to off and a new one without any data in it. If I create data when iCloud support has been switched to off, and then I switch back to on I get the same DB I had when the support has been switched to off.
Note: I believe nelico's answer is right. He wrote: "If your app is running and the user changes enables or disable Document & Data iCloud syncing via the settings app, your app will receive the SIGKILL signal."
When the user changes the settings the app is ALREADY in the background and receives the SIGKILL signal. This is what I do not understand and I do not want. Registering for NSUbiquityIdentityDidChangeNotification doesn't solve this problem.
Another, cleaner, solution is to listen for the NSUbiquityIdentityDidChangeNotification notification and when you get that notification then check the URLForUbiquityContainerIdentifier if it is null they either signed out or turned off 'Documents and Data'. You should also be tracking the current ubiquity token so that you can know not only if they logged off but if they changed iCloud accounts. It happens more than one might think because Apple Geniuses like to just create a new iCloud account when things go wrong on users devices.
ex:
id <NSObject,NSCopying,NSCoding> _currentUbiquityIdentityToken;
...
_currentUbiquityIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken];
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector (_iCloudAccountAvailabilityChanged:) name: NSUbiquityIdentityDidChangeNotification object: nil];
...
- (void)_iCloudAccountAvailabilityChanged:(NSNotification*)notif {
if (![_currentUbiquityIdentityToken isEqual:[[NSFileManager defaultManager] ubiquityIdentityToken]]) {
// Update the current token and rescan for documents.
_currentUbiquityIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken];
// Do something about the change here...
}
}
Usually you would put the checks in the method below. That way whenever the app becomes active (including first time its launched) you can check the settings just prior to returning control to the user, no need to do polling in the background. For UIManagedDocument you may want to migrate the store to a local only copy and remove any iCloud content or the opposite, depending on the users input.
BTW just include the check in the previous answer to test if the global iCloud settings have been turned on or off. I don't do this because its only necessary to change the apps behaviour if the user has set the app specific settings.
/*! The app is about to enter foreground so use this opportunity to check if the user has changed any
settings. They may have changed the iCloud account, logged into or out of iCloud, set Documents & Data to off (same effect as
if they logged out of iCloud) or they may have changed the app specific settings.
If the settings have been changed then check if iCloud is being turned off and ask the user if they want to save the files locally.
Otherwise just copy the files to iCloud (don't ask the user again, they've just turned iCloud on, so they obviously mean it!)
#param application The application
*/
- (void)applicationWillEnterForeground:(UIApplication *)application
{
//LOG(#"applicationWillEnterForeground called");
// Check if the settings have changed
[[NSUserDefaults standardUserDefaults] synchronize];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
bool userICloudChoice = [userDefaults boolForKey:_cloudPreferenceKey];
// Check against the current in memory setting
if (userICloudChoice == useICloudStorage) {
// The setting has not been changed so just ignore
//LOG(#" iCloud choice has not changed");
} else {
// The setting has changed so do something
//LOG(#" iCloud choice has been changed!!");
// iCloud has been turned off so ask the user if they want to keep files locally
if (!userICloudChoice) {
//LOG(#" Ask user if they want to keep iCloud files ?");
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
_cloudChangedAlert = [[UIAlertView alloc] initWithTitle:#"You're not using iCloud" message:#"What would you like to do with documents currently on this phone?" delegate:self cancelButtonTitle:#"Keep using iCloud" otherButtonTitles:#"Keep on My iPhone", #"Delete from My iPhone", nil];
} else {
_cloudChangedAlert = [[UIAlertView alloc] initWithTitle:#"You're not using iCloud" message:#"What would you like to do with documents currently on this phone?" delegate:self cancelButtonTitle:#"Keep using iCloud" otherButtonTitles:#"Keep on My iPad", #"Delete from My iPad", nil];
}
[_cloudChangedAlert show];
} else {
// iCloud has been turned on so just copy the files across, don't ask the user again...
//LOG(#" iCloud turned on so copy any created files across");
[[CloudManager sharedManager] setIsCloudEnabled:YES]; // This does all the work based on the settings passed to it
useICloudStorage = YES;
}
}
}
Oh and also register for this notification in case the user logs on using another iCloud account.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(checkUserICloudPreferenceAndSetupIfNecessary) name:NSUbiquityIdentityDidChangeNotification object:nil];
It's not possible to intercept the changes, but you can programmatically check to see if they have iCloud enabled:
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *ubiquityContainerURL = [fileManager URLForUbiquityContainerIdentifier:nil];
if (!ubiquityContainerURL) {
// iCloud is not enabled
}
When your app enters the background, you could periodically poll this and have your app respond to any change in state.
EDIT: If your app is running and the user changes enables or disable Document & Data iCloud syncing via the settings app, your app will receive the SIGKILL signal. However, the OS can terminate your app for a number of reasons so trapping SIGKILL is not a reliable method of intercepting iCloud sync setting changes. You are still better off periodically polling.