iOS Data Storage Guidelines Rejection - ios

My app was rejected cause it must follow the iOS Data Storage Guidelines. I have already read some answer here on stackoverflow, and i have already read some blogs... I know my problem, at first application launch i copy 1 sqlite db and unzip some images in documents folder. The problem it's icloud that automaticcaly backup any files in documents directory. I don't need to use icloud in my app but my files must remain in document folder because they are the base data of my application and must be persit (cache folder or temp folder aren't correct solutions). So i've read about a flag that i can set file by file to forbid the backup :
[URL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
I have also read that this method works only in ios higher than 5.0.1, in the other it will be simply ignored (i have setted my ios deployment target to 4.3)... If i use this method my app will be rejected again because the older ios devices backup aren't managed? If yes there is a cross-iosversion method to set the NSURLIsExcludedFromBackupKey?
EDIT
I'm sorry icloud isn't present in ios earlier 5.0 so i think that the problem regards only differences beetween version 5.0 and 5.0.1, I'm wrong?

I have passed all files that will stored in documents folder through this method.
Try this
-(BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
const char* filePath = [[URL path] fileSystemRepresentation];
const char* attrName = "com.apple.MobileBackup";
if (&NSURLIsExcludedFromBackupKey == nil) {
// iOS 5.0.1 and lower
u_int8_t attrValue = 1;
int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0);
return result == 0;
}
else {
// First try and remove the extended attribute if it is present
int result = getxattr(filePath, attrName, NULL, sizeof(u_int8_t), 0, 0);
if (result != -1) {
// The attribute exists, we need to remove it
int removeResult = removexattr(filePath, attrName, 0);
if (removeResult == 0) {
NSLog(#"Removed extended attribute on file %#", URL);
}
}
// Set the new key
NSError *error = nil;
[URL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:&error];
return error == nil;
}
}
I've found this method here in Stack Overflow:
Use NSURLIsExcludedFromBackupKey without crashing on iOS 5.0

NOTE:
do not use
NSString *mediaDir = [NSString stringWithFormat:#"%#/Library/Caches/%#", NSHomeDirectory(), MEDIA_DIRECTORY];
since you are making assumptions about the name.
rather, use:
NSArray* lCachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString* lCacheDirectory = [lCachePaths objectAtIndex:0];

In my app I solved it by adding the files (images in my case) to the bundle,
and check in the method where I load the images if they exist in the document folder where the user generated data / images will be placed.
If it isn't there, I know it's one of my prepopulated images and I load it from the bundle.
For the sqlite file:
Not a really satisfying solution, but I create the data in code now, instead of a prepopulated sqlite file.

For media files, we store them in the Caches/ directory. During application startup, we check if they are there. Otherwise the images are either loaded from the bundle (in your case unzipped) or in our case re-downloaded from the Server.
The exact location is
NSString *mediaDir = [NSString stringWithFormat:#"%#/Library/Caches/%#", NSHomeDirectory(), MEDIA_DIRECTORY];
which is the place where you SHOULD store your temporary files. The files in the cache are generally persistent, much more so than in Temp, but can be deleted at some point.

Related

Xamarin iOS DocumentPicker: How to import large files?

In the DidPickDocument event of UIDocumentPickerViewController I try to import/write the selected file into the app's local Documents directory.
This works fine with "small" files (e.g < 100MB) using a subclassed UIDocument class with overriden
public override bool LoadFromContents(NSObject contents, string typeName, out NSError outError)
{
outError = null;
if (contents != null)
{
Content = ((NSData)contents).ToArray();
}
...
...and by calling
MySubclassedDoc mySubclassedDoc = new MySubclassedDoc (nsurl);
bool success = await mySubclassedDoc.OpenAsync();
File.WriteAllBytes("targetFile.xyz", mySubclassedDoc.Content);
But if the file is larger (eg. 400MB) the app crashes before LoadFromContents is called because of insufficent memory (RAM).
So there need to be a way to stream the selected file directly to a file.
How can you do this using the given NSUrl ?
Since you have got the url, there's no need to convert it to NSData then store the data to a file. We can just use this url with NSFileManager like:
url.StartAccessingSecurityScopedResource();
string docPath = NSSearchPath.GetDirectories(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomain.User)[0];
string filePath = Path.Combine(docPath, "fileName.type");
NSError error;
NSFileManager fileManager = new NSFileManager();
fileManager.Copy(url.Path, filePath, out error);
url.StopAccessingSecurityScopedResource();
In this way we can store the file to our app's own document directory.
why don’t you just clone the file over to your Documents directory before reading it instead of deserializing the contents and reserializing them?
your code seems really inefficient. If your files can be 400MB you should clearly not load all the contents into memory. I guess you must have very large binary objects in the files if they can be 400MB ; try mmapping the file and storing pointers to the individual objects instead?

How to view the sqlite db in the application installed in iOS for testing

I have installed my application in the simulator and need to view the DB. Please tell me the application to view this or can I view it from Xcode itself.
DATABASE
//database connection
con = [[DataBase alloc]init];
NSError *error = [[NSError alloc]init];
NSFileManager *filemanager = [NSFileManager defaultManager];
NSArray *arryPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *DocumentPath = [arryPath objectAtIndex:0];
NSString *strDocumentPath = [DocumentPath stringByAppendingPathComponent:#"School.sqlite"];
// check file is exist or not
int success = [filemanager fileExistsAtPath:strDocumentPath];
//if file not exist at path
if (!success) {
NSLog(#"SchoolName is: %#",#"No Database exist");
NSString *strDefaultPath = [[[NSBundle mainBundle]resourcePath]stringByAppendingPathComponent:#"School.sqlite"];
success = [filemanager copyItemAtPath:strDefaultPath toPath:strDocumentPath error:&error];
if (!success) {
NSAssert1(0, #"Failed to create writable database file with message '%#'.", [error localizedDescription]);
}
}
//file exist at path
if (success) {
NSLog(#"SchoolName is: %#",#" Database exist");
if (sqlite3_open([strDocumentPath UTF8String],&database) == SQLITE_OK) {
} else {
sqlite3_close(database);
NSAssert1(0, #"Failed to open database with message '%s'.", sqlite3_errmsg(database));
}
}
Install DB Browser for Sqlite in Mac.
Provide path of .sqlite in documents directory of app to DB browser
It'll open live db from simulator
All entities of Core Data will have ZTablename naming convention
Keep refreshing DB browser for updated data during testing
Try to use the client e.g. from here http://sqlitebrowser.org/ and open sqlite file.
If you installed firefox on your PC, use "SQLite Manager" add-on is quite good, it's easy, lightly, and free. I've used it before to manage my sqlite db with ~40k records with no problem.
https://addons.mozilla.org/en-US/firefox/addon/sqlite-manager/
For live database Changes please try this:
1.Install DBBrowser for SQLite.
2.Log your strDocumentPath in console using NSLog And copy the path.
3.Open DBBrowser and open database and in finder press CMD+SHIFT+G and paste your link there and press ENTER. You will be redirected to that location.
4.Select your SQLite file and it will be opened in DBBrowser. And keep it there until you are done with your DB works.
Now whenever you refresh the database from DBBrowser you will get the recent changes in DB also any manual changes in DB will be effected in your Simulator.
Here is a way I do that
init() {
do {
// Get database path
if let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
// Connect to database
db = try Connection("\(path)/db.sqlite3")
// Initialize tables
try db?.run(rssNewspaper.create(ifNotExists: true) { table in
table.column(rssNewspaperId, primaryKey: .autoincrement)
table.column(rssNewspaperName)
table.column(rssIsActive)
})
}
} catch {
print(error.localizedDescription)
}
}
point debuger point in path and when deice is locked there print to path then open terminal and use this command "open (path/of/printed/path)" in between( ) past the printed value it will be open

Reuse core data created .db file in FMDB

I'm using Core Data to manage local DB in my iOS app. But then I have a requirement to insert bulk of records to local DB. So I choose FMDB to perform bulk insertion. But when I tried to insert the record I'm getting this DB Error: 1 "no such table: Table Name.
But when I cross checked, the local DB exist there with all the tables that I created in xcdatamodel.
- (FMDatabase *)dbInMainThread{
if (_dbInMainThread) {
return _dbInMainThread;
}
NSURL *storeURL = [[[CoreDataManager manager] applicationLibraryDirectory] URLByAppendingPathComponent:[DATABASE_NAME stringByAppendingString:#".sqlite"]];
[self checkDB:[storeURL relativePath]];
_dbInMainThread = [FMDatabase databaseWithPath:[storeURL relativePath]];
return _dbInMainThread;
}
-(void) checkDB:(NSString *)dbpath{
BOOL success;
NSFileManager * fm = [NSFileManager defaultManager];
success = [fm fileExistsAtPath:dbpath];
if (success) return;
}
Success bool in the above code snippet is always returning true.
I followed the steps in this SO answer.
Since I am using Core Data, I don't need to add the DB file to the project. I uninstalled the app, resets the simulator and verified the DB file was in the correct folder before starting insertion. But nothing helps.
What am I missing here
Thanks

Why does SecPKCS12Import automatically add SecIdentities to the Keychain?

The documentation on SecPKCS12Import states the following:
[…] You can then use the Keychain Services API (see Keychain Services
Reference) to put the identities and associated certificates in the
keychain.
This means that the items returned in the “items” argument (3rd argument of that function) should not be automatically added to the keychain. However, I have found that those items are automatically added to the keychain when using that function. If I try to add them using SecItemAdd, I get errSecDuplicateItem.
Is this a bug or should it be this way? Why are the items automatically added?
Here is some sample code:
NSDictionary *options = [[NSDictionary alloc] initWithObjectsAndKeys:#"password", (id)kSecImportExportPassphrase, nil];
CFArrayRef items_ = NULL;
OSStatus ret = SecPKCS12Import((CFDataRef)pkcs12data /* get this from somewhere … */, (CFDictionaryRef)options, &items_);
If you use that code and then open Keychain Access, you’ll see that the certificate and the private key have been added to the keychain.
Regards,
David.
It seems like Apple's documentation may be out of date for that link (SecPKCS12Import), because this link https://developer.apple.com/library/ios/qa/qa1745/_index.html mentions that "reading in a PKCS#12-formatted blob and then importing the contents of the blob into the app's keychain using the function SecPKCS12Import..."
Going by the document revision dates, QA1745 is more recent than the Certificate, Key, and Trust Services Reference.
Is this a bug or should it be this way?
It isn't a bug, the documentation is only incorrect. Well, it is correct for iOS, it is just incorrect for macOS.
Why are the items automatically added?
This is caused by the way how this function is implemented in macOS. The comments in the implementation reveal the cause:
// SecPKCS12Import is implemented on Mac OS X in terms of the existing
// SecKeychainItemImport API, which supports importing items into a
// specified keychain with initial access control settings for keys.
SecPKCS12Import in macOS is just a wrapper around SecKeychainItemImport and as the function name implies, this function imports into a keychain. This also explains the following code:
if (!importKeychain) {
// SecKeychainItemImport requires a keychain, so use default
status = SecKeychainCopyDefault(&importKeychain);
}
On iOS the function is implemented standalone and not as a wrapper since SecKeychainItemImport isn't even available on iOS.
But there is a way round that if that is a problem for you. Many people work around it by creating a temporary keychain, which will never be visible to the system or the user (and thus also not be visible to apps or in Keychain Access), of course, this will work too, but is a bit of an ugly hack.
Better: Use SecItemImport and as import format use kSecFormatPKCS12, then you also get the parsed identities but nothing is imported anywhere unless you request that.
SecPKCS12Import does NOT add items to the keychain. However, it WILL look in the keychain to see if imported items are already there. If it finds existing items, they will be returned for the SecIdentityRef (SecCertificateRef and SecKeyRef). This is why you can get errSecDuplicateItem when calling SecItemAdd after calling SecPKCS12Import.
When debugging, you might want to remove everything in your keychain using code like this:
void _EraseKeychain()
{
NSMutableArray *toDelete = [NSMutableArray array];
NSArray *classes = #[(__bridge id)kSecClassCertificate,
(__bridge id)kSecClassKey,
(__bridge id)kSecClassIdentity,
(__bridge id)kSecClassInternetPassword,
(__bridge id)kSecClassGenericPassword];
NSMutableDictionary *query = [NSMutableDictionary dictionary];
query[(id)kSecClass] = (__bridge id)kSecClassIdentity;
query[(id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
query[(id)kSecReturnPersistentRef] = #YES;
id class;
for( class in classes )
{
query[(__bridge id)kSecClass] = class;
CFTypeRef items = nil;
OSStatus result = SecItemCopyMatching((__bridge CFDictionaryRef)query, &items);
if( result == errSecSuccess )
{
[toDelete addObjectsFromArray:(__bridge NSArray*)items];
CFRelease(items);
}
}
id deleteRef;
for( deleteRef in toDelete )
{
NSString *objectKind = #"unknown";
if( CFGetTypeID(deleteRef) == CFDataGetTypeID() )
{
objectKind = [[NSString alloc] initWithUTF8String:(char *)[(__bridge NSData*)deleteRef bytes]];
}
NSDictionary *delRequest = #{(id)kSecValuePersistentRef:deleteRef};
OSStatus deleteResult = SecItemDelete((__bridge CFDictionaryRef)delRequest);
if( deleteResult == errSecSuccess )
NSLog(#"Deleted item(%#) with persistent ref %#", objectKind, deleteRef);
else if( deleteResult == errSecItemNotFound )
NSLog(#"Already deleted item(%#) with persistent ref %#", objectKind, deleteRef);
else
NSLog(#"Can't delete keychain item(%#) with persistent ref %#", objectKind, deleteRef);
}
}

Keychain iOS not always storing values

I´m developing a couple of iOS applications and i need to share a element between them, that i want to store in the keychain.
This element is used in a complex login process with 3 or 4 steps, in each one i need to read the value from the keychain, to do this i used the code bellow:
- (NSString *)installationToken
{
KeychainItemWrapper *kw = [[KeychainItemWrapper alloc] initWithIdentifier:#"uuid" accessGroup:#"yyyyy.xxxxxxxxxxx"];
if (![kw objectForKey:(NSString*)kSecAttrAccount] || [[kw objectForKey:(NSString*)kSecAttrAccount] isEqualToString:#""]) {
NSString *result;
CFUUIDRef uuid;
CFStringRef uuidStr;
uuid = CFUUIDCreate(NULL);
assert(uuid != NULL);
uuidStr = CFUUIDCreateString(NULL, uuid);
assert(uuidStr != NULL);
result = [NSString stringWithFormat:#"%#", uuidStr];
assert(result != nil);
CFRelease(uuidStr);
CFRelease(uuid);
[kw setObject:result forKey:(NSString*)kSecAttrAccount];
return result;
} else {
return [kw objectForKey:(NSString*)kSecAttrAccount];
}
}
This all works well in almost every device but in some, users are complaining. So, i checked what my server is receiving, and saw that different values are being sent.
I checked the code and in no other place i'm acessing/emptying this keychain element, what can be wrong with this? For the majority of devices this works like a charm but for some reason, in some devices, they aren't storing or retrieving well from the keychain.
The problem happens in different invocation in the same application.
If you are using Apples' sample code for KeyChainWrapper, then main problem is sometimes randomly, SecItemCopyMatching fails and then the sample code has resetKeychainItem which will basically reset your keychain.
if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)
{
// Stick these default values into Keychain if nothing found.
[self resetKeychainItem];
}
In our app, we noticed similar problems, and so now we are using
https://github.com/carlbrown/PDKeychainBindingsController to do all keyChain related functionality. Now it works very well.

Resources