diff --git a/CodenameOne/src/com/codename1/system/package-info.java b/CodenameOne/src/com/codename1/system/package-info.java index 2b5b12fdb7..66468b03b3 100644 --- a/CodenameOne/src/com/codename1/system/package-info.java +++ b/CodenameOne/src/com/codename1/system/package-info.java @@ -2,7 +2,7 @@ /// [support for making platform native API calls](https://www.codenameone.com/how-do-i---access-native-device-functionality-invoke-native-interfaces.html). Notice /// that when we say "native" we do not mean C/C++ always but rather the platforms "native" environment. So in the /// case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C -/// message would be sent and so forth. +/// or Swift message would be sent and so forth. /// /// Native interfaces are designed to only allow primitive types, Strings, arrays (single dimension only!) of primitives /// and PeerComponent values. Any other type of parameter/return type is prohibited. However, once in the native layer @@ -59,9 +59,9 @@ /// These sources should be placed under the appropriate folder in the native directory and are sent to the /// server for compilation. /// -/// For Objective-C, one would need to define a class matching the name of the package and the class name -/// combined where the "." elements are replaced by underscores. One would need to provide both a header and -/// an "m" file following this convention e.g.: +/// For iOS, one would need to define a class matching the name of the package and the class name +/// combined where the "." elements are replaced by underscores. This class can be implemented in Objective-C +/// (by providing both a header and an "m" file) or in Swift. Objective-C classes follow this convention e.g.: /// /// ```java /// @interface com_my_code_MyNative : NSObject { diff --git a/CodenameOne/src/com/codename1/system/package.html b/CodenameOne/src/com/codename1/system/package.html index 590dd50809..400f9a20ef 100644 --- a/CodenameOne/src/com/codename1/system/package.html +++ b/CodenameOne/src/com/codename1/system/package.html @@ -8,8 +8,8 @@ support for making platform native API calls. Notice that when we say "native" we do not mean C/C++ always but rather the platforms "native" environment. So in the - case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C - message would be sent and so forth. + case of Android the Java code will be invoked with full access to the Android API's, in case of iOS an Objective-C + or Swift message would be sent and so forth.
Native interfaces are designed to only allow primitive types, Strings, arrays (single dimension only!) of primitives @@ -66,9 +66,9 @@ server for compilation.
- For Objective-C, one would need to define a class matching the name of the package and the class name - combined where the "." elements are replaced by underscores. One would need to provide both a header and - an "m" file following this convention e.g.: + For iOS, one would need to define a class matching the name of the package and the class name + combined where the "." elements are replaced by underscores. This class can be implemented in Objective-C + (by providing both a header and an "m" file) or in Swift. Objective-C classes follow this convention e.g.:
@interface com_my_code_MyNative : NSObject {
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
index 4377ebab93..3f83d699cf 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
@@ -41,7 +41,9 @@
import java.awt.image.ImageProducer;
import java.awt.image.RGBImageFilter;
import java.io.*;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.FileChannel;
@@ -3972,6 +3974,117 @@ static String xmlize(String s) {
}
+ @Override
+ protected String registerNativeImplementationsAndCreateStubs(ClassLoader parentClassLoader, File stubDir, File... classesDirectory) throws MalformedURLException, IOException {
+ Class[] discoveredNativeInterfaces = findNativeInterfaces(parentClassLoader, classesDirectory);
+ String registerNativeFunctions = "";
+ if (discoveredNativeInterfaces != null && discoveredNativeInterfaces.length > 0) {
+ for (Class n : discoveredNativeInterfaces) {
+ registerNativeFunctions += " NativeLookup.register(" + n.getName() + ".class, "
+ + n.getName() + "Stub.class" + ");\n";
+ }
+ }
+
+ if (discoveredNativeInterfaces != null && discoveredNativeInterfaces.length > 0) {
+ for (Class currentNative : discoveredNativeInterfaces) {
+ File folder = new File(stubDir, currentNative.getPackage().getName().replace('.', File.separatorChar));
+ folder.mkdirs();
+ File javaFile = new File(folder, currentNative.getSimpleName() + "Stub.java");
+
+ String javaImplSourceFile = "package " + currentNative.getPackage().getName() + ";\n\n"
+ + "import com.codename1.ui.PeerComponent;\n\n"
+ + "public class " + currentNative.getSimpleName() + "Stub implements " + currentNative.getSimpleName() + "{\n"
+ + " private final Object impl = createImpl();\n\n"
+ + " private static Object createImpl() {\n"
+ + " try {\n"
+ + " return Class.forName(\"" + currentNative.getName() + getImplSuffix() + "\").newInstance();\n"
+ + " } catch (Throwable t) {\n"
+ + " throw new RuntimeException(\"Failed to instantiate native implementation for " + currentNative.getName() + "\", t);\n"
+ + " }\n"
+ + " }\n\n"
+ + " private Object __cn1Invoke(String methodName, Object[] args) {\n"
+ + " try {\n"
+ + " java.lang.reflect.Method[] methods = impl.getClass().getMethods();\n"
+ + " for (java.lang.reflect.Method method : methods) {\n"
+ + " if (method.getName().equals(methodName) && method.getParameterTypes().length == args.length) {\n"
+ + " return method.invoke(impl, args);\n"
+ + " }\n"
+ + " }\n"
+ + " throw new RuntimeException(methodName + \" with \" + args.length + \" args\");\n"
+ + " } catch (Throwable t) {\n"
+ + " throw new RuntimeException(\"Failed to invoke native method \" + methodName, t);\n"
+ + " }\n"
+ + " }\n\n";
+
+ for (Method m : currentNative.getMethods()) {
+ String name = m.getName();
+ if (name.equals("hashCode") || name.equals("equals") || name.equals("toString")) {
+ continue;
+ }
+
+ Class returnType = m.getReturnType();
+
+ javaImplSourceFile += " public " + returnType.getSimpleName() + " " + name + "(";
+ Class[] params = m.getParameterTypes();
+ String args = "";
+ if (params != null && params.length > 0) {
+ for (int iter = 0; iter < params.length; iter++) {
+ if (iter > 0) {
+ javaImplSourceFile += ", ";
+ args += ", ";
+ }
+ javaImplSourceFile += params[iter].getSimpleName() + " param" + iter;
+ if (params[iter].getName().equals("com.codename1.ui.PeerComponent")) {
+ args += convertPeerComponentToNative("param" + iter);
+ } else {
+ args += "param" + iter;
+ }
+ }
+ }
+ javaImplSourceFile += ") {\n";
+ String invocationExpression = "__cn1Invoke(\"" + name + "\", new Object[]{" + args + "})";
+ if (Void.class == returnType || Void.TYPE == returnType) {
+ javaImplSourceFile += " " + invocationExpression + ";\n }\n\n";
+ } else {
+ if (returnType.getName().equals("com.codename1.ui.PeerComponent")) {
+ javaImplSourceFile += " return " + generatePeerComponentCreationCode(invocationExpression) + ";\n }\n\n";
+ } else if (returnType.isPrimitive()) {
+ if (returnType == Boolean.TYPE) {
+ javaImplSourceFile += " return ((Boolean)" + invocationExpression + ").booleanValue();\n }\n\n";
+ } else if (returnType == Integer.TYPE) {
+ javaImplSourceFile += " return ((Integer)" + invocationExpression + ").intValue();\n }\n\n";
+ } else if (returnType == Long.TYPE) {
+ javaImplSourceFile += " return ((Long)" + invocationExpression + ").longValue();\n }\n\n";
+ } else if (returnType == Byte.TYPE) {
+ javaImplSourceFile += " return ((Byte)" + invocationExpression + ").byteValue();\n }\n\n";
+ } else if (returnType == Short.TYPE) {
+ javaImplSourceFile += " return ((Short)" + invocationExpression + ").shortValue();\n }\n\n";
+ } else if (returnType == Character.TYPE) {
+ javaImplSourceFile += " return ((Character)" + invocationExpression + ").charValue();\n }\n\n";
+ } else if (returnType == Float.TYPE) {
+ javaImplSourceFile += " return ((Float)" + invocationExpression + ").floatValue();\n }\n\n";
+ } else if (returnType == Double.TYPE) {
+ javaImplSourceFile += " return ((Double)" + invocationExpression + ").doubleValue();\n }\n\n";
+ } else {
+ javaImplSourceFile += " return (" + returnType.getSimpleName() + ")" + invocationExpression + ";\n }\n\n";
+ }
+ } else {
+ javaImplSourceFile += " return (" + returnType.getSimpleName() + ")" + invocationExpression + ";\n }\n\n";
+ }
+ }
+ }
+
+ javaImplSourceFile += "}\n";
+
+ try (FileOutputStream out = new FileOutputStream(javaFile)) {
+ out.write(javaImplSourceFile.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ }
+
+ return registerNativeFunctions;
+ }
+
@Override
protected String generatePeerComponentCreationCode(String methodCallString) {
return "PeerComponent.create(" + methodCallString + ")";
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 e635fb255d..16ba2a76e7 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
@@ -1097,16 +1097,62 @@ public void usesClassMethod(String cls, String method) {
String classNameWithUnderscores = currentNative.getName().replace('.', '_');
String mSourceFile = "#include \"xmlvm.h\"\n"
+ "#include \"java_lang_String.h\"\n"
+ + "#include \n"
+ "#import \"CodenameOne_GLViewController.h\"\n"
+ "#import \n"
- + "#import \"" + classNameWithUnderscores + "Impl.h\"\n" + newVMInclude
+ + "#import \n"
+ + newVMInclude
+ "#include \"" + classNameWithUnderscores + "ImplCodenameOne.h\"\n\n"
+ + "static id cn1_createNativeInterfacePeer(NSString* className) {\n"
+ + " NSMutableArray* candidates = [NSMutableArray arrayWithObject:className];\n"
+ + " NSString* executableName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\"CFBundleExecutable\"];\n"
+ + " NSString* bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\"CFBundleName\"];\n"
+ + " NSArray* moduleNames = @[executableName ?: @\"\", bundleName ?: @\"\"];\n"
+ + " for(NSString* moduleName in moduleNames) {\n"
+ + " if(moduleName.length == 0) {\n"
+ + " continue;\n"
+ + " }\n"
+ + " NSString* sanitized = [[moduleName stringByReplacingOccurrencesOfString:@\"-\" withString:@\"_\"] stringByReplacingOccurrencesOfString:@\" \" withString:@\"_\"];\n"
+ + " [candidates addObject:[sanitized stringByAppendingFormat:@\".%@\", className]];\n"
+ + " if(![sanitized isEqualToString:moduleName]) {\n"
+ + " [candidates addObject:[moduleName stringByAppendingFormat:@\".%@\", className]];\n"
+ + " }\n"
+ + " }\n"
+ + " Class cls = Nil;\n"
+ + " for(NSString* candidate in candidates) {\n"
+ + " cls = NSClassFromString(candidate);\n"
+ + " if(cls != Nil) {\n"
+ + " break;\n"
+ + " }\n"
+ + " }\n"
+ + " if(cls == Nil) {\n"
+ + " unsigned int classCount = 0;\n"
+ + " Class *classList = objc_copyClassList(&classCount);\n"
+ + " NSString* dottedSuffix = [@\".\" stringByAppendingString:className];\n"
+ + " for(unsigned int i = 0; i < classCount; i++) {\n"
+ + " NSString* runtimeName = [NSString stringWithUTF8String:class_getName(classList[i])];\n"
+ + " if([runtimeName isEqualToString:className] || [runtimeName hasSuffix:dottedSuffix] || [runtimeName hasSuffix:className]) {\n"
+ + " cls = classList[i];\n"
+ + " NSLog(@\"[CN1] Resolved native interface class %@ via runtime scan as %@\", className, runtimeName);\n"
+ + " break;\n"
+ + " }\n"
+ + " }\n"
+ + " if(classList != NULL) {\n"
+ + " free(classList);\n"
+ + " }\n"
+ + " }\n"
+ + " if(cls == Nil) {\n"
+ + " NSLog(@\"[CN1] Failed to find native interface class %@. Tried: %@\", className, candidates);\n"
+ + " return nil;\n"
+ + " }\n"
+ + " return [[cls alloc] init];\n"
+ + "}\n\n"
+ "JAVA_LONG " + classNameWithUnderscores + "ImplCodenameOne_initializeNativePeer__" + postfixForNewVM + "(" + prefixForNewVM + ") {\n"
- + " " + classNameWithUnderscores + "Impl* i = [[" + classNameWithUnderscores + "Impl alloc] init];\n"
+ + " id i = cn1_createNativeInterfacePeer(@\"" + classNameWithUnderscores + "Impl\");\n"
+ " return i;\n"
+ "}\n\n"
+ "void " + classNameWithUnderscores + "ImplCodenameOne_releaseNativePeerInstance___long(" + prefix2ForNewVM + "JAVA_LONG l) {\n"
- + " " + classNameWithUnderscores + "Impl* i = (" + classNameWithUnderscores + "Impl*)l;\n"
+ + " id i = (id)l;\n"
+ " [i release];\n"
+ "}\n\n"
+ "extern NSData* arrayToData(JAVA_OBJECT arr);\n"
@@ -1135,8 +1181,7 @@ public void usesClassMethod(String cls, String method) {
String mFileBody;
mFileArgs = "(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT me";
- mFileBody = " " + classNameWithUnderscores + "Impl* ptr = (" + classNameWithUnderscores +
- "Impl*)get_field_" + classNameWithUnderscores + "ImplCodenameOne_nativePeer(me);\n";
+ mFileBody = " id ptr = (id)get_field_" + classNameWithUnderscores + "ImplCodenameOne_nativePeer(me);\n";
if(!(returnType.equals(Void.class) || returnType.equals(Void.TYPE))) {
@@ -1866,6 +1911,25 @@ public void usesClassMethod(String cls, String method) {
+ " puts \"Backtrace:\\n\\t#{e.backtrace.join(\"\\n\\t\")}\"\n"
+ " puts 'An error occurred recreating schemes, but the build still might work...'\n"
+ "end\n"
+ + "begin\n"
+ + " main_target = xcproj.targets.find{|e| e.name==main_class_name}\n"
+ + " if main_target\n"
+ + " swift_refs = xcproj.files.select{|f| f.path && f.path.end_with?('.swift')}\n"
+ + " swift_refs.each do |ref|\n"
+ + " unless main_target.source_build_phase.files_references.include?(ref)\n"
+ + " main_target.source_build_phase.add_file_reference(ref, true)\n"
+ + " end\n"
+ + " main_target.resources_build_phase.files.each do |bf|\n"
+ + " if bf.file_ref == ref\n"
+ + " main_target.resources_build_phase.remove_build_file(bf)\n"
+ + " end\n"
+ + " end\n"
+ + " end\n"
+ + " end\n"
+ + "rescue => e\n"
+ + " puts \"Error while correcting Swift build phases: #{$!}\"\n"
+ + " puts \"Backtrace:\\n\\t#{e.backtrace.join(\"\\n\\t\")}\"\n"
+ + "end\n"
+ deploymentTargetStr
+ appExtensionsBuilder.toString();
File hooksDir = new File(tmpFile, "hooks");
diff --git a/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt
new file mode 100644
index 0000000000..29a6938b88
--- /dev/null
+++ b/scripts/hellocodenameone/android/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNativeImpl.kt
@@ -0,0 +1,15 @@
+package com.codenameone.examples.hellocodenameone
+
+class SwiftKotlinNativeImpl {
+ fun implementationLanguage(): String {
+ return "kotlin"
+ }
+
+ fun diagnostics(): String {
+ return "android-kotlin-native-impl"
+ }
+
+ fun isSupported(): Boolean {
+ return true
+ }
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/NativeInterfaceLanguageValidator.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/NativeInterfaceLanguageValidator.java
new file mode 100644
index 0000000000..f1b0a08dec
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/NativeInterfaceLanguageValidator.java
@@ -0,0 +1,50 @@
+package com.codenameone.examples.hellocodenameone;
+
+import com.codename1.system.NativeLookup;
+import com.codename1.ui.CN;
+
+public final class NativeInterfaceLanguageValidator {
+ private static String lastStatus = "UNINITIALIZED";
+
+ private NativeInterfaceLanguageValidator() {
+ }
+
+ public static String getLastStatus() {
+ return lastStatus;
+ }
+
+ public static void validate() {
+ String platformName = CN.getPlatformName();
+ String normalizedPlatform = platformName == null ? "" : platformName.toLowerCase();
+ System.out.println("CN1SS:SWIFT_DIAG:START platform=" + platformName);
+ lastStatus = "START platform=" + platformName;
+ boolean isAndroid = normalizedPlatform.contains("android");
+ boolean isIos = normalizedPlatform.contains("ios") || normalizedPlatform.contains("iphone");
+ if (!isAndroid && !isIos) {
+ System.out.println("CN1SS:SWIFT_DIAG:SKIP platform=" + platformName);
+ lastStatus = "SKIP platform=" + platformName;
+ return;
+ }
+
+ SwiftKotlinNative nativeImpl = NativeLookup.create(SwiftKotlinNative.class);
+ System.out.println("CN1SS:SWIFT_DIAG:NATIVE_LOOKUP result=" + (nativeImpl == null ? "null" : nativeImpl.getClass().getName()));
+ if (nativeImpl == null) {
+ lastStatus = "LOOKUP_NULL platform=" + platformName;
+ throw new IllegalStateException("SwiftKotlinNative lookup returned null on " + platformName);
+ }
+ if (!nativeImpl.isSupported()) {
+ lastStatus = "NOT_SUPPORTED platform=" + platformName;
+ throw new IllegalStateException("SwiftKotlinNative is not available on " + platformName);
+ }
+
+ String expected = isAndroid ? "kotlin" : "swift";
+ String actual = nativeImpl.implementationLanguage();
+ String diagnostics = nativeImpl.diagnostics();
+ System.out.println("CN1SS:SWIFT_DIAG:RESULT expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics);
+ if (!expected.equalsIgnoreCase(actual)) {
+ lastStatus = "MISMATCH expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics;
+ throw new IllegalStateException("Expected " + expected + " implementation on " + platformName + " but got " + actual + ". diagnostics=" + diagnostics);
+ }
+ lastStatus = "OK expected=" + expected + " actual=" + actual + " diagnostics=" + diagnostics;
+ }
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNative.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNative.java
new file mode 100644
index 0000000000..cc495d7b4a
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/SwiftKotlinNative.java
@@ -0,0 +1,8 @@
+package com.codenameone.examples.hellocodenameone;
+
+import com.codename1.system.NativeInterface;
+
+public interface SwiftKotlinNative extends NativeInterface {
+ String implementationLanguage();
+ String diagnostics();
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
index 29d31a3a1f..8bf85f419f 100644
--- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
@@ -7,6 +7,7 @@
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.util.StringUtil;
+import com.codenameone.examples.hellocodenameone.NativeInterfaceLanguageValidator;
import com.codenameone.examples.hellocodenameone.tests.graphics.AffineScale;
import com.codenameone.examples.hellocodenameone.tests.graphics.Clip;
import com.codenameone.examples.hellocodenameone.tests.graphics.DrawArc;
@@ -131,6 +132,7 @@ public void runSuite() {
}
log("CN1SS:INFO:suite finished test=" + testName);
}
+ log("CN1SS:INFO:swift_diag_status=" + NativeInterfaceLanguageValidator.getLastStatus());
log("CN1SS:SUITE:FINISHED");
TestReporting.getInstance().testExecutionFinished(getClass().getName());
if (CN.isSimulator()) {
diff --git a/scripts/hellocodenameone/common/src/main/kotlin/com/codenameone/examples/hellocodenameone/HelloCodenameOne.kt b/scripts/hellocodenameone/common/src/main/kotlin/com/codenameone/examples/hellocodenameone/HelloCodenameOne.kt
index 2214782035..a768e6f3f9 100644
--- a/scripts/hellocodenameone/common/src/main/kotlin/com/codenameone/examples/hellocodenameone/HelloCodenameOne.kt
+++ b/scripts/hellocodenameone/common/src/main/kotlin/com/codenameone/examples/hellocodenameone/HelloCodenameOne.kt
@@ -14,6 +14,12 @@ open class HelloCodenameOne : Lifecycle() {
"Jailbroken device detected by Display.isJailbrokenDevice()."
}
DefaultMethodDemo.validate()
+ try {
+ NativeInterfaceLanguageValidator.validate()
+ } catch (t: Throwable) {
+ System.out.println("CN1SS:SWIFT_DIAG:VALIDATION_EXCEPTION " + t.javaClass.name + ": " + t.message)
+ t.printStackTrace()
+ }
Cn1ssDeviceRunner.addTest(KotlinUiTest())
TestReporting.setInstance(Cn1ssDeviceRunnerReporter())
}
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h
new file mode 100644
index 0000000000..208e61f7c1
--- /dev/null
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h
@@ -0,0 +1,9 @@
+#import
+
+@interface com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl : NSObject
+
+-(NSString*)implementationLanguage;
+-(NSString*)diagnostics;
+-(BOOL)isSupported;
+
+@end
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
new file mode 100644
index 0000000000..7a27226bb9
--- /dev/null
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.m
@@ -0,0 +1,57 @@
+#import "com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.h"
+#import
+#include
+
+@implementation com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl
+
+-(id)getBridgeInstance {
+ Class bridgeClass = NSClassFromString(@"CN1SwiftKotlinNativeBridge");
+ if (bridgeClass == Nil) {
+ unsigned int classCount = 0;
+ Class *classList = objc_copyClassList(&classCount);
+ NSString *targetName = @"CN1SwiftKotlinNativeBridge";
+ NSString *dottedSuffix = [@".CN1SwiftKotlinNativeBridge" copy];
+ for (unsigned int i = 0; i < classCount; i++) {
+ NSString *runtimeName = [NSString stringWithUTF8String:class_getName(classList[i])];
+ if ([runtimeName isEqualToString:targetName] || [runtimeName hasSuffix:dottedSuffix] || [runtimeName hasSuffix:targetName]) {
+ bridgeClass = classList[i];
+ NSLog(@"[CN1] Found Swift bridge class as %@", runtimeName);
+ break;
+ }
+ }
+ if (classList != NULL) {
+ free(classList);
+ }
+ }
+ if (bridgeClass == Nil) {
+ NSLog(@"[CN1] Swift bridge class CN1SwiftKotlinNativeBridge was not found");
+ return nil;
+ }
+ return [[bridgeClass alloc] init];
+}
+
+-(NSString*)implementationLanguage {
+ id bridge = [self getBridgeInstance];
+ if (bridge != nil && [bridge respondsToSelector:@selector(implementationLanguage)]) {
+ return [bridge implementationLanguage];
+ }
+ return @"swift-bridge-missing";
+}
+
+-(NSString*)diagnostics {
+ id bridge = [self getBridgeInstance];
+ if (bridge != nil && [bridge respondsToSelector:@selector(diagnostics)]) {
+ return [bridge diagnostics];
+ }
+ return @"ios-swift-bridge-missing";
+}
+
+-(BOOL)isSupported {
+ id bridge = [self getBridgeInstance];
+ if (bridge != nil && [bridge respondsToSelector:@selector(isSupported)]) {
+ return [bridge isSupported];
+ }
+ return NO;
+}
+
+@end
diff --git a/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.swift b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.swift
new file mode 100644
index 0000000000..667fd23d12
--- /dev/null
+++ b/scripts/hellocodenameone/ios/src/main/objectivec/com_codenameone_examples_hellocodenameone_SwiftKotlinNativeImpl.swift
@@ -0,0 +1,17 @@
+import Foundation
+
+@objc(CN1SwiftKotlinNativeBridge)
+@objcMembers
+public class CN1SwiftKotlinNativeBridge: NSObject {
+ @objc func implementationLanguage() -> String {
+ return "swift"
+ }
+
+ @objc func diagnostics() -> String {
+ return "ios-swift-native-impl"
+ }
+
+ @objc func isSupported() -> Bool {
+ return true
+ }
+}
diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh
index a6328ceb2e..cd41b858d3 100755
--- a/scripts/run-ios-ui-tests.sh
+++ b/scripts/run-ios-ui-tests.sh
@@ -667,6 +667,17 @@ xcrun simctl spawn "$SIM_DEVICE_ID" \
--predicate '(composedMessage CONTAINS "CN1SS") OR (eventMessage CONTAINS "CN1SS")' \
> "$FALLBACK_LOG" 2>/dev/null || true
+SWIFT_DIAG_LINE="$( (grep -h "CN1SS:INFO:swift_diag_status=" "$TEST_LOG" "$FALLBACK_LOG" || true) | tail -n 1 )"
+if [ -n "$SWIFT_DIAG_LINE" ]; then
+ ri_log "Detected swift diagnostic status line: $SWIFT_DIAG_LINE"
+ if ! echo "$SWIFT_DIAG_LINE" | grep -q "swift_diag_status=OK "; then
+ ri_log "STAGE:SWIFT_DIAG_FAILED -> $SWIFT_DIAG_LINE"
+ exit 13
+ fi
+else
+ ri_log "STAGE:SWIFT_DIAG_MISSING -> No swift_diag_status marker found"
+fi
+
if [ -n "$SIM_DEVICE_ID" ]; then
xcrun simctl terminate "$SIM_DEVICE_ID" "$BUNDLE_IDENTIFIER" >/dev/null 2>&1 || true
fi