Swift in Android Apps

Posted on

The inability to share code between the iOS and Android ecosystems is a known issue and as it’s unlikely Java will be making an appearance on iOS any time soon (even if Kotlin could!), the focus has to be whether Swift can be used in Android apps.

Shortly after Swift was open-sourced, Zhuowei Zhang sent an email to the swift-dev mailing list, introducing a fork of Swift that was able to produce Android binaries. Brian Gesiak worked this into a pull request which was merged in Feb 2016. Zhuowei also published a proof of concept applicationthe best part of which was that it used the Swift Package Manager for the build so you could bring in other/Open Source files via git like a conventional project. It wasn’t long before Geordie J announced they had been able to publish an app containing Swift to Google Play using this one simple trick. Ports of Dispatch and Foundation followed in short order.

With Swift 4 on the horizon momentum has picked up again using Gonzalo Larralde’s swifty robot environment that can be used to build a swift toolchain for Android in a Docker container. This, and a couple of recent PRsmeans a viable environment exists that opens the possibility that model, business logic and network code can be shared between Android and iOS when it is written in Swift.

Beta Swift Android Toolchain

Bringing these contributions together a Toolchain has been prepared for Android which you can download from this link.

The toolchain will allow you develop Apps on macOS or Ubuntu Linux 16.04 provided you run the script swift-install/setup.sh afater extracting the archive. This links from your existing toolchain into the release tree and installs a basic gradle plugin to hook into the build process. To add a swift build to your Android studio project, add a classpath dependency for net.zhuoweizhang:swiftandroid:1.0.0 to it’s build.gradle and apply it.

These ideas are all brought together in a simple example app. Some generic JNI supprt code for the Swift side is taken from the java_swift project which is included in the project’s Package.swift for cloning. A feature of the toolchain’s gradle plugin is that it includes the code generator genswift.javaand, if the package name of your bindings file contains the path “swiftbindings”, all JNI code generation will be performed automatically as part of the build process when required.

