12

I have an app with a pre-filled .sqlite file that is copied into the user's Documents directory when the app is first opened. This file is 12.9MB. Twice now, my app has been rejected since changing target to iOS5 with this rejection note:

Binary Rejected Apr 24, 2012 10:12 AM

Reasons for Rejection:

2.23 Apps must follow the iOS Data Storage Guidelines or they will be rejected

Apr 24, 2012 10:12 AM. From Apple.

2.23

We found that your app does not follow the iOS Data Storage Guidelines, which is required per the App Store Review Guidelines.

In particular, we found that on content download, your app stores 12.81 MB. To check how much data your app is storing:

  • Install and launch your app
  • Go to Settings > iCloud > Storage & Backup > Manage Storage
  • If necessary, tap "Show all apps"
  • Check your app's storage

The iOS Data Storage Guidelines indicate that only content that the user creates using your app, e.g., documents, new files, edits, etc., may be stored in the /Documents directory - and backed up by iCloud.

Temporary files used by your app should only be stored in the /tmp directory; please remember to delete the files stored in this location when the user exits the app.

Data that can be recreated but must persist for proper functioning of your app - or because customers expect it to be available for offline use - should be marked with the "do not back up" attribute. For NSURL objects, add the NSURLIsExcludedFromBackupKey attribute to prevent the corresponding file from being backed up. For CFURLRef objects, use the corresponding kCFURLIsExcludedFromBackupKey attribute.

For more information, please see Technical Q&A 1719: How do I prevent files from being backed up to iCloud and iTunes?.

It is necessary to revise your app to meet the requirements of the iOS Data Storage Guidelines.

I have tried setting the "do not back up" attribute as recommended in the Data Storage Guidelines, but is was rejected again.

I do not use iCloud in my app, and Settings > iCloud > etc. shows no usage at all.

I cannot use the Caches or tmp directories as the database is modified by the user after creation.

I seem to be between a rock and a hard place here with Apple not allowing this kind of app to function at all.

Has anyone had this problem and managed to overcome it?

EDIT 17-5-12 I still haven't managed to get this app approved yet. Has anyone managed to do this?

EDIT 1-7-12 My app has just been rejected again for the same reason. I am at a loss as to what to do here, as surely it is a common use scenario.

EDIT 11-9-12 App now approved - please see my solution below. I hope it can help someone else.

Community
  • 1
  • 1
colincameron
  • 2,696
  • 4
  • 23
  • 46
  • I'm facing this issue as well. It concerns me that you've been rejected again. Did you try to set the attribute on the entire folder? I'm worried that Apple are just rejecting data models that aren't Core Data. If you have any more information please post your findings. – Lee Probert Jul 02 '12 at 12:06
  • I've been rejected 3 times for this. I haven't tried setting the attribute to the whole documents folder as it shouldn't make a difference. Has your app been rejected for this reason? – colincameron Jul 02 '12 at 12:14
  • Yes. Have you considered saving your DB file to the /Library instead? Maybe it's just because it is in the /Documents folder and this is ONLY for User generated content. It should be possible to write your own /Database folder. – Lee Probert Jul 02 '12 at 13:41
  • @LeeProbert would I be allowed to use the /Library folder for user-generated content? The problem is that the one file is a mixture of both. – colincameron Jul 03 '12 at 14:56
  • I noticed that the Google Analytics SDK for iOS was updated with their SQL lite file being moved from /Documents to /Library. I've just re-submitted my app with the same change. Will let you know how it goes. – Lee Probert Jul 04 '12 at 12:53
  • Have just been rejected again ... same problem. Looks like /Library aint gonna work. Will try /Caches now. – Lee Probert Jul 12 '12 at 18:32
  • Finally got the app approved after moving everything to Caches. – Lee Probert Jul 25 '12 at 09:57
  • Thanks for the update, but Caches isn't going to work for me, as it can be cleared at any time. – colincameron Jul 31 '12 at 10:27
  • Have you asked for clarification or appealed it? I just got rejected for this and I can't afford to continue getting rejected for this same thing (every 12 days). I'm curious about what all you've tried. I'm making a manager that determines where to save the data based on which OS they have. And it sets the do not copy IF it can. Only 5.0 has the problem of BOTH required to use caches which will flush under 5.0, PLUS no way to set the attribute. 4.x the caches won't empty and 5.0.1 and up you can set the attr. So I store data in caches in 5.0 and under, and in documents 5.0.1 and up. – badweasel Aug 01 '12 at 07:09
  • "@LeeProbert would I be allowed to use the /Library folder for user-generated content? The problem is that the one file is a mixture of both." This might be your problem. Put the user data in a different file than the non-user data. I'm having to do this as well. – badweasel Aug 01 '12 at 07:17
  • By the way... both Lee and c.cam.. how MUCH data are each of you talking about here? Sorry to post so many comments and questions but I'm just working out my solution and don't want to be rejected again. So I'm wondering if maybe it's a size issue as much as a where issue. – badweasel Aug 01 '12 at 07:34
  • In my case it was a DB file of around 60mb. It gets synced by the app using a bundled version so if it is to be cleaned it will just have to sync again. – Lee Probert Aug 01 '12 at 11:07
  • I've appealed the rejection and I'm awaiting a phone call from Apple to discuss it. The data is around 12MB. As it is a Core Data .sqlite file, I can't really separate the user data from the existing data. I will report back on what Apple say on the matter. – colincameron Aug 01 '12 at 13:02
  • Thanks.. I built a fileManager.. I don't know what else to call it.. Maybe it's more of a directoryManager.. but it checks the OS, creates your folder in the appropriate location, and if possible sets that do not backup bit. So instead of getting the library path the old way, you ask this for the path to your stuff. When done it will also handle copying data from the old path to the new after an OS upgrade. I'm thinking of releasing the code open source. If I did this would I get your checkmark? – badweasel Aug 02 '12 at 21:31

