How can I change .plist entries based on my Scheme? - ios

I have some App-Info.plist entries that need to change based on my environment. When I'm doing developmental work, they need to be one set of values, vs QA vs Production.
What would be nice is if I could simply have a script or something that runs based on the Scheme used to do the compilation.
Is this possible?

You can do it by performing some extra steps:
Duplicate [AppName]-Info.plist file by any name. Example: [AppName]-Info-dev.plist or [AppName]-Info-staging.plist etc
Map newly created .plist file in App's Target Settings. Get idea from following screenshot:
At the end, if you want to get some entry from .plist file then you need to get it like: [[[NSBundle mainBundle] infoDictionary] objectForKey:#"baseUrl"]
Project setting will automatically pick correct .plist file and give you required value.

I think msmq's answer is valid, but you should be a little careful about using the main info.plist this way. Doing that suggests that all your versions of info.plist are almost identical, except for a couple of differences. That's a recipe for divergence, and then hard-to-debug issues when you want to add a new URI handler or background mode or any of the other things that might modify info.plist.
Instead, I recommend you take the keys that vary out of the main info.plist. Create another plist (say "Config.plist") to store them. Add a Run Script build phase to copy the correct one over. See the Build Settings Reference for a list of variables you can substitute. An example script might be:
cp ${SOURCE_ROOT}/Resources/Config-${CONFIGURATION}.plist ${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Config.plist
Then you can read the file using something like this (based on Read in the Property List):
NSString *baseURL;
NSString *path = [[NSBundle mainBundle] pathForResource:#"Config" ofType:#"plist"];
NSData *plistXML = [[NSFileManager defaultManager] contentsAtPath:plistPath];
NSString *errorDesc = nil;
NSDictionary *dict = (NSDictionary *)[NSPropertyListSerialization
propertyListFromData:plistXML
mutabilityOption:NSPropertyListImmutable
format:NULL
errorDescription:&errorDesc];
if (dict != nil) {
baseUrl = dict[#"baseURL"];
} else {
NSAssert(#"Could not read plist: %#", errorDesc); // FIXME: Return error
}
There are other solutions of course. I personally generally use the preprocessor for this kind of problem. In my build configuration, I would set GCC_PREPROCESSOR_DEFINITIONS to include BaseURL=... for each build configuration and then in some header I would have:
#ifndef BaseURL
#define BaseURL #"http://default.example.com"
#endif
The plist way is probably clearer and easier if you have several things to set, especially if they're long or complicated (and definitely if they would need quoting). The preprocessor solution takes less code to process and has fewer failure modes (since the strings are embedded in the binary at compile time rather than read at runtime). But both are good solutions.

You can add a User-Defined-Setting in Xcode>Build-Settings, add its values according to all the schemes listed there. And then simply use that as a variable in Info plist file. That should work just fine.
This way you can avoid creating duplicate plist files, just for the sake of one or two different properties.

Related

Large files downloaded to Documents and backed up to iCloud

I have an iOS app in the app store that can download relatively large files that need to stay on the device for offline use. Those files are currently stored in the app's Documents folder but I'm just now reading that the Documents folder is backed up and should really only be used for user-generated content. This Apple technical Q&A states that the NSURLIsExcludedFromBackupKey should be set to prevent backup. This states that an app's /Library/Caches is the right place to put these kinds of files although further reading suggests that the folder may be cleared when the device is low on storage which is unacceptable for this app. I believe /Library/Application Support/ is then the best location for them -- does this sound right?
Unfortunately, this mistake got through the app review process. What are some best practices for fixing this now that people are using the app and already have some files persisted to the Documents folder and to their backups? It seems I need to move all the existing files and set their NSURLIsExcludedFromBackupKey on app update. How do I guarantee that this is done exactly once and that it isn't interrupted? Is moving the files out of the Documents folder important or could I leave them there? Will changing the files' backup status remove them from existing backups?
I'm using Swift 2.1.1 and targeting iOS 8.0+.
As stated in the technical Q&A, you best bet could be create a subdirectory in the Documents, and exclude that subdirectory once.
I don't believe you can write a 'do it once and be sure it is done' routine, since you can't guarantee your app doesn't crash while it is running. You certainly could set a completion flag when you are sure it is done so that once it is done you don't have to run it again.
Exclude your directory from backup, not the individual files.
From Xcode:
You can use this property to exclude cache and other app support files which are not needed in a backup. Some operations commonly made to user documents cause this property to be reset to false; consequently, do not use this property on user documents.
Here is the strategy I have used with good results
(sorry, its in objective-c -- I'm not a swift guy. Hopefully it will give you the idea):
- (BOOL)moveAllFiles{
// Searches through the documents directory for all files ending in .data
BOOL success = true;
NSString *myNewLocation = #"/store/my/files/here/now";
// Get the documents directory
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
// Get all files ending in .data (whatever your file type is)
NSArray *dataFilesArray = [NSArray arrayWithArray:[NSBundle pathsForResourcesOfType:#"data" inDirectory:documentDirectory]];
// If you have multiple resource types, use this line once each for each resource type, then append the arrays.
// Iterate the found files
NSString *fileName = [NSString string];
for (int i=0; i<[dataFilesArray count]; i++) {
fileName = [[dataFilesArray objectAtIndex:i] lastPathComponent];
// Move file, set success to false if unsuccessful move
if (![[NSFileManager defaultManager] moveItemAtPath:[dataFilesArray objectAtIndex:i]
toPath:[myNewLocation stringByAppendingPathComponent:fileName]
error:nil]) {
success = false; // Something went wrong
}
}
return success;
}
Now use the value of success to set a key in the user defaults file. Check for that key on startup. If it is false (or absent), run this routine (again).
This example is with file paths. You can do the same thing with file URLs if you wish.

How to create dev/staging/production environments

I'm trying to create an Xcode project that works in a development environment (using the computer as a local host), staging environment (a specific endpoint on a server), and a production environment (another specific server endpoint). I have found a few different tutorials about how to set this up. Some solutions suggest creating different target, some suggest creating different configurations. What is the most effective way to do this?
Following are the steps to create different schemes
Select the target
Create new scheme and give specific name to scheme (Dev,Staging,Prod,Debug)
i have attached screen shot for your reference
Sample Screen Shot for reference
if you want to set different url for different schemes then do the following steps:
1.Go to Build Setting
2.Go to user defined section
3.Create macros and specify urls for each environment
1. Duplicate your current project Target and rename like DEV/QA/PROD.
2. Add New Environment.plist into your project. Structure of Environment.plist will be like below.
3. In your service class add method to fetch baseURL according to your target settings. E.g Code:
NSURL* environmentsURL = [[NSBundle mainBundle] URLForResource:#"Environments" withExtension:#"plist"];
NSDictionary* environments = [NSDictionary dictionaryWithContentsOfURL:environmentsURL];
NSString* currentEnvironment = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"Environment"];
NSString* keyPath = [NSString stringWithFormat:#"%#.baseURL", currentEnvironment];
NSString *baseUrlString = [environments valueForKeyPath:keyPath];

iOS applications Localization.strings file name change

IOS application provide the support of localization through the Localizable.strings file. If I want to change the file name for some obvious reasons where would I have to put that reference.
Can anyone please help.
How iOS localization works:
As you would already know, iOS provides a nice API for getting localized string as following.
NSString *stringValue = [[NSBundle mainBundle] localizedStringForKey:key
value:#""
table:nil];
And it also provides a macro for quick access as:
#define NSLocalizedString(key, comment) \
[[NSBundle mainBundle] localizedStringForKey:(key) value:#"" table:nil]
iOS, by default, looks for strings in Localizable.strings file. However, we can also provide a custom file for iOS to look for strings into. And this is where things get interesting.
To provide a custom file, we can use the API as mentioned above in following manner.
NSString *localizedString = [[NSBundle mainBundle] localizedStringForKey:key
value:#""
table:#"AnotherLocalizableStringsFile"];
The table parameter takes a string argument AnotherLocalizableStringsFile which is the file containing the strings.
Another interesting parameter is the value parameter that takes in a string that should be returned in case no string is found matching the given key.
So following piece of code would return Invalid Key assuming the provided key does not exist in the file.
NSString *stringValue = [[NSBundle mainBundle] localizedStringForKey:#"Wrong_key_passed"
value:#"Invalid Key"
table:#"TargetLocalizable"];
The solution:
By using the combination of these two interesting parameters, we can devise a method to suit our requirements. We will ask iOS to look for strings specific to target in target specific strings file and fall back to Localizablestrings file when it comes to loading generic strings common to all targets.
Here’s how the piece of code looks like.
NSString *stringValue = [[NSBundle mainBundle] localizedStringForKey:#"Key"
value:NSLocalizedString(#"Key", nil)
table:#"TargetLocalizable"];
So, iOS looks for the string first in the given TargetLocalizable.strings file. If it doesn’t find it there, it would search in the base Localizable.strings file.
So all I had to do was to place all the strings common to all targets in Localizable.strings file and put the additional and overridden strings specific to the target in TargetLocalizable.strings file.
The name is 'fixed' - a localizable is named Localizable. you can only decide to not use NSBundle localization and roll your own stuff

Edit Programmatically plist in the App bundle at development time

I have created a plist on XCode that will have a few values that I can't insert manually. So I want to add this values programmatically at development time. But it seems that I can only read the plist I can not save a plist that is on the App bundle, which makes sense at runtime.. When I will distribute my app I want everyone to have this plist file that's why I am not saving on documents or cache. How can I achieve what I want?
From http://www.karelia.com/cocoa_legacy/Foundation_Categories/NSFileManager__Get_.m (pasted below) you can build a path within the user's personal library with the -(NSString *) pathFromUserLibraryPath:(NSString *)inSubPath method found there.
For example, NSString *editedPlist = [self pathFromUserLibraryPath:#"my.plist"]; gets you the name of the modified plist within the user's library (even if that plist doesn't exist yet).
How you read/write it is according to what kind of plist you have, but you could read it into a dictionary with:
NSMutableDictionary *thePlist= [[NSMutableDictionary alloc] initWithContentsOfFile:editedPlist ];
If you are unable to read, easily detected by, for example [thePlist count] == 0, then you would instead call the same initWithContentsOfFile: initializer with a path to the template within your bundle, but you would then write the plist out to the editedPlist path so it appears in the user directory.
Here is the utility method I referenced above:
/*
NSFileManager: Get the path within the user's Library directory
Original Source: <http://cocoa.karelia.com/Foundation_Categories/NSFileManager__Get_.m>
(See copyright notice at <http://cocoa.karelia.com>)
*/
/*" Return the path in the user library path of the given sub-path. In other words, if given inSubPath is "foo", the path returned will be /Users/myUser/Library/foo
"*/
- (NSString *) pathFromUserLibraryPath:(NSString *)inSubPath
{
NSArray *domains = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES);
NSString *baseDir= [domains objectAtIndex:0];
NSString *result = [baseDir stringByAppendingPathComponent:inSubPath];
return result;
}
What I would suggest is writing code that checks for the plist in the documents directory at start. If it's there, read it into memory.
If you don't find the file in the documents directory, read it from the app bundle instead. Then drop into the code that uses it from memory and writes the changed version to the documents directory.
Remember that all the objects you read from a plist file are read as immutable, even if you wrote mutable objects into the file. You have to write code that makes mutable copies of anything that you want to change. (And have to implement a mutable deep copy if you have complex structures like arrays of dictionaries that in turn contain arrays of strings.)

Define ENUM values in a .plist in Xcode

I use a configuration.plist file to configure certain parameters in my application and initialise few classes based on the contents of this plist file.
However I want to expose to the developer a list of options that can be selected as below(per say),
I can this kind of option available in application info.plist file but I don't get to see anywhere else on how I can achieve this.
I'm looking at getting a drop down list showing the list of available options, Possibly an ENUM list.
Appreciate any assistance.
You can read from the .plist file:
NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString* region = [infoDictionary objectForKey:(__bridge id) kCFBundleDevelopmentRegionKey];

Resources