Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/developer-guide/Miscellaneous-Features.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,8 @@ No code changes are required — Android's resource framework switches icons whe

iOS does not localize launcher icons natively, so Codename One wires up https://developer.apple.com/documentation/uikit/uiapplication/2806818-setalternateiconname[alternate app icons] for you:

* For each detected locale the build generates `AppIcon_<lang>_<COUNTRY>@2x.png` (120×120), `@3x.png` (180×180), `@2x~ipad.png` (152×152) and `83.5x83.5@2x~ipad.png` (167×167 for iPad Pro) in the app bundle root.
* A `CFBundleIcons` (and `CFBundleIcons~ipad`) entry is injected into `Info.plist` containing a `CFBundleAlternateIcons` dictionary with one entry per locale. `CFBundlePrimaryIcon` continues to reference the default `iPhone7App`/`iPadApp7` image families.
* For each detected locale the build generates an `AppIcon_<lang>_<COUNTRY>.appiconset` inside `Images.xcassets` containing the 120×120, 180×180, 152×152 and 167×167 (iPad Pro) PNGs along with a matching `Contents.json`.
* The build adds the matching `ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES` Xcode build setting so `actool` emits a coherent partial `Info.plist` covering both `CFBundlePrimaryIcon` and `CFBundleAlternateIcons`. The user's `Info.plist` is left untouched; injecting `CFBundleIcons` manually would be dropped during the actool merge and is therefore avoided.
* The `CodenameOne_GLAppDelegate` is patched to call `-[UIApplication setAlternateIconName:completionHandler:]` at launch. The delegate reads `[NSLocale preferredLanguages]`, tries the full `<lang>_<COUNTRY>` key first, then falls back to the language-only key, and clears the alternate icon (reverting to the default) if no variant matches.
* The injection is idempotent and runs before the `ios.afterFinishLaunching` hook, so any custom code you supply via that build hint is unaffected.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2264,6 +2264,8 @@ public void usesClassMethod(String cls, String method) {

injectToPlist(tmpFile, resDir, request);

addLocalizedIconsBuildSetting(pbxprojFile);

} catch (Exception ex) {
throw new BuildException("Failed to inject into plist");
}
Expand Down Expand Up @@ -2797,9 +2799,13 @@ public boolean accept(File file, String string) {
if(inject.indexOf("CFBundleShortVersionString") < 0) {
inject += "\n<key>CFBundleShortVersionString</key> <string>" + buildVersion +"</string>";
}
if (!localizedIcons.isEmpty() && !inject.contains("CFBundleAlternateIcons")) {
inject += buildLocalizedIconsPlistFragment();
}
// Localized icons are emitted by actool through the asset catalog
// (Images.xcassets/AppIcon_<locale>.appiconset) and the
// ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES build setting; the
// resulting partial Info.plist already contains the correct
// CFBundleIcons entries, so we deliberately do not inject any
// CFBundleAlternateIcons fragment here. Doing so would conflict with
// actool's output during the Info.plist merge.
String locationUsageDescription = null;
if (xcodeVersion >= 9) {
if ( (locationUsageDescription = request.getArg("ios.locationUsageDescription", null)) != null ){
Expand Down Expand Up @@ -3158,72 +3164,6 @@ private void copyIcon(String name, File srcDir, File destDir) throws IOException
copy(new File(srcDir, name), new File(destDir, name));
}

private String buildLocalizedIconsPlistFragment() {
// iOS resolves alternate icons by appending @2x/@3x/~ipad to each basename
// listed in CFBundleIconFiles. The iPhone basename catches AppIcon_<loc>@2x.png
// (120) and AppIcon_<loc>@3x.png (180); the iPad basename catches
// AppIcon_<loc>@2x~ipad.png (152). The 167x167 iPad Pro size cannot be
// expressed via @-modifiers, so it is referenced through its explicit
// 83.5x83.5 basename.
StringBuilder iphoneAlternate = new StringBuilder();
StringBuilder ipadAlternate = new StringBuilder();
for (Map.Entry<String, String> entry : localizedIcons.entrySet()) {
String iconName = entry.getKey();
iphoneAlternate.append(" <key>").append(iconName).append("</key>\n");
iphoneAlternate.append(" <dict>\n");
iphoneAlternate.append(" <key>CFBundleIconFiles</key>\n");
iphoneAlternate.append(" <array>\n");
iphoneAlternate.append(" <string>").append(iconName).append("</string>\n");
iphoneAlternate.append(" </array>\n");
iphoneAlternate.append(" <key>UIPrerenderedIcon</key>\n");
iphoneAlternate.append(" <false/>\n");
iphoneAlternate.append(" </dict>\n");

ipadAlternate.append(" <key>").append(iconName).append("</key>\n");
ipadAlternate.append(" <dict>\n");
ipadAlternate.append(" <key>CFBundleIconFiles</key>\n");
ipadAlternate.append(" <array>\n");
ipadAlternate.append(" <string>").append(iconName).append("</string>\n");
ipadAlternate.append(" <string>").append(iconName).append("83.5x83.5</string>\n");
ipadAlternate.append(" </array>\n");
ipadAlternate.append(" <key>UIPrerenderedIcon</key>\n");
ipadAlternate.append(" <false/>\n");
ipadAlternate.append(" </dict>\n");
}
StringBuilder out = new StringBuilder();
out.append("\n<key>CFBundleIcons</key>\n");
out.append("<dict>\n");
out.append(" <key>CFBundlePrimaryIcon</key>\n");
out.append(" <dict>\n");
out.append(" <key>CFBundleIconFiles</key>\n");
out.append(" <array>\n");
out.append(" <string>iPhone7App</string>\n");
out.append(" <string>iPhoneApp</string>\n");
out.append(" </array>\n");
out.append(" </dict>\n");
out.append(" <key>CFBundleAlternateIcons</key>\n");
out.append(" <dict>\n");
out.append(iphoneAlternate);
out.append(" </dict>\n");
out.append("</dict>\n");
out.append("<key>CFBundleIcons~ipad</key>\n");
out.append("<dict>\n");
out.append(" <key>CFBundlePrimaryIcon</key>\n");
out.append(" <dict>\n");
out.append(" <key>CFBundleIconFiles</key>\n");
out.append(" <array>\n");
out.append(" <string>iPadApp7</string>\n");
out.append(" <string>iPhone7App</string>\n");
out.append(" </array>\n");
out.append(" </dict>\n");
out.append(" <key>CFBundleAlternateIcons</key>\n");
out.append(" <dict>\n");
out.append(ipadAlternate);
out.append(" </dict>\n");
out.append("</dict>\n");
return out.toString();
}

private String buildLocalizedIconSelectorObjC() {
StringBuilder mapping = new StringBuilder();
mapping.append(" @{ ");
Expand Down Expand Up @@ -3337,19 +3277,24 @@ private boolean generateIcons(BuildRequest request) throws Exception {
"iPadPro@2x.png",
"AppStore.png");

processLocalizedIcons(resDir);
processLocalizedIcons(resDir, request);

return true;
}

/**
* Scans the resources directory for files named cn1_icon_LANG[_COUNTRY].png
* and registers them as iOS alternate app icons so the bundle will contain
* per-locale launcher icons in addition to the default icon. Runtime icon
* selection is wired up in the GL app delegate and the Info.plist
* CFBundleIcons entry is populated in {@link #injectToPlist}.
* and registers them as iOS alternate app icons. Each detected locale is
* written as its own AppIcon_LOC.appiconset inside Images.xcassets so that
* actool produces a coherent CFBundleIcons / CFBundleAlternateIcons
* partial Info.plist; mixing manual CFBundleIcons entries in the user
* Info.plist with an asset-catalog-managed primary icon is unsafe because
* actool's partial plist replaces ours during the merge. The matching
* ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES build setting is added in
* {@link #addLocalizedIconsBuildSetting}; runtime icon selection is wired
* up in the GL app delegate.
*/
private void processLocalizedIcons(File resDir) throws IOException {
private void processLocalizedIcons(File resDir, BuildRequest request) throws IOException {
File[] candidates = resDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
Expand All @@ -3360,6 +3305,8 @@ public boolean accept(File dir, String name) {
if (candidates == null || candidates.length == 0) {
return;
}
File assetCatalogDir = new File(tmpFile, "dist/" + request.getMainClass()
+ "-src/Images.xcassets");
for (File candidate : candidates) {
String base = candidate.getName();
// strip prefix/suffix
Expand Down Expand Up @@ -3388,22 +3335,94 @@ public boolean accept(File dir, String name) {
candidate.delete();
continue;
}
// File names must match the basenames listed in CFBundleIconFiles so iOS
// can resolve them via the standard @2x/@3x/~ipad modifiers; the
// 83.5x83.5 iPad Pro icon cannot be expressed via modifiers and so
// carries the size in the file name and is referenced explicitly in
// buildLocalizedIconsPlistFragment.
createIconFile(new File(resDir, iconName + "@2x.png"), img, 120, 120);
createIconFile(new File(resDir, iconName + "@3x.png"), img, 180, 180);
createIconFile(new File(resDir, iconName + "@2x~ipad.png"), img, 152, 152);
createIconFile(new File(resDir, iconName + "83.5x83.5@2x~ipad.png"), img, 167, 167);

File alternateIconset = new File(assetCatalogDir, iconName + ".appiconset");
if (!alternateIconset.exists() && !alternateIconset.mkdirs()) {
log("Failed to create alternate icon set directory: "
+ alternateIconset.getAbsolutePath());
candidate.delete();
continue;
}
String iphone2x = iconName + "60x60@2x.png";
String iphone3x = iconName + "60x60@3x.png";
String ipad2x = iconName + "76x76@2x~ipad.png";
String ipadPro2x = iconName + "83.5x83.5@2x~ipad.png";
createIconFile(new File(alternateIconset, iphone2x), img, 120, 120);
createIconFile(new File(alternateIconset, iphone3x), img, 180, 180);
createIconFile(new File(alternateIconset, ipad2x), img, 152, 152);
createIconFile(new File(alternateIconset, ipadPro2x), img, 167, 167);
writeAlternateAppIconContentsJson(new File(alternateIconset, "Contents.json"),
iphone2x, iphone3x, ipad2x, ipadPro2x);
localizedIcons.put(iconName, localeKey);
// Remove the original so it isn't bundled as a stray resource.
candidate.delete();
log("Registered localized app icon '" + iconName + "' for locale " + localeKey);
}
}

private void writeAlternateAppIconContentsJson(File contentsJson,
String iphone2x, String iphone3x, String ipad2x, String ipadPro2x) throws IOException {
String json = "{\n"
+ " \"images\" : [\n"
+ " {\n"
+ " \"size\" : \"60x60\",\n"
+ " \"idiom\" : \"iphone\",\n"
+ " \"filename\" : \"" + iphone2x + "\",\n"
+ " \"scale\" : \"2x\"\n"
+ " },\n"
+ " {\n"
+ " \"size\" : \"60x60\",\n"
+ " \"idiom\" : \"iphone\",\n"
+ " \"filename\" : \"" + iphone3x + "\",\n"
+ " \"scale\" : \"3x\"\n"
+ " },\n"
+ " {\n"
+ " \"size\" : \"76x76\",\n"
+ " \"idiom\" : \"ipad\",\n"
+ " \"filename\" : \"" + ipad2x + "\",\n"
+ " \"scale\" : \"2x\"\n"
+ " },\n"
+ " {\n"
+ " \"size\" : \"83.5x83.5\",\n"
+ " \"idiom\" : \"ipad\",\n"
+ " \"filename\" : \"" + ipadPro2x + "\",\n"
+ " \"scale\" : \"2x\"\n"
+ " }\n"
+ " ],\n"
+ " \"info\" : {\n"
+ " \"version\" : 1,\n"
+ " \"author\" : \"xcode\"\n"
+ " }\n"
+ "}\n";
try (Writer w = new OutputStreamWriter(Files.newOutputStream(contentsJson.toPath()),
StandardCharsets.UTF_8)) {
w.write(json);
}
}

private void addLocalizedIconsBuildSetting(File pbx) throws IOException {
if (localizedIcons.isEmpty()) {
return;
}
StringBuilder names = new StringBuilder();
boolean first = true;
for (String iconName : localizedIcons.keySet()) {
if (!first) {
names.append(' ');
}
first = false;
names.append(iconName);
}
// actool reads ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES and emits a
// partial Info.plist with both CFBundlePrimaryIcon and CFBundleAlternateIcons,
// which is what we need for setAlternateIconName: to resolve at runtime.
replaceAllInFile(pbx,
"ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;",
"ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n"
+ "\t\t\t\tASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = \""
+ names + "\";");
}


private boolean generateLaunchScreen(BuildRequest request) throws Exception {
File buildinRes = getBuildinRes();
Expand Down
Loading