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/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
index 0daff0c320..d9bbf856f9 100644
--- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
+++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
@@ -684,7 +684,7 @@ Sometimes you may wish to use an API that is unsupported by Codename One or inte
==== Introduction
-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, in case of iOS an Objective-C message would be sent and so forth.
+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, Java or Kotlin code can be invoked with full access to the Android API. In case of iOS, Objective-C or Swift code can be invoked and so forth.
TIP: You can still access C code under Android either by using JNI from the Android native code or by using a library
@@ -747,7 +747,7 @@ include::../demos/common/src/main/java/com/mycompany/myapp/MyNativeImplStub.java
The stub implementation always returns `false`, `null` or `0` by default. The `isSupported` also defaults to `false` thus allowing us to implement a `NativeInterface` on some platforms and leave the rest out without really knowing anything about these platforms.
-We can implement the Android version using code similar to this:
+We can implement the Android version in Java or Kotlin. Here is the Java version:
[source,java]
----
@@ -768,7 +768,27 @@ Codename One doesn't include the native platforms in its bundle e.g. the full An
TIP: When implementing a non-trivial native interface, send a server build with the "Include Source" option checked. Implement the native interface in the native IDE then copy and paste the native code back into Codename One
-The implementation of this interface is nearly identical for Android, J2ME & Java SE.
+The implementation of this interface is nearly identical for Android (Java/Kotlin), J2ME & Java SE.
+
+===== Swift (iOS) and Kotlin (Android) options
+
+For iOS native interfaces you can implement the generated `...Impl` class in Objective-C _or_ Swift. +
+For Android native interfaces you can implement the generated `...Impl` class in Java _or_ Kotlin.
+
+[cols="1,3",options="header"]
+|===
+| Platform
+| Typical file locations for native interface implementations
+
+| Android (Java/Kotlin)
+| `android/src/main/java/com/mycompany/myapp/MyNativeImpl.java` +
+`android/src/main/java/com/mycompany/myapp/MyNativeImpl.kt`
+
+| iOS (Objective-C/Swift)
+| `ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.h` +
+`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m` +
+`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.swift`
+|===
===== Use the Android Main Thread (Native EDT)
@@ -834,9 +854,10 @@ android.sdkVersion=25
Once those were defined the native code for the Android implementation became trivial to write and the library was easy as there were no jars to include.
-==== Objective-C (iOS)
+==== Objective-C and Swift (iOS)
-When generating the Objective-C code the "Generate Native Sources" tool produces two files: `com_mycompany_myapp_MyNativeImpl.h` & `com_mycompany_myapp_MyNativeImpl.m`.
+When generating the Objective-C code the "Generate Native Sources" tool produces two files by default: `com_mycompany_myapp_MyNativeImpl.h` & `com_mycompany_myapp_MyNativeImpl.m`.
+If you enable Swift stub generation in the Maven goal, it can also produce `com_mycompany_myapp_MyNativeImpl.swift`.
The `.m` files are the Objective-C equivalent of `.c` files and `.h` files contain the header/include information. In this case the `com_mycompany_myapp_MyNativeImpl.h` contains:
@@ -863,6 +884,8 @@ Here is a simple implementation similar to above:
include::../demos/ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m[tag=myNativeImplExample,indent=0]
----
+If you prefer Swift for iOS native interfaces, keep the same class naming convention (`com_mycompany_myapp_MyNativeImpl`) and annotate the class with `@objc(...)` so the runtime can discover it.
+
===== Using the iOS Main Thread (Native EDT)
iOS has a native thread you should use for all calls just like Android. Check out the Native EDT on Android section above for reference.
diff --git a/docs/developer-guide/appendix_goal_generate_native_interfaces.adoc b/docs/developer-guide/appendix_goal_generate_native_interfaces.adoc
index 3ca6d574a0..d630cc0892 100644
--- a/docs/developer-guide/appendix_goal_generate_native_interfaces.adoc
+++ b/docs/developer-guide/appendix_goal_generate_native_interfaces.adoc
@@ -17,6 +17,15 @@ After creating this (and possibly other) native interfaces in our project, run
mvn cn1:generate-native-interfaces
----
+By default this generates Java/Objective-C stubs. You can optionally include Swift and Kotlin stubs (both off by default):
+
+[source,bash]
+----
+mvn cn1:generate-native-interfaces \
+ -Dcn1.generateNativeInterfaces.swift=true \
+ -Dcn1.generateNativeInterfaces.kotlin=true
+----
+
This will generate the following files (if they don't exist yet).
javase::
@@ -26,9 +35,12 @@ ios::
. `ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.m`
android::
`android/src/main/java/com/mycompany/myapp/MyNativeImpl.java`
+android (optional Kotlin)::
+`android/src/main/java/com/mycompany/myapp/MyNativeImpl.kt`
javascript::
`javascript/src/main/javascript/com_mycompany_myapp_MyNativeImpl.js`
+ios (optional Swift)::
+`ios/src/main/objectivec/com_mycompany_myapp_MyNativeImpl.swift`
Open and edit these files to implement your native interface methods as desired.
-
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..f6645b2f33 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))) {
@@ -1714,6 +1759,9 @@ public void usesClassMethod(String cls, String method) {
deploymentTargetStr = "begin\n"
+ " xcproj.targets.find{|e|e.name=='" + request.getMainClass() + "'}.build_configurations.each{|config| \n"
+ " config.build_settings['PRODUCT_BUNDLE_IDENTIFIER']='"+request.getPackageName()+"'\n"
+ + " config.build_settings['DEFINES_MODULE']='YES'\n"
+ + " config.build_settings['SWIFT_VERSION']='5.0'\n"
+ + " config.build_settings['SWIFT_OBJC_BRIDGING_HEADER']='$(SRCROOT)/cn1-Bridging-Header.h'\n"
+ " }\n"
+ " xcproj.targets.each do |target|\n"
+ " target.build_configurations.each do |config|\n"
@@ -1853,6 +1901,7 @@ public void usesClassMethod(String cls, String method) {
String createSchemesScript = "#!/usr/bin/env ruby\n" +
"require 'xcodeproj'\n" +
+ "require 'pathname'\n" +
"main_class_name = \"" + request.getMainClass() + "\"\n" +
"project_file = \"" +
tmpDir.getAbsolutePath() + "/dist/" +
@@ -1866,8 +1915,63 @@ 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"
+ + " project_root = File.dirname(project_file)\n"
+ + " swift_paths = Dir.glob(File.join(project_root, main_class_name + '-src', '**', '*.swift'))\n"
+ + " swift_paths.each do |swift_path|\n"
+ + " rel_path = Pathname.new(swift_path).relative_path_from(Pathname.new(project_root)).to_s\n"
+ + " ref = xcproj.files.find{|f| f.path == rel_path} || xcproj.main_group.new_file(rel_path)\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"
+ + " begin\n"
+ + " main_target.resources_build_phase.remove_file_reference(ref)\n"
+ + " rescue\n"
+ + " end\n"
+ + " end\n"
+ + " swift_refs = xcproj.files.select do |f|\n"
+ + " file_name = f.path || f.name || f.display_name\n"
+ + " file_name && file_name.downcase.end_with?('.swift')\n"
+ + " end\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"
+ + " begin\n"
+ + " main_target.resources_build_phase.remove_file_reference(ref)\n"
+ + " rescue\n"
+ + " end\n"
+ + " end\n"
+ + " swift_resource_files = main_target.resources_build_phase.files.select do |bf|\n"
+ + " ref = bf.file_ref\n"
+ + " file_name = (ref && (ref.path || ref.name || ref.display_name)) || bf.display_name\n"
+ + " file_name && file_name.downcase.end_with?('.swift')\n"
+ + " end\n"
+ + " swift_resource_files.each do |bf|\n"
+ + " main_target.resources_build_phase.files.delete(bf)\n"
+ + " end\n"
+ + " source_folder_resources = main_target.resources_build_phase.files.select do |bf|\n"
+ + " ref = bf.file_ref\n"
+ + " next false unless ref && ref.path\n"
+ + " dir_path = File.join(project_root, ref.path)\n"
+ + " File.directory?(dir_path) && !Dir.glob(File.join(dir_path, '**', '*.swift')).empty?\n"
+ + " end\n"
+ + " source_folder_resources.each do |bf|\n"
+ + " main_target.resources_build_phase.files.delete(bf)\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 bridgingHeaderFile = new File(new File(tmpDir, "dist"), "cn1-Bridging-Header.h");
+ if (!bridgingHeaderFile.exists()) {
+ this.createFile(bridgingHeaderFile, "// Codename One generated Swift bridging header\n".getBytes(StandardCharsets.UTF_8));
+ }
File hooksDir = new File(tmpFile, "hooks");
hooksDir.mkdir();
File fixSchemesFile = new File(hooksDir, "fix_xcode_schemes.rb");
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateNativeInterfaces.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateNativeInterfaces.java
index 66e2677d9b..1b05e0d000 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateNativeInterfaces.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/GenerateNativeInterfaces.java
@@ -7,6 +7,7 @@
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
import org.objectweb.asm.*;
import java.io.File;
@@ -26,6 +27,11 @@
@Mojo(name="generate-native-interfaces")
@Execute(phase= LifecyclePhase.COMPILE)
public class GenerateNativeInterfaces extends AbstractCN1Mojo {
+ @Parameter(property = "cn1.generateNativeInterfaces.swift", defaultValue = "false")
+ private boolean generateIosSwift;
+
+ @Parameter(property = "cn1.generateNativeInterfaces.kotlin", defaultValue = "false")
+ private boolean generateAndroidKotlin;
@Override
protected void executeImpl() throws MojoExecutionException, MojoFailureException {
@@ -115,7 +121,7 @@ private void generateNativeInterface(String relativePath) throws Exception {
throw new IllegalStateException("Project needs to be compiled first");
}
- StubGenerator g = StubGenerator.create(getLog(), c);
+ StubGenerator g = StubGenerator.create(getLog(), c, generateIosSwift, generateAndroidKotlin);
String s = g.verify();
if (s != null) {
throw new RuntimeException("Generation Failed: " + s);
diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java
index 3b67977378..65e975f4e2 100644
--- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java
+++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/StubGenerator.java
@@ -47,15 +47,24 @@ private StubGenerator() {}
private File csFile;
private File iosHFile;
private File iosMFile;
+ private File iosSwiftFile;
+ private File androidKotlinFile;
private File jsFile;
private Log log;
+ private boolean generateIosSwift;
+ private boolean generateAndroidKotlin;
public static StubGenerator create(Log log, Class nativeInterface) {
+ return create(log, nativeInterface, false, false);
+ }
+ public static StubGenerator create(Log log, Class nativeInterface, boolean generateIosSwift, boolean generateAndroidKotlin) {
StubGenerator instance = new StubGenerator();
instance.log = log;
instance.nativeInterface = nativeInterface;
+ instance.generateIosSwift = generateIosSwift;
+ instance.generateAndroidKotlin = generateAndroidKotlin;
return instance;
}
@@ -142,7 +151,9 @@ private void initFileNames(File destination) {
String iosFilename = nativeInterface.getName().replace('.', '_') + "Impl.";
iosHFile = new File(path(destination.getAbsolutePath(), "ios", "src", "main", "objectivec", iosFilename+"h"));
iosMFile = new File(path(destination.getAbsolutePath(), "ios", "src", "main", "objectivec", iosFilename+"m"));
+ iosSwiftFile = new File(path(destination.getAbsolutePath(), "ios", "src", "main", "objectivec", iosFilename+"swift"));
iosMFile.getParentFile().mkdirs();
+ androidKotlinFile = new File(path(destination.getAbsolutePath(), "android", "src", "main", "java", nativeInterface.getName().replace('.', File.separatorChar) + "Impl.kt"));
jsFile = new File(path(destination.getAbsolutePath(), "javascript", "src", "main", "javascript", nativeInterface.getName().replace('.', '_') + ".js"));
jsFile.getParentFile().mkdirs();
@@ -153,7 +164,7 @@ private void initFileNames(File destination) {
*/
public boolean isFilesExist(File destination) {
initFileNames(destination);
- return androidFile.exists() || iosHFile.exists() || iosMFile.exists() ||
+ return androidFile.exists() || androidKotlinFile.exists() || iosHFile.exists() || iosMFile.exists() || iosSwiftFile.exists() ||
csFile.exists() || javaseFile.exists() || jsFile.exists();
}
@@ -190,6 +201,22 @@ public void generateCode(File destination, boolean overwrite) throws IOException
} else {
log.debug(iosHFile+" already exists. Skipping");
}
+ if(generateIosSwift) {
+ if(overwrite || !iosSwiftFile.exists()) {
+ log.info("Writing " + iosSwiftFile);
+ generateIOSSwiftFile();
+ } else {
+ log.debug(iosSwiftFile + " already exists. Skipping");
+ }
+ }
+ if(generateAndroidKotlin) {
+ if(overwrite || !androidKotlinFile.exists()) {
+ log.info("Writing " + androidKotlinFile);
+ generateAndroidKotlinFile();
+ } else {
+ log.debug(androidKotlinFile + " already exists. Skipping");
+ }
+ }
if(overwrite || !(jsFile.exists())) {
log.info("Writing "+jsFile);
generateJavaScriptFile();
@@ -271,6 +298,119 @@ private void generateIOSFiles() throws IOException {
fo.close();
}
+ private void generateIOSSwiftFile() throws IOException {
+ String className = nativeInterface.getName().replace('.', '_') + "Impl";
+ String swift = "import Foundation\n\n"
+ + "@objc(" + className + ")\n"
+ + "@objcMembers\n"
+ + "public class " + className + ": NSObject {\n";
+ for (Method mtd : nativeInterface.getMethods()) {
+ swift += " public func " + mtd.getName() + "(";
+ Class[] params = mtd.getParameterTypes();
+ if (params != null && params.length > 0) {
+ for (int i = 0; i < params.length; i++) {
+ if (i > 0) {
+ swift += ", ";
+ }
+ swift += "param" + i + ": " + javaTypeToSwiftType(params[i]);
+ }
+ }
+ Class returnType = mtd.getReturnType();
+ if (returnType != Void.TYPE && returnType != Void.class) {
+ swift += ") -> " + javaTypeToSwiftType(returnType) + " {\n";
+ swift += " return " + defaultSwiftReturn(returnType) + "\n";
+ } else {
+ swift += ") {\n";
+ }
+ swift += " }\n\n";
+ }
+ swift += "}\n";
+ try (FileOutputStream fo = new FileOutputStream(iosSwiftFile)) {
+ fo.write(swift.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ private void generateAndroidKotlinFile() throws IOException {
+ String className = nativeInterface.getSimpleName() + "Impl";
+ StringBuilder kotlin = new StringBuilder();
+ kotlin.append("package ").append(nativeInterface.getPackage().getName()).append("\n\n");
+ kotlin.append("class ").append(className).append(" {\n");
+ for (Method mtd : nativeInterface.getMethods()) {
+ kotlin.append(" fun ").append(mtd.getName()).append("(");
+ Class[] params = mtd.getParameterTypes();
+ if (params != null && params.length > 0) {
+ for (int i = 0; i < params.length; i++) {
+ if (i > 0) {
+ kotlin.append(", ");
+ }
+ kotlin.append("param").append(i).append(": ").append(javaTypeToKotlinType(params[i]));
+ }
+ }
+ Class returnType = mtd.getReturnType();
+ if (returnType != Void.TYPE && returnType != Void.class) {
+ kotlin.append("): ").append(javaTypeToKotlinType(returnType)).append(" {\n");
+ kotlin.append(" return ").append(defaultKotlinReturn(returnType)).append("\n");
+ } else {
+ kotlin.append(") {\n");
+ }
+ kotlin.append(" }\n\n");
+ }
+ kotlin.append("}\n");
+ try (FileOutputStream fo = new FileOutputStream(androidKotlinFile)) {
+ fo.write(kotlin.toString().getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ private String javaTypeToSwiftType(Class t) {
+ if (t == String.class) return "String";
+ if (t.isArray()) return "Data?";
+ if (t == Integer.class || t == Integer.TYPE || t == Character.class || t == Character.TYPE) return "Int";
+ if (t == Long.class || t == Long.TYPE) return "Int64";
+ if (t == Byte.class || t == Byte.TYPE || t == Short.class || t == Short.TYPE) return "Int";
+ if (t == Boolean.class || t == Boolean.TYPE) return "Bool";
+ if (t == Float.class || t == Float.TYPE) return "Float";
+ if (t == Double.class || t == Double.TYPE) return "Double";
+ if (t == Void.class || t == Void.TYPE) return "Void";
+ return "Any?";
+ }
+
+ private String defaultSwiftReturn(Class t) {
+ if (t == String.class) return "\"\"";
+ if (t.isArray()) return "nil";
+ if (t == Boolean.class || t == Boolean.TYPE) return "false";
+ if (t == Float.class || t == Float.TYPE || t == Double.class || t == Double.TYPE) return "0";
+ if (t.isPrimitive()) return "0";
+ return "nil";
+ }
+
+ private String javaTypeToKotlinType(Class t) {
+ if (t.getName().equals("com.codename1.ui.PeerComponent")) return "Any?";
+ if (t == String.class) return "String";
+ if (t.isArray()) return "ByteArray?";
+ if (t == Integer.class || t == Integer.TYPE || t == Character.class || t == Character.TYPE) return "Int";
+ if (t == Long.class || t == Long.TYPE) return "Long";
+ if (t == Byte.class || t == Byte.TYPE) return "Byte";
+ if (t == Short.class || t == Short.TYPE) return "Short";
+ if (t == Boolean.class || t == Boolean.TYPE) return "Boolean";
+ if (t == Float.class || t == Float.TYPE) return "Float";
+ if (t == Double.class || t == Double.TYPE) return "Double";
+ if (t == Void.class || t == Void.TYPE) return "Unit";
+ return "Any?";
+ }
+
+ private String defaultKotlinReturn(Class t) {
+ if (t == String.class) return "\"\"";
+ if (t.isArray()) return "null";
+ if (t == Boolean.class || t == Boolean.TYPE) return "false";
+ if (t == Float.class || t == Float.TYPE) return "0f";
+ if (t == Double.class || t == Double.TYPE) return "0.0";
+ if (t == Long.class || t == Long.TYPE) return "0L";
+ if (t == Byte.class || t == Byte.TYPE) return "0";
+ if (t == Short.class || t == Short.TYPE) return "0";
+ if (t == Integer.class || t == Integer.TYPE || t == Character.class || t == Character.TYPE) return "0";
+ return "null";
+ }
+
private String javaTypeToObjectiveCType(Class t) {
if(t == String.class) {
return "NSString*";
@@ -589,4 +729,4 @@ private void generateJavaScriptFile() throws IOException {
fo.write(t.getBytes(StandardCharsets.UTF_8));
fo.close();
}
-}
\ No newline at end of file
+}
diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/StubGeneratorTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/StubGeneratorTest.java
new file mode 100644
index 0000000000..9dd8358b08
--- /dev/null
+++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/StubGeneratorTest.java
@@ -0,0 +1,50 @@
+package com.codename1.maven;
+
+import com.codename1.maven.stubgen.TestNativeInterface;
+import org.apache.maven.plugin.logging.SystemStreamLog;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StubGeneratorTest {
+
+ @Test
+ void optionalSwiftAndKotlinStubsAreGeneratedOnlyWhenEnabled() throws Exception {
+ File tempDir = Files.createTempDirectory("cn1-stubgen").toFile();
+ try {
+ StubGenerator defaultGenerator = StubGenerator.create(new SystemStreamLog(), TestNativeInterface.class, false, false);
+ defaultGenerator.generateCode(tempDir, false);
+ File iosSwift = new File(tempDir, "ios/src/main/objectivec/com_codename1_maven_stubgen_TestNativeInterfaceImpl.swift");
+ File androidKotlin = new File(tempDir, "android/src/main/java/com/codename1/maven/stubgen/TestNativeInterfaceImpl.kt");
+ assertFalse(iosSwift.exists());
+ assertFalse(androidKotlin.exists());
+
+ StubGenerator enabledGenerator = StubGenerator.create(new SystemStreamLog(), TestNativeInterface.class, true, true);
+ enabledGenerator.generateCode(tempDir, false);
+ assertTrue(iosSwift.exists());
+ assertTrue(androidKotlin.exists());
+ } finally {
+ deleteTree(tempDir);
+ }
+ }
+
+ private static void deleteTree(File f) {
+ if (f == null || !f.exists()) {
+ return;
+ }
+ if (f.isDirectory()) {
+ File[] children = f.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteTree(child);
+ }
+ }
+ }
+ //noinspection ResultOfMethodCallIgnored
+ f.delete();
+ }
+}
diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/stubgen/TestNativeInterface.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/stubgen/TestNativeInterface.java
new file mode 100644
index 0000000000..1fd87220d1
--- /dev/null
+++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/stubgen/TestNativeInterface.java
@@ -0,0 +1,7 @@
+package com.codename1.maven.stubgen;
+
+import com.codename1.system.NativeInterface;
+
+public interface TestNativeInterface extends NativeInterface {
+ String hello(String name);
+}
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..fa4e74d647
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/NativeInterfaceLanguageValidator.java
@@ -0,0 +1,54 @@
+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);
+ }
+ if (isIos && !"ios-swift-native-impl".equals(diagnostics)) {
+ lastStatus = "SWIFT_BRIDGE_MISSING diagnostics=" + diagnostics;
+ throw new IllegalStateException("Swift implementation bridge not confirmed on iOS. 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..962706ca43 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,13 @@ 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()
+ // Keep running so DeviceRunner can emit CN1SS markers and report swift_diag_status explicitly.
+ }
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..2b4ac740e9
--- /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";
+}
+
+-(NSString*)diagnostics {
+ id bridge = [self getBridgeInstance];
+ if (bridge != nil && [bridge respondsToSelector:@selector(diagnostics)]) {
+ return [bridge diagnostics];
+ }
+ return @"ios-swift-bridge-missing-using-objc-shim";
+}
+
+-(BOOL)isSupported {
+ id bridge = [self getBridgeInstance];
+ if (bridge != nil && [bridge respondsToSelector:@selector(isSupported)]) {
+ return [bridge isSupported];
+ }
+ return YES;
+}
+
+@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