4 Answers4

9

OK, here is the solution I managed to get approved (finally!)

This is the code for setting the Skip Backup attribute - note that it is different for 5.0.1 and below and 5.1 and above.

#include <sys/xattr.h>
- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
    if (&NSURLIsExcludedFromBackupKey == nil) { // iOS <= 5.0.1
        const char* filePath = [[URL path] fileSystemRepresentation];

        const char* attrName = "com.apple.MobileBackup";
        u_int8_t attrValue = 1;

        int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0);
        return result == 0;
    } else { // iOS >= 5.1
        NSError *error = nil;
        [URL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:&error];
        return error == nil;
    }
}

And here is my persistentStoreCoordinator

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"store.sqlite"];

    NSError *error;

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *storePath = [[[self applicationDocumentsDirectory] path] stringByAppendingPathComponent:@"store.sqlite"];

    // For iOS 5.0 - store in Caches and just put up with purging
    // Users should be on at least 5.0.1 anyway
    if ([[[UIDevice currentDevice] systemVersion] isEqualToString:@"5.0"]) {
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        NSString *cacheDirectory = [paths objectAtIndex:0];
        NSString *oldStorePath = [storePath copy];
        storePath = [cacheDirectory stringByAppendingPathComponent:@"store.sqlite"];
        storeURL = [NSURL URLWithString:storePath];

        // Copy existing file
        if ([fileManager fileExistsAtPath:oldStorePath]) {
            [fileManager copyItemAtPath:oldStorePath toPath:storePath error:NULL];
            [fileManager removeItemAtPath:oldStorePath error:NULL];
        }
    }
    // END iOS 5.0

    if (![fileManager fileExistsAtPath:storePath]) {
        // File doesn't exist - copy it over
        NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:@"store" ofType:@"sqlite"];
        if (defaultStorePath) {
            [fileManager copyItemAtPath:defaultStorePath toPath:storePath error:NULL];
        }
    }

    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    [self addSkipBackupAttributeToItemAtURL:storeURL];

    return __persistentStoreCoordinator;
}

Note that I made the decision to just store in Caches and put up with purging for iOS 5.0 users.

This was approved by Apple this month.

Please don't copy and paste this code without reading and understanding it first - it may not be totally accurate or optimised, but I hope it can guide someone to a solution that helps them.

colincameron
  • 2,696
  • 4
  • 23
  • 46
4
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v)  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#include <sys/xattr.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

//Put this in your method

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSURL *pathURL= [NSURL fileURLWithPath:documentsDirectory];

    iOS5 = NO;
    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"5.0.1")) {
        iOS5 = YES;
    }

    // Set do not backup attribute to whole folder
    if (iOS5) {
        BOOL success = [self addSkipBackupAttributeToItemAtURL:pathURL];
        if (success) 
            NSLog(@"Marked %@", pathURL);
        else
            NSLog(@"Can't marked %@", pathURL);
    }
}

- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
    const char* filePath = [[URL path] fileSystemRepresentation];
    const char* attrName = "com.apple.MobileBackup";
    u_int8_t attrValue = 1;
    int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0);
    return result == 0;
}
colincameron
  • 2,696
  • 4
  • 23
  • 46
