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.
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
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.)
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 have a Data plist (conveniently named Data.plist) that is updated on launch of the app:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Determile cache file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *filePath = [NSString stringWithFormat:#"%#/%#", [paths objectAtIndex:0],#"Data.plist"];
NSString *dataURLString = #"http://link/to/Data.plist";
NSURL *dataURL = [[NSURL alloc] initWithString:dataURLString];
NSData *plistData = [NSData dataWithContentsOfURL:dataURL];
[plistData writeToFile:filePath atomically:YES];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSLog(#"The bundle is %#", filePath);
self.data = dict;
// Configure and show the window
[window addSubview:[navigationController view]];
[window makeKeyAndVisible];
return YES;
}
I'd like to be able to have some way of checking the saved plist against the server plist - I've seen some implementations that use external libraries but there has to be something in the original iOS SDK. Any ideas? I've read whatever code I do end up using needs to be implemented in viewWillAppear but I'm not sure what that code is exactly.
Two things... first, dataWithContentsOfURL: and generally any of Apple's (temptingly convenient) <anything>WithContentsOfURL: methods are extremely unsafe in the real world. It's blocking which means that no other code will execute until your request succeeds or fails. That means that if the server isn't available or your device doesn't have internet or for some other reason cannot retrieve your data, your phone will sit there until either the iOS watchdog process kills your app for freezing for too long, or it just fails. Then the rest of your app that is expecting data will freak out because suddenly you have no data when your code assumes you should. This is one of many problems with synchronous requests.
I won't go into how to implement asynchronous requests, but head over to Apple's documentation or you can use a wrapper framework like http://allseeing-i.com/ASIHTTPRequest/ that does it for you. Also have a look at http://www.cocoabyss.com/foundation/nsurlconnection-synchronous-asynchronous/
To answer your actual question, you could have a tiny text file on your server with a version number or time stamp and download that along with your plist. on subsequent launches, you can pull down the time stamp/version number and compare it against the one you've got stored, and if the version on the server is more recent, then you pull it and save the new time stamp/version number.
I have an app that is storing Task objects (custom class) inside of a NSMutableArray. The Task class conforms to the NSCoding and NSCopying protocols, and I have also implemented the encodeWithCoder and initWithCoder methods.
When the user adds a new task to an NSMutableArray list, I save the array using NSKeyedArchiver. The list populates a UITableView.
The data is being saved, and when I exit the app and reenter, the data is still there. When I use another app for a while and come back, the data is still there. However, when I "kill" the app in the multitasking task manage or restart the device, the data disappears. Here are some important code snippets:
#define kFilename #"epapsTasksFile"
.
- (NSString *)dataFilePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingPathComponent:kFilename];
}
- (void)viewDidLoad {
NSString *filePath = [self dataFilePath];
if([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
self.list = [[NSKeyedUnarchiver unarchiveObjectWithFile:kFilename] retain];
}
else {
self.list = [NSMutableArray arrayWithCapacity:1];
}
...
}
- (void)applicationWillResignActive:(NSNotification *)notification {
NSMutableArray *updatedList = [[NSMutableArray alloc] initWithArray:self.list];
[NSKeyedArchiver archiveRootObject:updatedList toFile:kFilename];
}
Why is my app not saving the data when the app is "killed" or the device is restarted? Also, it may be interesting to note that when I restart the iPhone simulator, the data stays in place.
You need to save the data
([NSKeyedArchiver archiveRootObject:updatedList toFile:kFilename];
in applicationWillTerminate delegate method as as well to save it on termination.
EDIT:
applicationWillTerminate is not gauranteed in IOS4.0 and above.
Best is to check the return status of archiveRootObject:toFile: and see if the data is stored properly. As you figured it out, it can be case with wrong file path.