Xcode building different environments for the same app - ios

I have an app that talks to a server. I have 2 versions of this server: a production one and a testing one. This means I'd need to have a production iOS app and a testing iOS app. The logic is essentially the same for both versions of the app, except it would need to use certain configurations depending on which server it connects to. Right now, my solution is to use a plist file that contains informations that the 2 versions need. The plist would contain a bunch of key-value pairs like:
url: test-server.domain.com
username: test-subject
password: test-password
I have 2 git branches: a production branch and a testing branch for each of the iOS app version. The content of the said plist file would be different on each branch.
Question is: is there a more elegant way to solve this? Preferably I'd like to only have 1 branch. I've looked into using Xcode's environment variables, but those don't stick when I archive/build my apps.

There sure is! You can do this with environment variables, as described in this blogpost: Flavors for iOS. You can even switch app icon and app name based on the variables when building, therefore, you can check in code in the following way: if #APP_DEV // call dev API ELSIF // call prod API.
Steps
1: On the project settings level, duplicate the debug configuration and rename it to staging. (Optional)
2: Open the project build settings and add some user-defined settings: APP_ENV with values dev => dev, staging => stage, production => prod.
3: You need multiple bundle IDs to load apps side by side (for instance a beta or production app). Therefor, create another user-defined settings APP_BUNDLE_SUFFIX with the value .$(APP_ENV:lower). This turns Dev into .dev. Leave production empty if you like to have no extra suffix when submitting the app.
4: Append the APP_BUNDLE_SUFFIX to your product bundle ID (com.testapp.YourCoolApp.$(APP_BUNDLE_SUFFIX)).
5: In Swift projects, you can add the added variables to the "Active Compilation Conditions" on the same screen. Do this by adding ENV_$(APP_ENV) to the environments listed. In Objective-C projects, this can be done by adding the environments to the "Preprocessor macro's", but pay attention, you'll need to suffix =1 to the value in order to activate the variables.
Making the env vars stick
You might need to create separate schemes to achieve this. You can duplicate schemes, or option+click on the scheme at the top and switch the build config.

I usually use Preprocessor Macros for this.
Define variable like DEBUG for Debug and nothing in Release.
And then use this like
enum AppConfig {
case debug
case testFlight
case appStore
var host: String {
switch self {
case .debug: return "test host"
default: return "production host"
}
}
static var current: AppConfiguration {
#if DEBUG
return .debug
#else
if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" {
return .testFlight
} else {
return .appStore
}
#endif
}
}
Wherever you want to use host, you can use AppConfig.current.host. The only thing you need to do is create scheme for debug and release.

Related

Using Environment Variables for Per Environment Configuration in iOS

I am working on an iOS application that uses a pretty normal multi-environment deployment model. We have a QA, Prod, and "Dev" version of the app that all talk to their own corresponding backends. I am new to iOS development but am familiar with Node, Java, and a few other development environments.
The first thing I reached to for this problem was Environment Variables. I saw that XCode had a way to set environment variables in a Scheme and they could be read pretty easily. So I used 4 environment variables per environment to configure a few needed backend hosts. Everything seemed to be going fine until I realized that those environment variables seem to ONLY be available when running the app through XCode. Is that correct? Is there no way to configure environment variables that "bundle up" with an app? If so, the ability to configure environment variables at all seems like a footgun.
What I mean is, In a NodeJS or Java app, I can set a number of useful "necessary" configs like a backend hosts and use some approach to provide those values when running the app for real. It seems like in iOS / Swift, environment variables are only useful for development-time debugging settings? The asymmetry between what's available in XCode vs a "real" shipped app seems odd.
Is there a similar standard way that I can configure my app for multiple different environments that works on shipped applications and ideally just involves reading some value at runtime rather than using conditionals and/or using compiler flags or something?
You are correct. The Environment Variables are only meaningful when executing the Scheme in Xcode. Their primary use in that context is to activate debugging features that are not on all the time. For example, if you try to create a bitmap Core Graphics context (CGContext) with an invalid set of parameters, the debugger may tell you to set an environment variable to see additional debugging output from Core Graphics. Or you can set environment variables to turn on memory management debugging features.
When you are running an application on a server, the Unix framework in which the application is running is part of the "user experience". In that context it makes sense for the application to use things like environment variables.
As developers we understand that a mobile app is running inside a unix process, but that unix environment is mostly unavailable to us. A similar feature that is common to Unix apps is command line arguments. An iOS application on startup receives command line arguments (argc and argv) but there is no way specify those when the app is launched either.
There are a number of places you could include configuration information like that which you describe in your application. The most common that I can think of is to include the setting in the applications Info.plist. At runtime you could access the contents of the property list by fetching the main bundle and asking for it's infoDictionary:
let infoBundle = Bundle.main.infoDictionary
let mySetting = infoBundle["SomeSetting"]
When the application's info.plist is created, it DOES have access to the environment variables declared in the Scheme so you could put the environment variables in the scheme, reference them in the Info.plist, and retrieve them at runtime from the main bundle.
try Using FeatureFlags, maybe will help you, check this
https://medium.com/#rwbutler/feature-flags-a-b-testing-mvt-on-ios-718339ac7aa1

