I'm trying to use the following code to persist the current list of local notifications. NSArray explicitly lists the kind of objects it will work with, which implies I can not use this with an array full of UILocalNotification objects. However, UILocalNotifications does implement NSCoding, which led me to believe there must be an easy way to serialize/deserialize this list of objects. Do I need to do the encoding and file persistence myself? Also, is there a way to get more information about why the write failed?
- (NSString*)getSavedNotifsPath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingString:#"saved_notifs.plist"];
}
- (void)prepareToHide {
UIApplication* app = [UIApplication sharedApplication];
NSArray *existingNotifications = [app scheduledLocalNotifications];
if (! [existingNotifications writeToFile:[self getSavedNotifsPath] atomically:NO] ) {
// alert
[self showSomething:#"write failed"];
}
}
First, change the code
return [documentsDirectory stringByAppendingString:#"saved_notifs.plist"];
to
return [documentsDirectory stringByAppendingPathComponent:#"saved_notifs.plist"];
stringByAppendingPathComponent: will ensure a slash (/) is included, if needed, before the file name.
NSArray can only save property list objects, which UILocalNotification is not. Instead, try using NSKeyedArchiver. For example:
- (void)prepareToHide {
UIApplication* app = [UIApplication sharedApplication];
NSArray *existingNotifications = [app scheduledLocalNotifications];
NSString *path = [self getSavedNotifsPath];
BOOL success = [NSKeyedArchiver archiveRootObject:existingNotifications toFile:path];
if (! success ) {
// alert
[self showSomething:#"write failed"];
}
}
Use NSKeyedUnarchiver to retrieve the array from the saved file.
Note: I have not actually tested this so I'm not 100% sure it will work. But give it a try and see what happens.
Related
I'm currently working on a small and specific iPhone app to connect to a server and download some JSON data. My problem is about a property list I use to save some data (login, password and domain of the server). I've easily created the plist in Xcode but when I try to edit it with the texts entered by the user, I'm having a problem and I don't know how to fix it ...
Here is the textFieldDidEndEditing: method, in which I try to write to the plist, with what the user have written in a domainTextField :
- (void)textFieldDidEndEditing:(UITextField *)textField
{
NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES);
NSString *filepath = [NSString stringWithFormat:#"%#/userInfo.plist", [paths objectAtIndex:0]];
NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithContentsOfFile:filepath];
if (self.domainTextField) {
NSString *domain = [NSString stringWithFormat:#"%#", self.domainTextField.text];
[userInfo setValue:domain forKey:#"domain"];
}
if ([userInfo writeToFile:filepath atomically:TRUE]) {
NSLog(#"write succeeded");
} else {
NSLog(#"write failed");
}
}
I'm always receiving a "write failed" error here and after playing a bit with the debugger, I found that the problem is with the setValue:forKey: method. In fact, I expect this method to write that in the plist : domain = "whateverthedomainis" because domain is a NSString but it writes instead : domain = whateverthedomainis. And as the writeToFile:atomically: method makes sure that all the objects are property list objects, it returns NO. I tried to put backslashes, but that didn't help.
What can I do to resolve this error?
StringWithFormat will use if you have any arguments in a string so you can directly assign a string value ,like this
NSString *domain = self.domainTextField.text;
Have tried storing my NSMutableArray's object to NSUserDefaults but, no luck.
My NSMutableArray contains this log right here:
`ALAsset - Type:Photo, URLs:assets-library://asset/asset.JPG?id=92A7A24F-D54B-496E-B250-542BBE37BE8C&ext=JPG`
I know that its a ALAsset object, in the AGImagePickerController it is compared as NSDictionary, so what I needed to do is save the NSDictionary or the Array I used to where I store my ALAsset object then save it in either in NSDocu or NSCaches as a file then retrieve it again (This was my idea).
But the problem is,Though I tried this code but not working, and doesn't display anything in NSDocu or NSCache Directories.
First try (info is the one that contains ALAsset object):
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
NSString *filePath = [basePath stringByAppendingPathComponent:#"filename.plist"];
NSDictionary *dictionary = [NSDictionary dictionaryWithContentsOfURL:filePath];
NSString *error;
NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:plistDict format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
if(plistData) {
[info writeToFile:filePath atomically:YES];
} else {
NSLog(error);
}
Second try:
- (NSString *)createEditableCopyOfFileIfNeeded:(NSString *)_filename {
// First, test for existence.
BOOL success;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *writableFilePath = [documentsDirectory stringByAppendingPathComponent: _filename ];
success = [fileManager fileExistsAtPath:writableFilePath];
if (success) return writableFilePath;
// The writable file does not exist, so copy the default to the appropriate location.
NSString *defaultFilePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: _filename ];
success = [fileManager copyItemAtPath:defaultFilePath toPath:writableFilePath error:&error];
if (!success) {
NSLog([error localizedDescription]);
NSAssert1(0, #"Failed to create writable file with message '%#'.", [error localizedDescription]);
}
return writableFilePath;
}
Save it this way:
NSString *writableFilePath = [self createEditableCopyOfFileIfNeeded:[NSString stringWithString:#"hiscores"]];
if (![info writeToFile:writableFilePath atomically:YES]){
NSLog(#"WRITE ERROR");
}
Third try:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:??????];
[info writeToFile:filePath atomically:YES];
Fourth try(Unsure of because of its modifying in the appbundle):
https://stackoverflow.com/a/6311129/1302274
Is there other way? Hope someone would guide me.
You can store your NSMutableArray to NSUserDefault by archiving it to NSData and than retrieving it by Unarchiving it back to NSMutableArray.
-(NSData*) getArchievedDataFromArray:(NSMutableArray*)arr
{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:arr];
return data;
}
-(NSMutableArray*) getArrayFromArchievedData:(NSData*)data
{
NSMutableArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:data];
return arr;
}
For saving array to NSUserDefault :
[[NSUserDefaults standardUserDefaults] setObject:[self getArchievedDataFromArray: yourArray] forKey:#"YourKey"];
[[NSUserDefaults standardUserDefaults] synchronize];
For retrieving array back from NSUserDefault :
NSMutableArray *yourArray = [self getArrayFromArchievedData:[[NSUserDefaults standardUserDefaults]objectForKey:#"YourKey"]];
Also you can save Array in form of NSData to a file in NSDocumentDirectory or NSCachesDirectory. Hope this helps....
Edited: An UIImage+NSCoding category
.h file
#import <UIKit/UIKit.h>
#interface UIImage (NSCoding)
- (id) initWithCoderForArchiver:(NSCoder *)decoder;
- (void) encodeWithCoderForArchiver:(NSCoder *)encoder ;
#end
.m file
#import "UIImage+NSCoding.h"
#import <objc/runtime.h>
#define kEncodingKey #"UIImage"
#implementation UIImage (NSCoding)
+ (void) load
{
#autoreleasepool {
if (![UIImage conformsToProtocol:#protocol(NSCoding)]) {
Class class = [UIImage class];
if (!class_addMethod(
class,
#selector(initWithCoder:),
class_getMethodImplementation(class, #selector(initWithCoderForArchiver:)),
protocol_getMethodDescription(#protocol(NSCoding), #selector(initWithCoder:), YES, YES).types
)) {
NSLog(#"Critical Error - [UIImage initWithCoder:] not defined.");
}
if (!class_addMethod(
class,
#selector(encodeWithCoder:),
class_getMethodImplementation(class, #selector(encodeWithCoderForArchiver:)),
protocol_getMethodDescription(#protocol(NSCoding), #selector(encodeWithCoder:), YES, YES).types
)) {
NSLog(#"Critical Error - [UIImage encodeWithCoder:] not defined.");
}
}
}
}
- (id) initWithCoderForArchiver:(NSCoder *)decoder {
if (self = [super init]) {
NSData *data = [decoder decodeObjectForKey:kEncodingKey];
self = [self initWithData:data];
}
return self;
}
- (void) encodeWithCoderForArchiver:(NSCoder *)encoder {
NSData *data = UIImagePNGRepresentation(self);
[encoder encodeObject:data forKey:kEncodingKey];
}
#end
The documentation of NSArray for the "writeToFile:atomically:" method, shows that all members must be property list objects. ALAsset is not a property list object, so writing that to a file is not going to work.
I know that its a ALAsset object, in the AGImagePickerController it is
compared as NSDictionary
If you looked carefully then you would have seen that it does not compare ALAsset's, but their 'ALAssetPropertyURLs' property. The value of that property is an NSDictionary.
As ALAsset does not have a public constructor, there is no way you can reconstruct it after reading from a file or NSUserDefaults, even if you manage to write it.
So the best thing you can do is to re-fetch the ALAssets from the source that you originally got them from. I assume that is an ALAssetsGroup? Instead of saving to file and retrieving again, why don't you just regenerate them with the same query on ALAssetsGroup as you originally used to generate them?
EDIT:
So you say you got the original ALAsset's from an AGImagePickerController. In order to store them, you can take Matej's advice in the comments and store the URLs that identify them.
But keep in mind that AGImagePickerController is a means for the user to pick a number of photos and then do something with them. That is, the ALAssets are simply intermediare results pointing to the original locations of the photos. If you store the URL's and retrieve them later, there is no guarantee at all that the originals are still there.
So ask yourself: what is it that you want the user to do with the photos, and store the result of that action, rather than the assets themselves. For example, one reasonable action you could do is to create a new ALAssetGroup (with the addAssetsGroupAlbumWithName: method on ALAssetsLibrary), and store the assets in there. ALAssetGroups are automatically saved, so you don't need to do anything yourself for that.
EDIT 2 - after more information from the OP
What Matej hints at in the comments, is to convert the array of ALAssets that you have into an array of dictionaries by retrieving the urls from the assets. As you can read in the ALAsset class documentation you can do that in the following way:
NSArray *assetArray = // your array of ALAssets
NSMutableArray *urls = [NSMutableArray arrayWithCapacity:assetArray.count];
for( ALAsset *asset in assetArray ) {
NSDictionary *urlDictionary = [asset valueForProperty:#"ALAssetPropertyURLs"];
[urls addObject:urlDictionary];
}
The resulting array of dictionaries you can save in any way you like.
After restart of your app, you read the array of dictionaries back from where you stored it. Then Matej suggests to use ALAssetsLibrary's assetForURL:resultBlock:failureBlock: to recreate the ALAssets. But as we now know you want to put a checkmark on the original assets again, it is better to fetch the original array of ALAssets, and check whether any of them are present in the recovered urls. The following should work for that:
NSArray *assetArray = // the full array of ALAssets from AGImagePickerController
NSArray *urls = // the recovered array of NSDictionaries
for( ALAsset *asset in assetArray ) {
NSDictionary *urlDictionary = [asset valueForProperty:#"ALAssetPropertyURLs"];
if( [urls containsObject:urlDictionary] ) {
... // set checkmark on asset
}
}
This assumes the original assets have not changed, which is not under your control (the user has removed/added photos, for example).
This is the method I use for storing array or dictionary objects.
- (NSArray*)readPlist
{
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *plistPath = [[documentPaths lastObject] stringByAppendingPathComponent:#"filename.plist"];
NSFileManager *fMgr = [NSFileManager defaultManager];
if (![fMgr fileExistsAtPath:plistPath]) {
[self writePlist:[NSArray array]];
}
return [NSArray arrayWithContentsOfFile:plistPath];
}
- (void)writePlist:(NSArray*)arr
{
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *plistPath = [[documentPaths lastObject] stringByAppendingPathComponent:#"filename.plist"];
NSFileManager *fMgr = [NSFileManager defaultManager];
if ([fMgr fileExistsAtPath:plistPath])
[fMgr removeItemAtPath:plistPath error:nil];
[arr writeToFile:plistPath atomically:YES];
}
Hi I was wondering how can I call a method just for one time in application life ... My application should download some files from server and I need do it just for one time; I mean mean just one time per installation
here is my method
//Download some images from server and save it into directory
- (void) downloadCovers {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
[self saveFile:#"mag1" ofType:#"png" fromURL:#"http://myweb.com/mag1.png" inDirectory:documentsDirectory];
}
and this method set images as UIButton BG :
- (void)buttonsBGImage {
UIImage * bgMag1 = [self loadImage:#"mag1" ofType:#"png" inDirectory:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]];
[mag1 setBackgroundImage:bgMag1 forState:UIControlStateNormal];
NSLog(#"BG IS SET");
}
why not just testing if the file is exist or not in local storage!
//Download some images from server and save it into directory
- (void) downloadCovers {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *pathToImg = [NSString stringWithFormat:#"%#/mag1.png",documentsDirectory];
BOOL isExist = [[NSFileManager defaultManager]fileExistsAtPath:pathToImg];
if (!isExist) {
[self saveFile:#"mag1" ofType:#"png" fromURL:#"http://myweb.com/mag1.png" inDirectory:documentsDirectory];
}
}
You can't do it for a method, but you can do it for a function using pthread_once:
static pthread_once_t once = PTHREAD_ONCE_INIT;
pthread_once(& once, SomeFunction);
or you can execute a block once using dispatch_once (the most natural choice for your current implementation).
In some cases (not this one), you may also prefer to do your work in +initialize.
EDIT: Question was clarified
Just check for the file's existence, or use a preference if you want this to persist across multiple launches.
Set a flag as a NSUserDefaults key and check for this NSUserDefault value in your downloadCovers method. If it is already set, do nothing, else download files and set the flag to true.
Like so:
-(void) downloadCovers {
BOOL downloaded = [[NSUserDefaults standardUserDefaults] boolForKey: #"downloaded"];
if (!downloaded) {
//download code here
[[NSUserDefaults standardUserDefaults] setBool:YES forKey: #"downloaded"];
}
}
Cheers
- (void)buttonsBGImage {
if (!mag1.backgroundImage){
UIImage * bgMag1 = [self loadImage:#"mag1" ofType:#"png" inDirectory:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]];
[mag1 setBackgroundImage:bgMag1 forState:UIControlStateNormal];
NSLog(#"BG IS SET");
}
}
I am getting slightly frustrated with the DropBox API. It is supposed to be all simple and straight forward, but I have yet to come a across a simple and plain explanation of how to do a simple sync.
I followed all the instruction you can find in the readme which comes withe DropBox API. To test the whole thing, I have created two buttons to download and upload a file from or to my DropBox. The files are found in my app documents folder.
This works splendidly:
-(void) DBupload:(id)sender
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"MyExample.txt"];
// NSError *error;
[self.restClient uploadFile:#"MyExample.txt" toPath:#"/MyExamplePath" fromPath:filePath];
}
-(void) DBdownload:(id)sender
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"MyExample.txt"];
NSError *error;
[self.restClient loadFile:#"/myExamplePath/MyExample.txt" intoPath:filePath];
}
However, I am now wondering how to achieve a simply sync. Right now, I can manually upload and download. But what I need in order to sync is to:
find out if the MyExample.txt in my App's folder or in my DropBox folder is more recent
if the txt in the App's folder is more recent: drop it into dropbox (overriding the old), i.e. call my DBupload method
if the txt in the drop box is more recent: download it into the apps folder, i.e. call my DBdownload method
Perhaps I am just too dumb, but does dropBox detail somewhere how to achieve this rather simple and straight forward task?
I know that there is this but it doesn't really give any code samples.
Thanks for any suggestions.
EDIT
OK, so I figured that my first step is to find out the last modified date of MyExample.txt which is found in the dropBox.
I wrote a wonderful method called DBsync in which I simply put this command:
-(void) DBsync
{
[self.restClient loadMetadata:#"/myExamplePath"];
}
This calls the following method which gets the metadata. This was a suggested answer to this post, and I commented it a bit out so as to make it plain what is happening (if there are more dumb people like myself:
- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata {
NSLog(#"restClient:loadedMetadata function called");
NSEnumerator *e= [metadata.contents objectEnumerator]; // indexes files in dropbox?
DBMetadata *dbObject; // loads metadate we need, e.g. lastModifiedDated
int numberOfFiles = [metadata.contents count]; // counts files in DropBox - I guess we don't really need this
NSLog(#"numberOfFiles %i", numberOfFiles);
while ((dbObject = [e nextObject])) { // this goes through every single file in the DropBox
if (!dbObject.isDirectory) { // this determines whether we are talking about a file or a folder
NSString *fileName = [dbObject.path lastPathComponent]; // this puts the name of the last component, e.g. in /MyExamplePath/MyExample.txt = MyExample.txt into fileName
NSLog(#"File which is currently being checked: %#", fileName);
if ([fileName isEqualToString:#"MyExample.txt"]) {
NSLog(#"Found it: %#", fileName);
NSLog(#"Date last modified: %#", dbObject.lastModifiedDate);
/* to do: call dbupload if dbObject.lastModifiedDate > than your local file*/
}
}
}
}
I will post the next step once I managed to do so...
I think what you are looking for is the loadmetadata method. Here is an untested example:
- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata {
NSEnumerator *e= [metadata.contents objectEnumerator];
DBMetadata *dbObject;
numberOfFiles = [metadata.contents count];
while ((dbObject = [e nextObject])) {
if (!dbObject.isDirectory) {
NSString *fileName = [dbObject.path lastPathComponent];
if (![fileName isEqualToString:#"MyExample.txt"]) {
/* call dbupload if dbObject.lastModifiedDate > than your local file*/
}
}
}
You don't need an enumerator, just use the good old for... loop ;)
- (void)restClient:(DBRestClient *)client loadedMetadata:(DBMetadata *)metadata {
for (DBMetadata * child in metadata.contents) {
if (!child.isDirectory) {
NSString *fileName = [dbObject.path lastPathComponent];
if (![fileName isEqualToString:#"MyExample.txt"]) {
/* call dbupload if dbObject.lastModifiedDate > than your local file*/
}
}
}
}
I am writing an iPhone app – a client for some social network. The app support multiple accounts. Info about accounts are stored in a keyed archive.
A method used for saving:
- (void) saveAccounts {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];
path = [path stringByAppendingPathComponent:#"accounts.bin"];
// NSMutableArray *accounts = ...;
[NSKeyedArchiver archiveRootObject:accounts toFile:path];
}
A method uses for reading:
- (NSMutableArray *) loadAccounts {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];
path = [path stringByAppendingPathComponent:#"accounts.bin"];
NSMutableArray *restoredAccounts = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
return [restoredAccounts retain];
}
The method saveAccounts is used only if some account is added/modified/deleted. The method loadAccounts is used every time the app starts. There isn't any other code that access this file.
I and one of my testers get an issue. At some moment the starts to act like accounts.bin is missing. If loadAccounts returns nil, the app offers to add an account. After I enter an account (the app must call saveAccounts), the app works normally, but when I launch it again, it asks me to add account again. The only solutions is too reinstall the app to iPhone, after reinstall it works for some time with any troubles.
We (I and my tester who get an issue) use iPhone 3G with 3.1.2.
An another tester who didn't experience this issue on his iPhone 3GS with 3.1.2.
Any ideas why this file disappears?
update
I found bug in my code. There was a code that deletes whole Document directory. Because this part of a code is a remote server related, it was hard to trace this code. Bug appeared under very rare conditions only.
Now the bug is found, and the code is corrected. wkw's answer didn't solved my problem, but it forced me to dig deeper. Thank you!
How about -- as a debugging device --verifying the contents of your Documents directory in loadAccounts or at least whenever the unarchiver returns nil. There's some code below to get the names of files in a directory. Set a breakpoint and just dump the NSArray to view the items.
If you can see that the file exists in the Docs dir, then your problem is elsewhere. Perhaps it did not successfully archive. check the return value on the call to archive the data:
if( ! [NSKeyedArchiver archiveRootObject:accounts toFile:path] ){
NSLog(#"Oooops! account data failed to write to disk");
}
Get names of files in directory:
- (NSArray*) directoryContentsNames:(NSString*) directoryPath {
NSArray* result;
{
result = [[NSFileManager defaultManager]
directoryContentsAtPath: directoryPath];
}
if( result && [result count] > 0 ){
NSMutableArray *items = [[[NSMutableArray alloc] init] autorelease];
for( NSString *name in result ){
if( ! [name isEqualToString:#".DS_Store"] )
[items addObject: name];
}
result = items;
}
return result;
}
Perhaps NSUserDefaults might be easier to use?
Also, is there a reason to use Keyed(Un)Archiver instead of NSArray's writeToFile?
if( ! [accounts writeToFile:path atomically:YES] )
; // do something since write failed
and to read the file in:
NSMutableArray *accounts = [[NSArray arrayWithContentsOfFile:path] mutableCopy];