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

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.

Related

Apple/fastlane submit for review action failing with unclear error message about privacy text

I am following fastlane's default configuration conventions for a React Native project. That is to say, my project structure looks like (e.g.):
/ios/fastlane/metadata/en-GB
with the en-GB subdirectory containing the following files:
description.txt
keywords.txt
name.txt
privacy_url.txt
release_notes.txt
support_url.txt
Everything was working perfectly until I tried to internationalise.
Now, when fastlane's upload_to_app_store() (aliased with deliver()) runs in my CI/CD pipeline, it fails and I see the following error:
The provided entity is missing a required attribute - You must provide a value for the attribute 'privacyPolicyText' with this request - /data/attributes/privacyPolicyText
I already have privacy_url.txt in there which was previously sufficient.
The thing that fixed it was adding a apple_tv_privacy_policy.txt (just containing a URL pointing to our privacy policy) file to the en-GB subdirectory.
Weird, given that the Fastfile was working completely fine before without any sort of privacy policy for TV. Very frustrating error message!

Set default language before the app starts for the first time in iOS/Swift

I wanna set another language as default when the app starts for the first time. iOS doesn't not have this language as an interface language, so my only way is to set it manually. I've found dozens of solutions on SO, and those that work, ONLY work in simulator and not on a real device. Here's what I've tried:
First of all, the way to set the app's language is:
UserDefaults.standard.set(["xx-XX", "yy-YY"], forKey: "AppleLanguages")
The thing is, that this needs the app to be restarted, while I want it to be set the first time the app starts. I've also tried:
UserDefaults.standard.register(defaults: ["AppleLanguages": ["xx-XX", "yy-YY"]])
and even calling UserDefaults.standard.synchronize() which in most cases is not needed at all.
And the places I've tried putting this code:
application(:didFinishLaunchingWithOptions:)
application init :|
Subclassing UIApplication, creating main.swift and adding the
above codes as top level statements
And in info.plist:
Setting Localization native development region to xx-XX
Setting the first element of Localizations to xx-XX
By the way, implementing custom localization is not an option at this point in time.
And what confuses me the most, is why such a feature works in simulator and not a real device.
EDIT:
I even deleted the default english language, and adding English (World) again, so that there was no Development Language label beside it. Still didn't work.
On Xcode next from the Start Stop Buttons you can press the "projectName" -> Edit Scheme... -> Run -> Options and change in Application Language
Well, there was not one single legitimate way to do it. So I had to use a nasty trick.
Added a language that we're not using (in this case fr)
In the process of creation, chose english (yy-YY as in my question) as the reference language (So that I wouldn't have to do any translation/localization again)
At this point, my development language kicked in, and xx-XX (in this case fa-IR) was my default. And you know the rest, setting the AppleLanguages user default.
Again, this is not the best and a clean way to do it, and if you are early in the development process, go with a custom implementation of localizing your app, or use L10n-Swift. That is of course, if you can't stick with what iOS chooses for you based on the device locale selected by the user.
To make the default language as a particular one this following code works like a charm.(Swift 4.2)
let arr = NSArray(objects: "ja")
UserDefaults.standard.set(arr, forKey: "AppleLanguages")

What is customURLScheme in Firebase Dynamic Links?

In the documentation it says to add the following lines to my AppDelegate.swift:
// Set deepLinkURLScheme to the custom URL scheme you defined in your
// Xcode project.
FIROptions.default().deepLinkURLScheme = self.customURLScheme
From what I understand this should be the same link you put in your info.plist. However, I'm confused why in the quickstart-ios repo they decided to make this equal to "dlscheme".
Can anybody help me understand what exactly this scheme is?
This is not clear in the Dynamic Links integration instructions — I ran into the same issue even though I work with these things all day at Branch.io (full disclosure: we're an alternative/improvement to Dynamic Links).
When configuring a custom URI scheme, you need to supply both an Identifier and a URL Scheme. Apple recommends using a reverse domain value for the Identifier, but since your bundle ID is also typically reverse domain format, these two often end up being identical.
By default, Firebase expects you to use your bundle identifier as your custom URI scheme. When you do this, their default configuration takes over and you don't need to specify the FIROptions.default().deepLinkURLScheme = self.customURLScheme line at all. The URI scheme config ends up looking like this, which is a bit counter-intuitive:
However, if you decide to use a value that is not your bundle ID for the URL Scheme (very common), then you DO need the FIROptions.default().deepLinkURLScheme = self.customURLScheme line. But you also need this one before it: let customURLScheme = "somethingelse". You can see this here in the quickstart, and also where the URI scheme is defined in the info.plist file here.
Basically, the Firebase team tried to simplify things by assuming the bundle ID as the custom URI scheme value. This is not a bad option, but it can be confusing and as you can see, even their own quickstart project uses a more advanced config.

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

Get path to iOS application based on name or bundle identifier

Is there an easier way to get the path to an iOS application, than searching /var/mobile/Applications?
I know both the name and the bundle identifier, however the path is not consistent on different iOS devices.
This is for use in a jailbreak tweak, so I can use PrivateFrameworks and other code not allowed by Apple.
If you're running code that executes in Springboard, this should be fairly simple. Get SBApplicationController's sharedInstance, then get the SBApplication you're looking for with the applicationWithDisplayIdentifier: method (or using whatever method you choose). The SBApplication class contains properties for path, containerPath, and bundle (among many others), one of which should be what you're looking for. I haven't tried this myself, so I can't guarantee it'll work, but based on a quick glance at the Springboard header files (you can take a look here, or dump the header files yourself), it should work.
On the other hand, if you're not running from Springboard (ie. if you're making an actual App Store-style application), then you may be out of luck. You could look into inter-process communication with Springboard and see if something can be done there, but it'd probably be more trouble than it's worth.
If running in an app, you can define:
extern NSString* SBSCopyBundlePathForDisplayIdentifier(NSString* bundleId);
and link to the SpringboardServices framework.
Or you can use the library AppList and then it's:
ALApplicationList *al = [ALApplicationList sharedApplicationList];
NSString *appPath = [al valueForKey:#"path" forDisplayIdentifier:bundleID];
In this case it's doing what Andrew R. mentions in his answer for you. (I assume the same requirements are necessary, i.e. must be running from Springboard.)
Update: This no longer seems to be working on iOS 11.

Resources