I am creating a chat messenger app like whatsapp and trying to implement notification functionality similar to whatsapp. In whatsapp when you receive a notification it stores the data somewhere and when you turn off your wifi and go into the app the message is injected in the application. This mean whatsapp is somehow accessing the notification even when application is closed.
My Approach: I am receiving notification in background mode and saving that into a file. So if the user gets disconnected from the internet and goes into the app the messages are still injected on applicationWillAppear. This works perfect but when you forcefully close your app (Double clicking home and swiping the app up) it does not work. I have search almost everything and it says background fetch will not work if you forcefully close your application.
Then how whatsapp is doing it? What can be any other solution?
My Solution:
Turned on Background Modes for Background Fetch and remote notification from XCode. Next added following code inside application:willFinishLaunchingWithOptions:
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
Added this in AppDelegate.m file
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))handler{
NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES);
if (0 < [paths count]) {
NSString *documentsDirPath = [paths objectAtIndex:0];
NSString *filePath = [documentsDirPath stringByAppendingPathComponent:#"notification.txt"];
NSString *content = [NSString stringWithFormat:#"%#", userInfo];
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:filePath]) {
// Append text to the end of file
NSFileHandle *fileHandler = [NSFileHandle fileHandleForWritingAtPath:filePath];
[fileHandler seekToEndOfFile];
[fileHandler writeData:data];
[fileHandler closeFile];
} else {
// Create the file and write text to it.
[content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
}
handler(UIBackgroundFetchResultNewData);
}
Update: I do have following flag in my notification
content-available: 1
Background pushes do not get delivered to a user-terminated app.
Unless it is a voip push, in that case the app will be started by the OS if necessary (but yes you can only make use of voip push if your app provides voip functionality to the user.)
Related
I am developing an apple watch application which records an audio file, saves it and then transfers the file URL to the iPhone app via WCSession (Watch Connectivity framework). My code looks like this
In InterfaceController.m
NSURL *directory = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.name.watchtest"];
__block NSString *recordingName = #"myTestFile.mp4";
__block NSURL * outputURL = [directory URLByAppendingPathComponent:recordingName];
if ([WCSession isSupported]) {
if ([self.watchSession isReachable]) {
[self.watchSession transferFile:outputURL metadata:nil];
}
}
In ViewController.m (WCSession delegate)
-(void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file
{
NSError *error;
NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *docsDir = [dirPaths objectAtIndex:0];
NSFileManager *filemgr = [NSFileManager defaultManager];
NSString *filePath = [docsDir stringByAppendingString:#"/myTestFile.mp4"];
[filemgr moveItemAtPath:file.fileURL.path toPath:filePath error:&error];
if ([filemgr fileExistsAtPath:file.fileURL.path]) {
urlOfAudioFile = [[NSURL alloc] initFileURLWithPath:filePath];
[self uploadToServer:urlOfAudioFile];
}
}
This works absolutely fine if both the WatchApp & the iPhone App are Active.
How can I make it work when the iPhone is in the background/ inactive/ in the locked state?
The documentation on transferFile(_:metadata:) clearly states:
Use this method to send a file that is local to the current device.
Files are transferred to the counterpart asynchronously on a
background thread. The system attempts to send files as quickly as
possible but may throttle delivery speeds to accommodate performance
and power concerns. Use the outstandingFileTransfers method to get a
list of files that are queued for delivery but have not yet been
delivered to the counterpart.
...
This method can only be called while the session is active—that is,
the activationState property is set to activated. Calling this method
for an inactive or deactivated session is a programmer error.
So as per your code:
if ([WCSession isSupported]) {
if ([self.watchSession isReachable]) {
[self.watchSession transferFile:outputURL metadata:nil];
}
}
If the isSupported & isReachable checks fail, then basically WCSession is inactive and your code will not reach the transferFile(_:metadata:) part.
This is the correct behavior and you would have to handle this case manually.
But... when you have a valid session and transferFile(_:metadata:) does get called then whether the iPhone is locked, or the app is in background, or even if the app is not running, it will receive the file via a background thread.
So to answer your question, if the iPhone app is "inactive"; as in isReachable is false then the file transfer will not happen.
Ref:
https://developer.apple.com/documentation/watchconnectivity/wcsession/1615667-transferfile
In application I'm developing I have a requirement that user can customise push notification sounds server-side. I checked documentation and it states that
"The sound files can be in the main bundle of the client app or in the Library/Sounds folder of the app’s data container."
So I'm putting sounds I download from back-end to this subfolder in Library, which I create like this:
+ (NSString *)soundFolderPath
{
NSString* path = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
path = [path stringByAppendingPathComponent:#"Sounds"];
NSLog(#"%#: Path to sounds folder: %#", [self class], path);
return path;
}
- (void)createSoundsDirectoryIfNeeded
{
NSFileManager* manager = [NSFileManager defaultManager];
NSString* path = [self.class soundFolderPath];
if (![manager fileExistsAtPath:path])
{
NSError* error = nil;
[manager createDirectoryAtPath:path withIntermediateDirectories:NO attributes:nil error:&error];
if (error) {
[self handleError:error];
return;
}
}
}
But when I send push notification, while app is in background - it plays default notification sound instead of a custom one. I've checked data container of the app - files exist and are in the correct folder. File format conforms to what is described in documentation and I even added those sounds as resources to bundle and that way it actually worked.
I can't find a solution to this and at this point I'm wondering if it's even possible.
Can someone help me with this?
In my current project I have a push notification. When I tap the app icon I want to get the received notification from the launch options object, but it always returns nil:
NSDictionary *userInfo = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
You can't detect that case, because application is not open using push notification (it has been open via app icon).
Try to open application by swiping push notification.
EDIT:
If you want to be invoked for push notification (via background fetch, when your application is not active) you need to ask your backend developer to set "content-available": 1 in push notification.
After that -application:didReceiveRemoteNotification:fetchCompletionHandler: will be invoked (when push-notification arrives), so you can save the payload into a file and then when application will be open, you can read the file and take actions.
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(#"#BACKGROUND FETCH CALLED: %#", userInfo);
// When we get a push, just writing it to file
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"userInfo.plist"];
[userInfo writeToFile:filePath atomically:YES];
completionHandler(UIBackgroundFetchResultNewData);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Checking if application was launched by tapping icon, or push notification
if (!launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"userInfo.plist"];
[[NSFileManager defaultManager] removeItemAtPath:filePath
error:nil];
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:filePath];
if (info) {
// Launched by tapping icon
// ... your handling here
}
} else {
NSDictionary *info = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
// Launched with swiping
// ... your handling here
}
return YES;
}
Also don't forget to enable "Remote notifications" in "Background Modes"
When you launch the application from a PUSH NOTIFICATION ACTION, [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey] will return you the push notification payload (in dictionary format). When I say push notification action, it means either tapping the push notification from action center or from the push notification alert dialog (Depending on the device settings, push notification delivery mechanism varies).
If you launch the application by tapping the APP ICON, [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey] always returns nil. Because, it hasn't been launched from any kind of push notification.
I use this code to save some PDF data to a file, send it to another app using the "Open In" menu, then delete the file when that's done:
- (void)openIn:(NSData *)fileData {
// save the PDF data to a temporary file
NSString *fileName = [NSString stringWithFormat:#"%#.pdf", self.name];
NSString *filePath = [NSString stringWithFormat:#"%#/Documents/%#", NSHomeDirectory(), fileName];
BOOL result = [fileData writeToFile:filePath atomically:TRUE];
if (result) {
NSURL *URL = [NSURL fileURLWithPath:filePath];
UIDocumentInteractionController *controller = [[UIDocumentInteractionController interactionControllerWithURL:URL] retain];
controller.delegate = self;
[controller presentOpenInMenuFromBarButtonItem:self.openInButton animated:TRUE];
}
}
- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
// when the document interaction controller finishes, delete the temporary file
NSString *fileName = [NSString stringWithFormat:#"%#.pdf", self.name];
NSString *filePath = [NSString stringWithFormat:#"%#/Documents/%#", NSHomeDirectory(), fileName];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}
This has worked fine until iOS 8. Now, the file is created and I can verify that it contains the correct content, the Open In menu appears, I can select an app, and the delegate method runs and cleans up the file. But instead of iOS switching to the selected app and copying the file into it as it did before, the Open In menu simply closes when I select an app, and the file is not copied.
This works if I give the UIDocumentInteractionController an existing file. It also works if I use the provided fileData but change the destination filename to the filename of an existing file. This suggests a permissions problem -- as if new files are created in iOS 8 with default permissions that UIDocumentInteractionController can't read.
Does anyone know what's happening and how I can work around it?
It looks like the order of operations has changed slightly in iOS 8. DidDismissOpenInMenu used to run after the file was finished sending, but now it runs after the file begins sending. This means my cleanup code was sometimes running before the file was finished sending, leaving no file to send. I figured this out after noticing that smaller files were being sent okay; apparently the processing for smaller files was finishing before my cleanup code got them, but the processing for larger files was not.
To ensure the correct timing, but also clean up files that are created when the user opens the DocumentInteractionController and then dismisses the controller without doing anything, I changed my methods like this:
- (void)openIn:(NSData *)fileData {
// save the PDF data to a temporary file
NSString *fileName = [NSString stringWithFormat:#"%#.pdf", self.name];
NSString *filePath = [NSString stringWithFormat:#"%#/Documents/%#", NSHomeDirectory(), fileName];
BOOL result = [fileData writeToFile:filePath atomically:TRUE];
if (result) {
self.sendingFile = FALSE;
NSURL *URL = [NSURL fileURLWithPath:filePath];
UIDocumentInteractionController *controller = [[UIDocumentInteractionController interactionControllerWithURL:URL] retain];
controller.delegate = self;
[controller presentOpenInMenuFromBarButtonItem:self.openInButton animated:TRUE];
}
}
- (void)documentInteractionController:(UIDocumentInteractionController *)controller willBeginSendingToApplication:(NSString *)application {
// the user chose to send the file, so we shouldn't clean it up until that's done
self.sendingFile = TRUE;
}
- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
if (!self.sendingFile) {
// the user didn't choose to send the file, so we can clean it up now
[self openInCleanup];
}
}
- (void)documentInteractionController:(UIDocumentInteractionController *)controller didEndSendingToApplication:(NSString *)application {
// the user chose to send the file, and the sending is finished, so we can clean it up now
[self openInCleanup];
self.sendingFile = FALSE;
}
- (void)openInCleanup {
// delete the temporary file
NSString *fileName = [NSString stringWithFormat:#"%#.pdf", self.name];
NSString *filePath = [NSString stringWithFormat:#"%#/Documents/%#", NSHomeDirectory(), fileName];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}
Update for iOS 11
Before iOS 11, it seems that the operating system kept a copy of the file available until the receiving app was finished reading it, even though my cleanup function ran as soon as the file was sent out from my app. In iOS 11, this changed and the receiving app fails to read the file because my app deletes it before that's done. So now instead of saving the temporary file to Documents and using the openInCleanup method to delete it immediately, I'm saving the temporary file to tmp and emptying the tmp folder next time the app launches. This approach should also work with older iOS versions. Just remove openInCleanup, change Documents to tmp in the paths, and add this to applicationDidFinishLaunching:
// clear the tmp directory, which will contain any files saved for Open In
NSString *tmpDirectory = [NSString stringWithFormat:#"%#/tmp", NSHomeDirectory()];
NSArray *tmpFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirectory error:NULL];
for (NSString *tmpFile in tmpFiles) {
[[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:#"%#/%#", tmpDirectory, tmpFile] error:NULL];
}
After reading this post, I already hoped to have found the solution to a similar problem:
For me, as of iOS 8, sharing was only working with Mail.app. It was failing for Dropbox, etc.
Turns out it was something else:
On my interactionController I was setting an annotation like this:
interactionController.annotation = #"Some text"
For unknown reasons, this prevented Dropbox to open at all. There were no error messages or anything. Removing this line solved the issue.
I am trying to implement a simple plist example from "Beginning iPhone 3 Development book". I looked into the code but my data was never saved to a plist file. Actually my project site map is as follows: Whenever you launch the app it fires in TestViewController. On the TestViewController, there is a button. When you click on the button it pushes another view controller which is PersistenceViewController and here is the code I wrote in PersistenceViewController. My doubt: is the applicationWillTerminate being called in this method? I don't think so..please help. I am learning how to persist the data now.
In .h file #define kFilename #"data2.plist"
- (NSString *)dataFilePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:kFilename];
return path;
}
- (void)applicationWillTerminate:(NSNotification *)notification {
NSMutableArray *contactFormArray = [[NSMutableArray alloc] init];
NSLog(#"App Terminate:%d",[contactFormArray count]);
[contactFormArray addObject:nameField.text];
[contactFormArray addObject:emailField.text];
[contactFormArray addObject:phoneField.text];
[contactFormArray addObject:companyField.text];
[contactFormArray writeToFile:[self dataFilePath] atomically:YES];
[contactFormArray release];
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSArray *contactFormArray = [[NSArray alloc] initWithContentsOfFile:filePath];
NSLog(#"Did Load:%d",[contactFormArray count]);
nameField.text = [contactFormArray objectAtIndex:0];
emailField.text = [contactFormArray objectAtIndex:1];
phoneField.text = [contactFormArray objectAtIndex:2];
companyField.text = [contactFormArray objectAtIndex:3];
[contactFormArray release];
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(applicationWillTerminate:)name:UIApplicationWillTerminateNotification object:app];
[super viewDidLoad];
}
Thanks for any valuable suggestions...
applicationWillTerminate is called when the user quits the application (usually by pressing the Home button). Any code in this delegate method will be executed (if it doesn't take too much time). In your case, the Plist file will be saved.
If you are using iOS 4, pressing the Home button may send your application into the background (not quitting). If the application is killed using the debugger or crashes, that method will not be called.
Additional Information:
On iOS 4, multitasking is enabled in Xcode projects by default. This prevents the applicationWillTerminate method from being called. If you do not want to support multitasking, place UIApplicationExitsOnSuspend in your MyAppName-Info.plist file and check the checkbox. If you do want to support it, place any code in the applicationDidEnterBackground delegate method that you want to execute before the application enters an inactive state.
applicationWillTerminate: will not be called on multitasking devices (iOS4) if app is suspended and then killed via the multitasking UI. According to Apple: "apps are not aware of any transitions into or out-of the suspended state". So if you're saving anything inside applicationWillTerminate: try to do it in applicationWillResignActive:.
I might be mistaken here, but doesn't applicationWillTerminate have to be coded into the app's delegate.m file, rather than in some other .m file? Regardless, this might not matter due to the suspended state of apps in iOS 4.