In the examples, the app binding sources are contained in the directory:
src/main/java/com/johnholdsworth/swiftbindings/*.java

The generated Swift with the JNI required will be placed in:
src/main/swift/Sources.

Some supporting Java proxy classes for messaging from Java to Swift will also be generated in:
src/main/java/org/swiftjava/com_johnholdsworth/*Proxy.java

The result is an application & build process something like the following:

The toolchain could be described at being at a fairly advanced stage of Beta with a view to a release when Swift 4 firms up. If you have any problems with the toolchain, please file an issue with the repo set up for this purpose, or you can contact the authors by email. Announcements of major releases will be made on twitter @Injection4Xcode.

Code Generator — genswift.java

The only way for native apps to communicate to Java, the Java Native Interface, is renowned for requiring acres of inadequately checked boilerplate code. To avoid this, a code generator script has been written which takes this off the developer’s hands and introduces strong type safety. The script takes as input a set of Java sources (the “bindings”) specifying the interfaces and types that bind the two parts of an application together. Using this generated code, primitives, objects, collections can be passed from Java to Swift and messaged. Conversely, Swift objects and structs and collections can be passed back to Java if they conform to one of the protocols declared in the bindings and the protocol name ends in “Listener”.

The following type mappings are available from the generator as arguments or return values out of the box:

Java TypeSwift Typeboolean, byte, char, short, int, long, float, double, String<−>Bool, Int8, UInt16, Int16, Int, Int64, Float, Double, Stringboolean[], byte[], char[], short[], int[], long[], float[], double[], String[]<->[Bool], [Int8], [UInt16], [Int16], [Int32], [Int64], [Float], [Double], [String]boolean[][], byte[][], char[][], short[][], int[][], long[][], float[][], double[][], String[][]<->[[Bool]], [[Int8]], [[UInt16]], [[Int16]], [[Int32]], [[Int64]], [[Float]], [[Double]], [[String]]Where the enum, class of object or interface are declared in the bindings file:enum, enum[], enum[][]<->enum, [enum], [[enum]]object, object[], object[][]<->object, [object], [[object]]interface, interface[], interface[][]<->protocol, [protocol], [[protocol]]Map types require a static valueClass()method declared on a concrete typeMap<String,String>, Map<String,[String]]><->[String:String], [String:[String]]Map<String,object>, Map<String,[object]]><->[String:object], [String:[object]]Map<String,protocol>, Map<String,[protocol]]><->[String:protocol], [String:[protocol]]

By convention, all non-primitive arguments are optional and the return type for functions returning a non-primitive is an implicitly unwrapped optional. A function that throws can throw an Exception object from Swift back to Java and vice versa.

A typical bindings file linking the two sides of the two example applications looks like this:

package com.johnholdsworth.swiftbindings;

import com.johnholdsworth.swiftbindings.SwiftHelloTypes.TextListener;

import com.johnholdsworth.swiftbindings.SwiftHelloTypes.ListenerMap;

import com.johnholdsworth.swiftbindings.SwiftHelloTypes.ListenerMapList;

public interface SwiftHelloBinding {

// Messages from JavaActivity to Swift

public interface Listener {

public void setCacheDir( String cacheDir );

public void processNumber( double number );

public void processText( String text );

public void processedMap( ListenerMap map );

public void processedMapList( ListenerMapList map );

public void processStringMap( StringMap map );

public void processStringMapList( StringMapList map );

public double throwException() throws Exception;

public SwiftHelloTest.TestListener testResponder( int loopback );

}

// Messages from Swift back to Activity

public interface Responder {

public void processedNumber( double number );

public void processedText( String text );

public void processedTextListener( TextListener text );

public void processedTextListenerArray( TextListener text[] );

public void processedTextListener2dArray( TextListener text[][] );

public void processMap( ListenerMap map );

public void processMapList( ListenerMapList map );

public void processedStringMap( StringMap map );

public void processedStringMapList( StringMapList map );

public double throwException() throws Exception;

public String[] debug( String msg );

public SwiftHelloTest.TestListener testResponder( int loopback );

}

}

The types can be separated out into their own file:

// Shared types/interfaces between Java and Swift

package com.johnholdsworth.swiftbindings;

import java.util.HashMap;

public interface SwiftHelloTypes {

// An example of publishing an object to Java.

// Add the associated protocol to an class and

// objects can be passed to a responder message.

public interface TextListener {

public String getText();

}

// These are required because of type erasure in Java jars

public static class ListenerMap extends HashMap<String,TextListener> {

public static Class<?> valueClass() {

return TextListener.class;

}

}

public static class ListenerMapList extends HashMap<String,TextListener[]> {

public static Class<?> valueClass() {

return (new TextListener [] {}).getClass();

}

}

public static class StringMap extends HashMap<String,String> {

public static Class<?> valueClass() {

return String.class;

}

public StringMap() {

super();

}

@SuppressWarnings(“unchecked”)

public StringMap(Map map) {

super(map);

}

}

public static class StringMapList extends HashMap<String,String[]> {

public static Class<?> valueClass() {

return (new String [] {}).getClass();

}

public StringMapList() {

super();

}

@SuppressWarnings(“unchecked”)

public StringMapList(Map map) {

super(map);

}

}

}

Finally, to tie the two sides of the application together there is one JNI entry point you need to provide in Swift to bootstrap the link between the two languages. This called as a native method from Java when the application starts.

// link back to Java side of Application

var responder: SwiftHelloBinding_ResponderForward!

// one-off call to bind the Java and Swift sections of app

@_silgen_name(“Java_net_zhuoweizhang_swifthello_SwiftHello_bind”)

public func bind_samples( __env: UnsafeMutablePointer<JNIEnv?>, __this: jobject?, __self: jobject? )-> jobject? {

// This Swift instance forwards to Java through JNI

responder = SwiftHelloBinding_ResponderForward( javaObject: __self )

// This Swift instance receives native calls from Java

var locals = [jobject]()

return SwiftListenerImpl().localJavaObject( &locals )

}

From here it’s a case of writing Java or Kotlin code against the binding interfaces as you would normally and Swift code against the generated types which will have a name like SwiftHelloTypes_TextListener. You can see how it looks from the Android side in Java & Kotlin. The corresponding Swift implementation can be viewed here.