From c9ef994369256efb3a0db3372ceb5a64a0861645 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:52:08 +0300 Subject: [PATCH] Fixed icon localization issue in iOS --- .../Miscellaneous-Features.asciidoc | 2 +- .../com/codename1/builders/IPhoneBuilder.java | 54 +++++++++++++------ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/docs/developer-guide/Miscellaneous-Features.asciidoc b/docs/developer-guide/Miscellaneous-Features.asciidoc index 9f15c765ec..6f452056b6 100644 --- a/docs/developer-guide/Miscellaneous-Features.asciidoc +++ b/docs/developer-guide/Miscellaneous-Features.asciidoc @@ -473,7 +473,7 @@ 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__60x60@2x.png` (120×120), `@3x.png` (180×180) and `76x76@2x~ipad.png` (152×152) in the app bundle root. +* For each detected locale the build generates `AppIcon__@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. * The `CodenameOne_GLAppDelegate` is patched to call `-[UIApplication setAlternateIconName:completionHandler:]` at launch. The delegate reads `[NSLocale preferredLanguages]`, tries the full `_` 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. diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index b888fbb8cc..6a508d11bc 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -3159,18 +3159,36 @@ private void copyIcon(String name, File srcDir, File destDir) throws IOException } private String buildLocalizedIconsPlistFragment() { - StringBuilder alternateIcons = new StringBuilder(); + // iOS resolves alternate icons by appending @2x/@3x/~ipad to each basename + // listed in CFBundleIconFiles. The iPhone basename catches AppIcon_@2x.png + // (120) and AppIcon_@3x.png (180); the iPad basename catches + // AppIcon_@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 entry : localizedIcons.entrySet()) { String iconName = entry.getKey(); - alternateIcons.append(" ").append(iconName).append("\n"); - alternateIcons.append(" \n"); - alternateIcons.append(" CFBundleIconFiles\n"); - alternateIcons.append(" \n"); - alternateIcons.append(" ").append(iconName).append("\n"); - alternateIcons.append(" \n"); - alternateIcons.append(" UIPrerenderedIcon\n"); - alternateIcons.append(" \n"); - alternateIcons.append(" \n"); + iphoneAlternate.append(" ").append(iconName).append("\n"); + iphoneAlternate.append(" \n"); + iphoneAlternate.append(" CFBundleIconFiles\n"); + iphoneAlternate.append(" \n"); + iphoneAlternate.append(" ").append(iconName).append("\n"); + iphoneAlternate.append(" \n"); + iphoneAlternate.append(" UIPrerenderedIcon\n"); + iphoneAlternate.append(" \n"); + iphoneAlternate.append(" \n"); + + ipadAlternate.append(" ").append(iconName).append("\n"); + ipadAlternate.append(" \n"); + ipadAlternate.append(" CFBundleIconFiles\n"); + ipadAlternate.append(" \n"); + ipadAlternate.append(" ").append(iconName).append("\n"); + ipadAlternate.append(" ").append(iconName).append("83.5x83.5\n"); + ipadAlternate.append(" \n"); + ipadAlternate.append(" UIPrerenderedIcon\n"); + ipadAlternate.append(" \n"); + ipadAlternate.append(" \n"); } StringBuilder out = new StringBuilder(); out.append("\nCFBundleIcons\n"); @@ -3185,7 +3203,7 @@ private String buildLocalizedIconsPlistFragment() { out.append(" \n"); out.append(" CFBundleAlternateIcons\n"); out.append(" \n"); - out.append(alternateIcons); + out.append(iphoneAlternate); out.append(" \n"); out.append("\n"); out.append("CFBundleIcons~ipad\n"); @@ -3200,7 +3218,7 @@ private String buildLocalizedIconsPlistFragment() { out.append(" \n"); out.append(" CFBundleAlternateIcons\n"); out.append(" \n"); - out.append(alternateIcons); + out.append(ipadAlternate); out.append(" \n"); out.append("\n"); return out.toString(); @@ -3370,9 +3388,15 @@ public boolean accept(File dir, String name) { candidate.delete(); continue; } - createIconFile(new File(resDir, iconName + "60x60@2x.png"), img, 120, 120); - createIconFile(new File(resDir, iconName + "60x60@3x.png"), img, 180, 180); - createIconFile(new File(resDir, iconName + "76x76@2x~ipad.png"), img, 152, 152); + // 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); localizedIcons.put(iconName, localeKey); // Remove the original so it isn't bundled as a stray resource. candidate.delete();