Developer
  • 49
  • 2
  • Thanks for the answer - BTW indent code with 4 spaces to format it correctly. Are you saying I should set the attribute to the whole documents folder? This is the exact method I am using, except I am applying it to the file. – colincameron Apr 25 '12 at 11:48
  • Also.. this code doesn't support 5.1 and up. You are only supposed to use that method to set the attribute in 5.0.1. – badweasel Aug 01 '12 at 07:11
1

You said that you don't use iCloud. In that case, you should simply move your sqlite file to a directory with suffix .nosync. That should do it!

NSString *dataFileName = @"my.sqlite";
NSString *dataFileDirectoryName = @"Data.nosync";
NSString *documentsDirectoryPath = [self applicationDocumentsDirectory];
NSFileManager *fileManager = [NSFileManager defaultManager];

if ([fileManager fileExistsAtPath:[documentsDirectoryPath stringByAppendingPathComponent:dataFileDirectoryName]] == NO) {
    NSError *fileSystemError;
    [fileManager createDirectoryAtPath:[documentsDirectoryPath stringByAppendingPathComponent:dataFileDirectoryName]
           withIntermediateDirectories:YES
    attributes:nil
         error:&fileSystemError];

    if (fileSystemError != nil) {
        NSLog(@"Error creating database directory %@", fileSystemError);
    }
}

NSString *dataFilePath = [[documentsDirectoryPath
                           stringByAppendingPathComponent:dataFileDirectoryName]
                          stringByAppendingPathComponent:dataFileName];

// Move your file at dataFilePath location!

HTH.

Mustafa
  • 20,504
  • 42
  • 146
  • 209
  • Thanks for the suggestion. Can you point me to the documentation for the `.nosync` suffix? I haven't heard that one before. – colincameron Aug 02 '12 at 13:46
  • 1
    Check "Guidance for Library-style Applications" section in Using Core Data with iCloud Release Notes. http://developer.apple.com/library/ios/#releasenotes/DataManagement/RN-iCloudCoreData/_index.html – Mustafa Aug 03 '12 at 05:10
  • That sounds like your answer to me. – badweasel Aug 04 '12 at 07:19
0

I've been looking into this and have found this very interesting article on the subject : http://iphoneincubator.com/blog/data-management/local-file-storage-in-ios-5

I believe that you will continue to be rejected if you try to use the /Documents folder to store your DB file.

I would suggest you bite the bullet and use /Cache. The worst user case scenario would be that their device runs low on memory and the app gets cleaned removing the DB file. In this case when your app launches again it should copy over the bundled DB file into /Cache and the user would sync to go and grab the excess data from your remote server. That is assuming this is how your app works.

To get your app to move your DB file from /Documents to /Cache you can just tell NSFileManager to do this for you ...

#define FILE_MANAGER     [NSFileManager defaultManager]
#define DOCUMENTS_PATH   [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex: 0]
#define CACHES_PATH      [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex: 0]
#define DB_DOCS_PATH     [DOCUMENTS_PATH stringByAppendingPathComponent: @"database.db"]
#define DB_PATH          [CACHES_PATH stringByAppendingPathComponent: @"database.db"]

if([FILE_MANAGER moveItemAtPath: DB_DOCS_PATH toPath:DB_PATH error:&error])
    {
        NSLog(@"SUCCESSFULLY MOVED %@ to %@",DB_DOCS_PATH,DB_PATH);
    }

This will prevent your existing users from using their iCloud storage unnecessarily.

Lee Probert
  • 10,308
  • 8
  • 43
  • 70
  • I noticed that the Google analytics iOS SDK also copied its sql file to the /Documents folder so I had a look to see if there was an update. There was ... http://feeds.feedburner.com/ga-dev-changelog-collection-ios They are now saving to the /Library and not in the /Caches folder but just in the root. – Lee Probert Jul 02 '12 at 16:22
  • Unfortunately losing data is not an option for my app. The app ships with a number of pieces of sheet music/chord sheets and the user can create playlists of these for performance. These playlists can be saved and recalled later, so I don't want the data to be deleted. – colincameron Jul 03 '12 at 14:54
  • is everything they save on the device though, or are you syncing to a remote server as well? If you aren't then I would consider it anyway. Check out Parse.com as an option for this. Then you can safely clean cache and have it re-sync from the remote server. – Lee Probert Jul 04 '12 at 12:56
  • Does saving to the /Library folder remove the need to flag for not backing up? – Eric Jul 11 '12 at 06:17
  • I believe iTunes backs up the /Library folder but am not sure if this is the same as iCloud backing up your /Documents folder and using your storage space. – Lee Probert Jul 11 '12 at 16:21
  • Eric.. no it does not. Only the caches and tmp folder don't backup, unless the attribute is set. – badweasel Aug 01 '12 at 07:13