From 080566994c565b1803fb13c6d1cb6db081741e9c Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 6 May 2026 13:12:26 +0300 Subject: [PATCH] iOS localized icons: defer setAlternateIconName: to active state and surface errors (#4870 follow-up) Builds on #4870 (CFBundleIconName injection) which made actool emit the correct partial Info.plist. Even with a correctly merged CFBundleIcons.CFBundleAlternateIcons table, calling -[UIApplication setAlternateIconName:completionHandler:] from application:didFinishLaunchingWithOptions: still fails silently with NSCocoaErrorDomain Code=3072 ("operation was cancelled") because no foreground UIScene is yet available to anchor the system icon-change alert. Without a completion handler the failure was silent, which is why developers reported "no localized icon and no permission dialog" even after #4870. Changes in buildLocalizedIconSelectorObjC(): * Defer the icon switch to the next UIApplicationDidBecomeActiveNotification (or run immediately on the main queue if the app is already active), giving iOS an active scene to host the system alert. * Pass a real completion handler that NSLogs successes and errors so any remaining bundle-configuration problem is visible in the device log instead of being swallowed. Validated end-to-end against a freshly generated cn1app-archetype project: actool produces the expected CFBundleAlternateIcons entry, the resulting .app builds for the iOS 18.6 and iOS 26.3 simulators, and LaunchServices logs "Setting preferredIconName to AppIcon_es" on launch under es_ES locale, where the prior selector logged NSUserCancelledError. The matching change is in the BuildDaemon repo. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/codename1/builders/IPhoneBuilder.java | 73 ++++++++++++++----- 1 file changed, 53 insertions(+), 20 deletions(-) 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 188bd09878..ddb3e90687 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 @@ -3185,32 +3185,65 @@ private String buildLocalizedIconSelectorObjC() { mapping.append("@\"").append(entry.getValue()).append("\": @\"").append(entry.getKey()).append("\""); } mapping.append(" }"); + // Wait for UIApplicationDidBecomeActiveNotification before calling + // -[UIApplication setAlternateIconName:]. Calling from didFinishLaunching -- + // even via dispatch_async on the main queue -- routinely fails with + // NSCocoaErrorDomain Code=3072 ("operation was cancelled") because the + // system alert iOS shows for an icon change has no active foreground scene + // to anchor to yet. Deferring to the active state fixes the silent failure + // that the user reported on top of #4870. The completion handler is wired + // up so any remaining bundle-configuration problem surfaces in the device + // log instead of being swallowed (the original nil handler hid this). return "\n // Codename One localized app icon selection\n" + " if ([[UIApplication sharedApplication] respondsToSelector:@selector(setAlternateIconName:completionHandler:)]) {\n" + " NSDictionary *cn1LocalizedIcons =\n" + mapping + ";\n" - + " NSString *cn1CurrentIcon = [[UIApplication sharedApplication] alternateIconName];\n" - + " NSString *cn1TargetIcon = nil;\n" - + " NSArray *cn1PrefLangs = [NSLocale preferredLanguages];\n" - + " if (cn1PrefLangs.count > 0) {\n" - + " NSString *cn1PrefLang = [cn1PrefLangs objectAtIndex:0];\n" - + " NSArray *cn1LangParts = [cn1PrefLang componentsSeparatedByCharactersInSet:\n" - + " [NSCharacterSet characterSetWithCharactersInString:@\"-_\"]];\n" - + " if (cn1LangParts.count >= 2) {\n" - + " NSString *cn1Key = [NSString stringWithFormat:@\"%@_%@\",\n" - + " [[cn1LangParts objectAtIndex:0] lowercaseString],\n" - + " [[cn1LangParts objectAtIndex:1] uppercaseString]];\n" - + " cn1TargetIcon = [cn1LocalizedIcons objectForKey:cn1Key];\n" + + " void (^cn1ApplyIcon)(void) = ^{\n" + + " NSString *cn1CurrentIcon = [[UIApplication sharedApplication] alternateIconName];\n" + + " NSString *cn1TargetIcon = nil;\n" + + " NSArray *cn1PrefLangs = [NSLocale preferredLanguages];\n" + + " if (cn1PrefLangs.count > 0) {\n" + + " NSString *cn1PrefLang = [cn1PrefLangs objectAtIndex:0];\n" + + " NSArray *cn1LangParts = [cn1PrefLang componentsSeparatedByCharactersInSet:\n" + + " [NSCharacterSet characterSetWithCharactersInString:@\"-_\"]];\n" + + " if (cn1LangParts.count >= 2) {\n" + + " NSString *cn1Key = [NSString stringWithFormat:@\"%@_%@\",\n" + + " [[cn1LangParts objectAtIndex:0] lowercaseString],\n" + + " [[cn1LangParts objectAtIndex:1] uppercaseString]];\n" + + " cn1TargetIcon = [cn1LocalizedIcons objectForKey:cn1Key];\n" + + " }\n" + + " if (cn1TargetIcon == nil && cn1LangParts.count >= 1) {\n" + + " NSString *cn1Key = [[cn1LangParts objectAtIndex:0] lowercaseString];\n" + + " cn1TargetIcon = [cn1LocalizedIcons objectForKey:cn1Key];\n" + + " }\n" + " }\n" - + " if (cn1TargetIcon == nil && cn1LangParts.count >= 1) {\n" - + " NSString *cn1Key = [[cn1LangParts objectAtIndex:0] lowercaseString];\n" - + " cn1TargetIcon = [cn1LocalizedIcons objectForKey:cn1Key];\n" + + " BOOL cn1NeedsUpdate = (cn1TargetIcon == nil && cn1CurrentIcon != nil)\n" + + " || (cn1TargetIcon != nil && ![cn1TargetIcon isEqualToString:cn1CurrentIcon]);\n" + + " if (!cn1NeedsUpdate) {\n" + + " return;\n" + " }\n" - + " }\n" - + " BOOL cn1NeedsUpdate = (cn1TargetIcon == nil && cn1CurrentIcon != nil)\n" - + " || (cn1TargetIcon != nil && ![cn1TargetIcon isEqualToString:cn1CurrentIcon]);\n" - + " if (cn1NeedsUpdate) {\n" - + " [[UIApplication sharedApplication] setAlternateIconName:cn1TargetIcon completionHandler:nil];\n" + + " NSString *cn1FinalTarget = cn1TargetIcon;\n" + + " [[UIApplication sharedApplication] setAlternateIconName:cn1FinalTarget completionHandler:^(NSError * _Nullable cn1IconErr) {\n" + + " if (cn1IconErr != nil) {\n" + + " NSLog(@\"[CodenameOne] Failed to set alternate app icon '%@': %@\", cn1FinalTarget ?: @\"(primary)\", cn1IconErr);\n" + + " } else {\n" + + " NSLog(@\"[CodenameOne] Set alternate app icon to '%@'\", cn1FinalTarget ?: @\"(primary)\");\n" + + " }\n" + + " }];\n" + + " };\n" + + " if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {\n" + + " dispatch_async(dispatch_get_main_queue(), cn1ApplyIcon);\n" + + " } else {\n" + + " __block id cn1ActiveObs = nil;\n" + + " cn1ActiveObs = [[NSNotificationCenter defaultCenter]\n" + + " addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:[NSOperationQueue mainQueue]\n" + + " usingBlock:^(NSNotification *cn1Note) {\n" + + " if (cn1ActiveObs != nil) {\n" + + " [[NSNotificationCenter defaultCenter] removeObserver:cn1ActiveObs];\n" + + " cn1ActiveObs = nil;\n" + + " }\n" + + " cn1ApplyIcon();\n" + + " }];\n" + " }\n" + " }\n"; }