Xcode UI tests, development language, fallback translation and CI servers

Following scenario:
I'm having an iOS project that automatically gets unit and UI tested on every git push on a CI server (CircleCI). The tests are executed with fastlane which is also used to create screenshots for the App Store automatically.
Now, my project gets translated into several languages. I want fastlane to work on all languages (to be able to take the screenshots), so I changed the UI tests from something like this:
app.navigationBars.buttons["Confirm"].tap()
to
let buttonTitle = NSLocalizedString("navbar.confirm", comment: "")
app.navigationBars.buttons[buttonTitle].tap()
I thought this will do the trick, but it doesn't. I don't know how the simulator is configured in CircleCI, but the UI tests now fail with
[00:46:34]: ▸ testDashboard, No matches found for Find: Elements matching predicate '"navbar.confirm" IN identifiers' from input {(
So for some reason the fallback language set with CFBundleDevelopmentRegion is not respected, probably because the language is not in the preferredLanguages list of the bundle. This is an issue in itself as I do not want keys to be displayed for end users in any case. I'd like to make sure this never ever happens.
So I tried to fix that in turn by writing a wrapper for NSLocalizedString that checks whether NSLocalizedString(..) returns the key and if so loads the default (en) bundle and gets the string localized that way.
However it seems you can't load another bundle in the UI tests. The test will crash and fail. So I can't use this workaround.
Am I just overlooking some obvious solution? I can't be the only one having this issue, right? Any hints?
When you use NSLocalizedString("navbar.confirm", comment: "") the system tries to retrieve a value for that key from the strings file in the main bundle.
The main bundle does not work when running UITests because it gives you the bundle of the UITest Runner App instead of the UITest bundle.
To be able to use NSLocalizedString in a UITest you have to do 2 things:
1. Add your Localizable.strings file to your UITest target
2. Access the file via the UITest bundle (You can use a little helper method for that):
func localized(_ key: String) -> String {
let uiTestBundle = Bundle(for: AClassFromYourUITests.self)
return NSLocalizedString(key, bundle: uiTestBundle, comment: "")
}
You can use any class from your UITests to access the UITest bundle.
Now you can use NSLocalizedString in your UITest like this:
app.navigationBars.buttons[localized("navbar.confirm")].tap()
I wrote a little blog post about this a while ago, if you are interested in more details.
Set accessibility identifiers on your UI elements and query using those in your tests instead of using the localized string values. This will make your tests language-agnostic and there will be no need to fiddle with access to different bundles.
See this answer for an example of how to set and use accessibility identifiers.

How to create a multiple choice key-value in info.plist

I would like to be able to achieve this:
Basically, if you want to choose another Country for the setting "Localization native development region" you juste have to go the right, of the row, click on the up/down arrow icon and you will be able to choose another option, like this:
This is what I'm trying to do. I would like a Key called "Environnement" with two possible values: "Dev" and "Production". Seriously i can't find the documentation to achieve this..
Thanks you guys ..
That is a special picker for some of the default iOS settings, which means that you won't be able to do what you are trying.
I think you want to use this new setting in your plist to for example call a development API instance instead of the production one.
You may want to create separate Targets to manage the different environments that you have, right now is Dev and Prod but you may run into a Stage or Test environment in the future. With a separate target not only you can identify which environment but also change the app's bundle identifier to install both versions on the same device and change the app name to do MyApp DEV, MyApp TST and MyApp for production. It will give you a better version control.
Once you have created a target for your dev environment you will need to add a C flag to identify each one of them.
Dev Flag:
Prod Flag:
Then declare your constants for what your specific use case like:
/* 0 = dev 1 = test 2 = prod */
#ifdef PROD
#define BASE_URL #"https://api.mydomain.com/"
#define ENVIRONMENT 2
#elif TEST
#define BASE_URL #"https://api-test.mydomain.com/"
#define ENVIRONMENT 1
#else
#define BASE_URL #"https://api-dev.mydomain.com/"
#define ENVIRONMENT 0
#endif
And you should be ready to go to manage different environments.

Localise App display name that have append suffix

I have a issue with getting app display name to include the appending suffix when adding localisation to InfoStrings.plist.
I have add different scheme and User-Defined attribute. So in my info.plist, i have App Name $(BUNDLE_DISPLAY_NAME_SUFFIX) in my CFBundleDisplayName. It will append a -S to my app name when running on development scheme and normal app name on release scheme that i created. Everything is working well.
However, when I try to translate the app name, it does not work anymore. So in my infoPlist.strings, I tried the following:
"CFBundleDisplayName" = "App Name ";
"CFBundleDisplayName" = "App Name $(BUNDLE_DISPLAY_NAME_SUFFIX)";
Both does not append the -S anymore when I run on development scheme. Does anyone know how I could still do that? Like maybe how to get the $(Bundle_DISPLAY_NAME_SUFFIX) to be read in the infoPlist.strings.
More specifically, how do I include a preprocessor in InfoPlist.strings?
I found the answer to your question in another thread, here, but it says you need a scrip for this.
How you create different suffixes (not what was asked for)
Here is how you set up different display name of your app based on the your scheme. You can do this by setting up different configurations. Go to the project settings -> select the project (not the target) -> Info tab -> then create as many configurations you would like. Maybe one for Production, Debug and one for Beta releases.
Then select your Target -> Build settings tab -> Enter display in search. Under User defined you can create your own variable, call it e.g. BUNDLE_DISPLAY_NAME_SUFFIX. Give it different values for Production, Debug and Beta.
Open your Info.plist file, under Bundle display name, your see maybe MyApp, append the string ${BUNDLE_DISPLAY_NAME_SUFFIX} so it makes MyApp${BUNDLE_DISPLAY_NAME_SUFFIX}.
Finally configure your schemes to use the correct configuration. You probably want to use Production for Archive and Debug for Debug.
Here is an image of the User defined variable

How to make code behave differently when running locally vs. in QA vs. production?

In an ASP.NET MVC application I have a controller set up to deliver e-mail notifications. When running locally on a development machine I want the e-mails delivered to the developer, when in QA I don't want any e-mail notifications going out and in Production I want the notifications going out to their intended recipients
Have three different web.configs and add an AppSetting which tells you where you are so you can determine if you should send an email.
You could also define constants in your web.config using the CompilerOptions attribute:
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs"
type="Microsoft.CSharp.CSharpCodeProvider, System, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"
compilerOptions="/d:Test"/>
</compilers>
</system.codedom>
And in your code use
#if !Test
SendMail();
#endif
This sounds like something you should do using an IoC. I typically use StructureMap which would allow me to setup different profiles. Then all I would have to do is have a web.config switch configured to set the appropriate environment profile.
For example, in StructureMap you could do the following:
ObjectFactory.Initialize( x => {
x.CreateProfile( "Development", p =>
{
p.Type<IEmailProvider>().Is.OfConcreteType<DeveloperEmailProvider>();
} );
x.CreateProfile( "QA", p =>
{
p.Type<IEmailProvider>().Is.OfConcreteType<NullEmailProvider>();
} );
x.CreateProfile( "Production", p =>
{
p.Type<IEmailProvider>().Is.OfConcreteType<ProductionEmailProvider>();
} );
} );
ObjectFactory.Profile = ConfigurationSettings.AppSettings["Profile"];
For lightweight projects, I just use an AppSetting value, and then for all emails, pass the recipient address through the following message:
public static MailAddress MailTo(string email)
{
if (Boolean.Parse(ConfigurationManager.AppSettings["RedirectEmails"]))
{
return new MailAddress(ConfigurationManager.AppSettings["DebugMailbox"]);
}
return new MailAddress(email);
}
Our larger projects use NAnt build scripts, which use template config files to generate a different configuration for different build targets (so you have a Web.Config.template file, which is merged with either a local.properties, test.properties or release.properties XML file containing the relevant variables).
I would configure the web.config differently for the environments of how System.Net.Mail sends out the emails. Take a look at Scott Gu's blog post about it. For Development, I'd have it drop the emails on the server somewhere. For QA, have it not send anything anywhere, and for Production have it configured to use the normal SMTP server.
I think the IoC answer is a good general solution. For the specific case of emails being sent directly to an SMTP server, you could instead use the config solution here: How can I save an email instead of sending when using SmtpClient?. The config solution is quick and cheap, especially if the dev team is not big on IoC.
I've seen projects though where there is an intermediate service on another machine that does the email processing. In this case the config solution does not work.
I did this on a recent project. My solution is fairly involved, but in a nutshell there are two Web.config keys that control this: EmailTestMode and EmailEnabled. If EmailTestMode is on, messages are generated but sent to a specific address rather than their intended recipient. If EmailEnabled is off, messages are logged but not sent.
I went to the trouble of building a Messenger class that manages these items for me - I just call a method with the various attributes of the message, and it figures out whether and where to send it. I also have a separate configuration block in Web.config that contains all of the system messages. That way, the sender, recipient, subject and body can be easily modified from a configuration file. In most cases the body is either generated by the app or uses String.Format() to fill in values.
Like others have said, use different Application Settings in your web.config. Your runtime code can then use the correct version of your settings.
Another cool way of doing this is to use Conditional Attributes to build and call debug versions of your methods.
I use my version control software to do this for me. Basically I have multiple web.config files for each of my environments (dev, test, qa, prod). Now in the version control software I tag all the files for the correct environment. So when I need to build the qa evnvironment I get all the files tagged "QA" and so on.
The way we do it is to have two config keys in our machine.config
ProductionServers="PROD_SERVER"
TestServers="LOCAL_MACHINE|TEST_SERVER"
Then we have a function that tests machine name(System.Environment.MachineName) vs those values. That way we never have to change any configs on the servers and when we want to point at prod instead of test we just change our local machine.config.

Resources