The document discusses writing Kotlin multiplatform libraries that are compatible with iOS. It begins with a disclaimer from the presenter. It then provides biographical information about the presenter and introduces the topic of writing Kotlin libraries that can be used from Swift on iOS. The presentation format will cover naming clashes between Kotlin and Swift, disappearing types when mapping Kotlin generics to Swift, and other interoperability challenges. It provides examples and solutions for overcoming these challenges to enable sharing code between Kotlin and Swift.
2. Disclaimer
My presentation, comments and
opinions are provided in my personal
capacity and not as a representative
of Walmart. They do not reflect the
views of Walmart and are not
endorsed by Walmart.
10. Presentation Format
//KOTLIN API
data class Person(
val name: String,
val age: Int
)
//EXPORTED OBJ-C HEADER
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Person")))
@interface SharedPerson : SharedBase
- (instancetype)initWithName:(NSString *)name age:(int32_t)age
__attribute__((swift_name("init(name:age:)")))
__attribute__((objc_designated_initializer));
- (SharedPerson *)doCopyName:(NSString *)name age:(int32_t)age
__attribute__((swift_name("doCopy(name:age:)")));
- (BOOL)isEqual:(id _Nullable)other
__attribute__((swift_name("isEqual(_:)")));
- (NSUInteger)hash __attribute__((swift_name("hash()")));
- (NSString *)description
__attribute__((swift_name("description()")));
@property (readonly) int32_t age
__attribute__((swift_name("age")));
@property (readonly) NSString *name
__attribute__((swift_name("name")));
@end
// SWIFT CLIENT CODE
let keanuReeves = Person(name: "Keanu Charles Reeves", age: 59)
[OBJ-C]
11. Presentation Format
//KOTLIN API
data class Person(
val name: String,
val age: Int
)
//HEADER “TRANSLATED” TO SWIFT
public class Person : KotlinBase {
public init(name: String, age: Int32)
open func doCopy(name: String, age:
Int32) -> Person
open func isEqual(_ other: Any?) ->
Bool
open func hash() -> UInt
open func description() -> String
open var age: Int32 { get }
open var name: String { get }
}
// Which one is which?
let keanuReeves = Person(name: "Keanu Charles Reeves", age: 59)
13. Name Clashing
THE
PROBLEM
• Kotlin
• Multiple levels of namespace due to packages;
• Method signature is the method name plus parameter types.
• Objective-C
• No support to namespaces;
• Method signature is the method name plus parameter names.
• Swift
• One level of namespace: the exported framework;
• Method signature is the method name plus parameter names.
14. Classes with same name but different packages
package io.aoriani.network
class Item
...
package io.aoriani.models
class Item
public class Item : KotlinBase {
public init()
}
public class Item_ : KotlinBase {
public init()
}
// Which one is which?
let item1 = Item()
let item2 = Item_()
THE
PROBLEM
15. Classes with same name but different packages
package io.aoriani.network
@ObjCName("ItemResponse")
class Item
...
package io.aoriani.models
@ObjCName("ItemModel")
class Item
public class ItemResponse :
KotlinBase {
public init()
}
public class ItemModel : KotlinBase {
public init()
}
let itemDomainModel = ItemModel()
let itemNetworkResponse = ItemResponse()
THE
SOLUTION
16. Overloading with params named the same
fun sort(data: List<Int>){}
fun sort(data: Map<String, Int>){}
public class ExampleKt : KotlinBase {
open class func sort(data: [KotlinInt])
open class func sort(data_ data:
[String : KotlinInt])
}
ExampleKt.sort(data: [1, 2, 3])
ExampleKt.sort(data_: ["A" : 1, "B": 2])
THE
PROBLEM
17. Overloading with params named the same
fun sort(listOfInts: List<Int>){}
fun sort(mapOfStringToInt: Map<String,
Int>){}
public class ExampleKt : KotlinBase {
open class func sort(listOfInts:
[KotlinInt])
open class func sort(mapOfStringToInt:
[String : KotlinInt])
}
ExampleKt.sort(listOfInts: [1, 2, 3])
ExampleKt.sort(mapOfStringToInt: ["A" : 1, "B": 2])
THE
SOLUTION
18. Interface fields, same name, different types
interface Person {
val id: String
}
interface Robot {
val id: Long
}
public protocol Person {
var id: String { get }
}
public protocol Robot {
var id_: Int64 { get }
}
// Only possible because Konan renamed field for Robot
class Android: Person, Robot {
let id: String = ""
let id_: Int64 = 0
}
THE
PROBLEM
20. Collection with types conforming to protocol
interface Product
class Consumer {
fun consume(products: Set<Product>) {}
}
public protocol Product {}
public class Consumer : KotlinBase {
public init()
open func consume(products:
Set<AnyHashable>)
}
// Compiles fine in Swift
let consumer = Consumer()
let fakeProducts: Set<Int> = [1, 2, 3]
consumer.consume(products: fakeProducts)
THE
PROBLEM
21. Collection with types conforming to protocol
THE
PROBLEM
Set<Product> NSSet<id<Product>>
Set<T>
where T: Hashable
[OBJ-C]
MAPS TO BRIDGES TO
• Swift Set requires elements conforming to protocol Hashable;
• Kotlin Interfaces are exported as Objetive-C protocols;
• Objective-C protocols cannot conform to other protocols, let alone Swift Protocols, thus the
argument type is erased to AnyHashable;
• All KMP Classes inherit from NSObject which conforms to Hashable;
22. Collection with types conforming to protocol
abstract class Product
class Consumer {
fun consume(products: Set<Product>) {}
}
open class Product : KotlinBase {
public init()
}
public class Consumer : KotlinBase {
public init()
open func consume(products:
Set<Product>)
}
let consumer = Consumer()
let fakeProducts: Set<Int> = [1, 2, 3]
// Error: Cannot convert value of type 'Set<Int>' to expected
argument type 'Set<Product>'
//consumer.consume(products: fakeProducts)
let realProducts: Set<Product> = [Product(), Product()]
consumer.consume(products: realProducts)
THE
SOLUTION
23. Values classes
@JvmInline
value class PostalCode(private val code:
String)
class Letter(val zipcode: PostalCode)
public class Letter : KotlinBase {
public init(zipcode: Any)
open var zipcode: Any { get }
}
//This compiles fine
let letter = Letter(postalCode: UIViewController())
THE
PROBLEM
25. Interface extensions
interface Person {
val firstName: String
val lastName: String
}
fun Person.fullName() = "$firstName
$lastName"
public protocol Person {
var firstName: String { get }
var lastName: String { get }
}
public class ExampleKt : KotlinBase {
open class func fullName(_ receiver:
Person) -> String
}
class SwiftPerson: Person {…}
let swiftPerson = SwiftPerson(firstName: "Bruce", lastName: "Wayne")
// Java déjà vu?
print(ExampleKt.fullName(swiftPerson))
THE
PROBLEM
26. Class extensions
abstract class Person(
val firstName: String,
val lastName: String
)
fun Person.fullName() = "$firstName
$lastName"
open class Person : KotlinBase {
public init(firstName: String,
lastName: String)
open var firstName: String { get }
open var lastName: String { get }
}
extension Person {
open func fullName() -> String
}
class SwiftPerson: Person {…}
let swiftPerson = SwiftPerson(firstName: "Bruce", lastName: "Wayne")
print(swiftPerson.fullName())
THE
SOLUTION
27. Filling the gaps between Kotlin and Swift
class MyRange {
fun isInRange(value: Int, range: IntRange) =
value in range
}
fun test(){
MyRange().isInRange(1, -7..6)
}
public class MyRange : KotlinBase {
public init()
open func isInRange(value: Int32,
range: KotlinIntRange) -> Bool
}
let myRange = MyRange()
// This is a bit clumsy
myRange.isInRange(value: 1, range: KotlinIntRange(start: -1,
endInclusive: 2))
THE
PROBLEM
28. Filling the gaps between Kotlin and Swift
class MyRange {
@ShouldRefineInSwift
fun isInRange(value: Int, range: IntRange) =
value in range
}
public class MyRange : KotlinBase {
public init()
}
let myRange = MyRange()
myRange.isRange(value: 2, range: -1...2)
extension MyRange {
func isInRange(value value: Int32,
range range: ClosedRange<Int32>) -> Bool {
return __is(inRangeValue: value,
range: KotlinIntRange(start:
range.lowerBound, endInclusive:
range.lowerBound))
}
}
THE
SOLUTION
29. Providing only platform specific code
import io.ktor.http.Url
import java.net.URL
fun Url.toURL(): URL =
URL(this.toString())
let url = Service.shared.baseUrl.toURL()
let session = URLSession(configuration: .default)
session.dataTask(with: url) {
...
extension Ktor_httpUrl {
open func toURL() -> URL
}
import io.ktor.http.Url
import platform.Foundation.NSURL
fun Url.toURL(): NSURL =
NSURL(string = toString())
androidMain
iosMain
THE
SOLUTION
30. Default arguments
fun compareString(a: String, b: String,
ignoreCase: Boolean = false): Boolean {
return a.equals(b, ignoreCase =
ignoreCase)
}
public class ExampleKt : KotlinBase {
open class func compareString(a:
String, b: String, ignoreCase: Bool) ->
Bool
}
let result1 = ExampleKt.compareString(a: "HELLO", b: "hello", ignoreCase:
true)
// Compile error: Missing argument for parameter 'ignoreCase' in call
let result2 = ExampleKt.compareString(a: "Roma", b: "Londres")
THE
PROBLEM
32. Exceptions
class Bomb {
fun explode() : Unit =
throw IOException(”Bomb has detonated")
}
public class Bomb : KotlinBase {
public init()
open func explode()
}
func defuseBomb() {
let bomb = Bomb()
do {
bomb.explode()
} catch {
// 'catch' block is unreachable because no errors are thrown in 'do'
block
print("Bomb disarmed!")
}
}
THE
PROBLEM
33. Function doesn't have or inherit @Throws annotation and thus exception isn't propagated
from Kotlin to Objective-C/Swift as NSError.
It is considered unexpected and unhandled instead. Program will be terminated.
Uncaught Kotlin exception: io.ktor.utils.io.errors.IOException: Bomb has detonated
at
0 shared 0x103e6ec95 kfun:kotlin.Exception#<init>(kotlin.String?;kotlin.Throwable?){} + 133
(/opt/buildAgent/work/f43969c6214a19e7/kotlin/kotlin-native/runtime/src/main/kotlin/kotlin/Exceptions.kt:25:63)
at
1 shared 0x1040c6de7 kfun:io.ktor.utils.io.errors.IOException#<init>(kotlin.String;kotlin.T
hrowable?){} + 119 (/opt/buildAgent/work/8d547b974a7be21f/ktor-io/posix/src/io/ktor/utils/io/errors/IOException.kt:4:58)
at
2 shared 0x1040c6e4d kfun:io.ktor.utils.io.errors.IOException#<init>(kotlin.String){} + 93
(/opt/buildAgent/work/8d547b974a7be21f/ktor-io/posix/src/io/ktor/utils/io/errors/IOException.kt:5:50)
at 3 shared 0x103dc6591 kfun:io.aoriani.kmpapp.Bomb#explode(){} + 145
at 4 shared 0x103dc8c54 objc2kotlin_kfun:io.aoriani.kmpapp.Bomb#explode(){} + 148
at 5 iosApp 0x1027a6c59 $s6iosApp10defuseBombyyF + 57
(/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/ContentView.swift:33:14)
at 6 iosApp 0x1027a6f5d $s6iosApp11ContentViewVACycfC + 141
(/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/ContentView.swift:10:0)
at 7 iosApp 0x1027a67eb $s6iosApp6iOSAppV4bodyQrvgAA11ContentViewVyXEfU_ + 27
(/Users/aoriani/Development/Multiplatform/KmpApp/iosApp/iosApp/iOSApp.swift:7:4)
at 8 SwiftUI 0x105435a01 get_witness_table
7SwiftUI4ViewRzlAA15ModifiedContentVyxAA25ComplicationIdiomModifierVGAaBHPxAaBHD1__AfA0cH0HPyHCHCTm + 60924
at 9 iosApp 0x1027a66cc $s6iosApp6iOSAppV4bodyQrvg + 156 (/
CRASH!
34. Exceptions
class Bomb {
@Throws(IOException::class)
fun explode() : Unit =
throw IOException("Error")
}
public class Bomb : KotlinBase {
public init()
open func explode() throws
}
func defuseBomb() {
let bomb = Bomb()
do {
try bomb.explode()
} catch let error as NSError {
switch error.kotlinException
{
case let ioe as
Ktor_ioIOException:
print("Bomb defused:
(ioe.message ?? "")")
default:
print("Sorry could not
defuse!")
}
}
}
- (BOOL)explodeAndReturnError:(NSError
* _Nullable * _Nullable)error
__attribute__((swift_name(”explode()"))
);
[OBJ-C]
Konan
Xcode Bridging
THE
SOLUTION
36. Kotlin Enums vs Swift Enums
enum class Result {
LOADING,
SUCCESS,
FAILURE
}
public class Result : KotlinEnum<Result> {
open class var loading: Result { get }
open class var success: Result { get }
open class var failure: Result { get }
open class func values() ->
KotlinArray<Result>
open class var entries: [Result] { get
}
}
func testEnum() {
let result: Result = .failure
switch result {
case .loading: print("Loading")
case .success: print("Success")
case .failure: print("Failed")
default: print("I will never be called")
}
}
THE
PROBLEM
37. Kotlin Sealed Class vs Swift Enums
sealed class Result {
data object Loading: Result()
class Success(val value: String): Result()
class Failure(val throwable: Throwable): Result()
}
open class Result : KotlinBase {}
extension Result {
public class Loading : Result {
public convenience init()
open class var shared: Result.Loading {
get }
open func isEqual(_ other: Any?) -> Bool
open func hash() -> UInt
open func description() -> String
}
public class Failure : Result {
public init(throwable: KotlinThrowable)
open var throwable: KotlinThrowable { get
}
}
public class Success : Result {
public init(value: String)
open var value: String { get }
}
}
func testSealedClass() {
let result: Result =
Result.Loading.shared
switch result {
case is Result.Loading:
print("loading")
case let success as Result.Success:
print("Success: (success.value)")
case let failure as Result.Failure:
print("Failure:
(failure.throwable.message ?? "")")
default: print("I will never be called")
}
}
THE
SOLUTION
38. Kotlin Sealed Class vs Swift Enums
func testWrappedSealedClass() {
if let result =
SwiftResult(Result.Loading.shared) {
switch result {
case .loading:
print("Loading")
case let .success(value):
print("Success: (value)")
case let .failure(throwable):
print("Failure:
(throwable.message ?? "")")
}
}
}
enum SwiftResult {
case loading
case success(value: String)
case failure(throwable: KotlinThrowable)
}
extension SwiftResult {
init?(_ result: Result) {
switch result {
case is Result.Loading:
self = .loading
case let success as Result.Success:
self = .success(value:
success.value)
case let failure as Result.Failure:
self = .failure(throwable:
failure.throwable)
default: return nil
}
}
}
Libraries that can create the wrapper
for you:
• MOKO KSwift
• Touchlab SKIE
THE
SOLUTION
40. Generics
@property (nonatomic,strong) NSArray *arrayOfUrls;
@property (nonatomic,strong) NSDictionary *myDictNameToAge;
@property (nonatomic,strong) NSSet *setOfStrings;
open var arrayOfUrls: [Any]
open var myDictNameToAge: [AnyHashable : Any]
open var setOfStrings: Set<AnyHashable>
@property (nonatomic,strong) NSArray<NSURL *> *arrayOfUrls;
@property (nonatomic,strong) NSDictionary<NSString *, NSNumber *> *myDictNameToAge;
@property (nonatomic,strong) NSSet<NSString *> *setOfStrings;
open var arrayOfUrls: [URL]
open var myDictNameToAge: [String : NSNumber]
open var setOfStrings: Set<String>
[OBJ-C]
[OBJ-C]
Before Lightweight Generics
After Lightweight Generics
42. Generics : Classes
class Dog<T>(val data: T)
// Bounded Type Parameter
class Bird<T: Number>(val data: T)
public class Dog<T> : KotlinBase
where T : AnyObject {
public init(data: T?)
open var data: T? { get }
}
//Bounded Typed Parameter
public class Bird<T> : KotlinBase
where T : AnyObject {
public init(data: T)
open var data: T { get }
}
43. Generics : Methods & Functions
interface MyInterface
class MethodTest {
fun <T: Number> eat(value: T): Int =
value.toInt()
fun <T: MyInterface> drink(value: T) = a
}
public protocol MyInterface {}
public class MethodTest : KotlinBase {
public init()
open func eat(value: Any) -> Int32
open func drink(value: MyInterface) ->
MyInterface
}
45. Coroutines
class SuspendFunctions {
suspend fun generateInteger(): Int {
delay(1_000)
return 1
}
}
public class SuspendFunctions :
KotlinBase {
public init()
open func
generateInteger(completionHandler:
@escaping (KotlinInt?, Error?) -> Void)
// After Swift 5.5 we also have
open func generateInteger() async
throws -> KotlinInt
}
func usingTask() {
let task = Task { @MainActor in
do {
let object = SuspendFunctions()
let result = try await
object.generateInteger()
print("Result: (result)")
} catch {
print("We had an error:
(error)")
}
}
// This doesn't do what do you think it
would do
task.cancel()
}
THE
PROBLEM
46. Flows
class Flows {
fun generateIntegers(): Flow<Int> {
return flowOf(1,2,3)
}
}
public class Flows : KotlinBase {
public init()
open func genenerateIntegers() ->
Kotlinx_coroutines_coreFlow
}
THE
PROBLEM
48. KMP-NativeCouroutines: Coroutine
import KMPNativeCoroutinesAsync
func usingNativeSuspend() {
let task = Task {
let object = CoroutinesAndFlows()
do {
let v = try await asyncFunction(for: object.randomInt())
} catch {
print("We met an error: (error)")
}
}
//Cancels Swift task and Kotlin Job
task.cancel()
}
THE
SOLUTION
49. KMP-NativeCouroutines: Flow
import Combine
import KMPNativeCoroutinesCombine
func usingNativeFlow() {
let object = CoroutinesAndFlows()
let publisher = createPublisher(for: object.flowOfInt(value: 3))
let cancellable = publisher.sink { completion in
switch completion {
case .finished:
print("Flow has finished")
case let .failure(err):
print("Flow terminated with error (err)")
}
} receiveValue: { value in
print("Received (value)")
}
// Cancels Kotlin Flow and Combine Publisher
cancellable.cancel()
}
THE
SOLUTION
50. And if you found everything too complicated…
Just use Compose Multiplatform!😁
51. Acknowledgement
• Thomas Hultgren
• Daniel Youn
• Paolo Rotolo
• John O’Reilly
• Pamela Hill
• Better Programming
• Towards Devs
• ProAndroidDev
• Android Weekly
• Kotlin Weekly
DisclaimerMy presentation, comments and opinions are provided in my personal capacity and not as a representative of Walmart. They do not reflect the views of Walmart and are not endorsed by Walmart.
Hi, I am André OrianiSono italo-brasiliano perciò questo accento un po’ diverso. I work with Android since their very first public releases, because at Motorola at that time, so I worked on the development of their very first Android devices.Currently I am a tech lead for Walmart in California.
If there any slide you want to take a screenshot, this is the one. This whole talk is based on series of articles I wrote about KMP and Swift, so all the content for this talk is there.
Kotlin multiplatform is awesome!Who here has ever written an iOS app?Now with some knowledge of Compose,Ktor, and some other KMP libraries, you can an iOS app, an Web app and even a Desktop app from the same code with little to no knowledge of those platforms.
However In order to accomplish our secret goal of complete domination of the world of mobile development we need to bring the iOS developers to our side. And how can we do that? By writing Kotling Multiplatform APIs that are so good that they won’t even notice that they were written in Kotlin in the first place. And that is the whole topic of this presentation.
But you can ask me: “André, Kotlin and Swift look so similar… What is that so hard?
Well, If you ever visit San Francisco and cross the Golden Gate Bridge you gonna have to pay the toll. And The toll to cross the bridge between Kotlin and Swift is called Objective-C.
The Kotlin API you wrote is compiled by Konan, the Kotlin Native Compiler, and exported as an Objective-C Framework. Then Xcode bridges the Objective-C framework to Swift. Pay attention, Xcode is the the one that exposes your Kotlin API to Swift by doing the bridging work. That is were the problem resides. You can have features in Kotlin that are similar to Features in Swift, but because they are missing in Objective, they are lost in translation. But why Objective-C? Many reasons, One is that Swift Binary interface only became stable quite recently. Another is similar to using Java over Kotlin. It makes your API available to a wider audience.
So this is going to be the format for this presentation.
First I am gonna propose a Kotlin API
Then I show how that API is exported to Objective-C
And finally I am gonna show you some Swift client code using the API.But as you can see the Objective-C code can be quite verbose and hard to read, so I am gonna be providing the translated version to Swift
Much better, right ?So pay attention to the icons: Kotlin icon for KMP API code
Swift orange for the exported APIAnd SwiftUI blue for the swift client code.
The first problem we will deal with is Name clashing. So whenever you see extra underscore in your exported API is because some name clashing happened.
Why do those clashes happen ? First, whereas Kotlin has multiple levels of namespace due to the ability of nesting of packages, Swift has only one level-– the exported framework– and Objective has not support to namespaces at all. So all your KMP classes will be thrown in a single namespace.Second, whereas the method signature in Kotlin is the method name plus the parameter types, both Swift and Objective use the method name plus the parameter names. So if you have overloaded functions with the same parameter names they will clash
Here we have two classes with the same name, Item, but they belong to different packages, network and models. In Swift, to avoid the clash
one of them gets an underscore, but which one ? Is item1 in the example a network object or a model one.
To address such situation we can use the ObjCName annotation which allows us to specify what will be the symbol’s name in Objective-C and Swift. So now it is clear to Swift developers which class is the network model and which one is the domain model.
In this case we have an overloaded function sort but both versions use the same parameter name, data. That is okay in Kotlin because the parameter types are distinct. But it is a problem in Swift, because as I said before the parameter names are used for the method signature so they will clash. Konan, the kotlin native compiler, will avoid the clash by adding an underscore again.
There are two ways to solve this problem: Either you can use the ObjcName annotation again…
or you can just use better names for the parameters.
This case is a bit curious. We have two interfaces, Person and Robot, and both have the field id. But id is a string for Person and a long for Robot. In Kotlin would be impossible to have an Android class that implement both interfaces. The Kotlin JVM compiler would complain about the id fields having same name but conflicting types. However Konan tries to be proactive here and adds an underscore, nut to which one of them? I don’t know, it is unpredictable. If you are the owner of the interfaces you can use the ObjCName annotation or renamed one of the id fields
Now let’s talk about disappearing types. Here, I am goona covering cases here in which Swift will use a complete different type from the one you defined in your Kotlin API
Here we defined an interface Product and a class Consumer that has a method consume that take a Set of Products. In Swift , our interface Product becomes the protocol Product. Our class Consumer is still a class, but the consume method now takes a set of AnyHashables… Where did those AnyHashable come from ? And if you look at the example you can define an set of integers and use with the consume method, because integers are, of course, Hashable. It will compile fine, but it won’t work at runtime.
So what is going on here ?
So when you define a Set in your Kotlin API, it will be mapped to an Objective-C NSets. Objective-C NSSets are bridged to Swift Sets.
But a swift Set requires its elements to conform to the protocol Hashable, a swift protocol.
The problem is that Kotlin Interfaces are exported as Objetive-C protocols.. And an Objective-C protocol cannot be extend to conform to other protocols, let alone Swift Protocols. Therefore our product interface cannot conform to Hashable protocol. Consequently the argument type is erased to AnyHashable.
Similar thing will happen with Maps. They will be mapped to NSDictionaries, which will be bridged to Swift Dictionaries. Which in turn will require the type for the key to conform to Hashable.On the other hand All KMP Classes inherit indirectly from KotlinBase which inherits from NSObject. And NSObject which conforms to Hashable; After all all objects in Kotlin have the hashcode method.
Therefore the solution is to use abstract classes. Well, Kotlin interfaces kinda behave like abstract classes with everything abstract. Swift protocols are a bit different because other things beyond classes like swift structs can conform to protocols. So now Product is class in Kotlin and Swift and the consume method now takes a set of Products. And the client can only pass a set of products.
Okay, here we defined an inline class PostalCode, zip code if you are American, and a class letter that takes the Postalcod . But then when you look to the swift interface postalCode has now type Any , which is Swift is really anything, objects, structs, primitive types and what not. You can see on the example that I can even pass an UIViewControler to Letter. In better cases Kotlin will replace it with the boxed type, in this case a string, but the general advice is to avoid inline classes.
Providing convenience. In this section I will talk about case in which the exported API is still usable by Swift developers but it could be improved to become more intuitive to them.
Here we define an interface Person and define the extension fullName. Person is exported as a protocol and when we look to the code generated for the extension it remembers when you use a Kotlin extension from Java, it is, it becomes this static method whose first parameter is a receiver, the interface you are extendingThe problem here is that Objective-C protocols cannot have extensions. so Konan has to generate that is very similar to what we see from Java.
And if you paid attention to a couple slides ago, you know the solution will be to use a class. And in fact Kotlin extension for classes are exported as extension to Objective-C Classes, so everything works as expected. So we are seeing a trend here, that Objective-C protocols don’t behave the same way we expect Kotlin interfaced to do. But don’t take it bad, Protocols are an essential part of Swift programming.
Both Kotlin have Ranges, but Objective doesn’t, so as I said ranges are lost in translation. So when you API has ranges it is pretty clumsy, akward for swift developers to use it. They have to instance this KotlinInRange object. Wouldn’t be better if they could use ranges as well ?
The trick here is to use the annotation ShouldRefineInSwift. Your method will still be available in Objective-C, but it hidden from Swift because two underscore were prepended to its name. You can see on the first panel on the right that the isInRange method has disappeared in Swift
So now we can write a Swift extension that accepts swift ranges. The extension wraps the original method and converts Swift Ranges to Kotlin ranges.
So that is the general strategy to fix a gap in the translation that was created by objective-c. We create a swift extension to cover that gap.
Another thing you can is that you don’t need to write everything in common. We can have code that is specific to one platform. For instance here we have an extension to the Ktor’s URL class that converts it to a native URL type. So in the Android Main it converts to a Java net URL. In the iOSMain sourceset it converts to NSURL in Objective, and thanks to Xcode, URL in Swift, that can be used in iOS
Well, I have bad news for you. Default argument in functions won’t work because, guess what ? Because Objective-C doesn’t support them again. So they are also lost in translation. There’s a proposal for having an annotation similar to JVMOverloads, that would generate all the overloads to you, but until that is a reality, you will have to code the overloads yourself. So you see in the example that if I don’t supply the ignoreCase argument it will not compile
Ah, exceptions. Exceptions are curios case . Remember that in Java we have checked and unchecked exceptions. If it is a checked exception, either you must catch or you warn you caller that you may throw an exception thru a throw clause. Kotlin went away with checked exceptions. But Swift still have them, so that leads to interesting results…
Here we have a class bomb with a method that will certainly throw an exception. Our swift developer tries to be very careful to defuse such bomb, it has this nice do-catch-all block around out problematic method. But….
Boom…. I crashes anyway, despite the efforts of the swift developer And the fault was all ours as it is made pretty clear from the error message
Function doesn’t have or inherit @Throw annotation and this exception isn't prorogated from Kotlin to Objective-C Swift as NSError.
So let’s add the throw annotation and see what that message exactly means.
So I added the Throw annotation to warn that our method may throw an IOException. This time I must do different, I need to show you the exported Objetive-C code. We see that Konan completely changed the method signature. The method is now named explodeAndReturnError and it takes a pointer to pointer to a NSError Object. That is the convention followed by the Cocoa libraries, libraries like Foundation and UIKit that are used for iOS Apps. The conventions is that any method that can generator errors will have a NSError pointer as the last parameter. If that pointer is null , the method succeeded. If not we can inspect the returned NSError object to understand what was the problem.
Those methods when bridge to Swift will become methods that can throw an NSError. So we see that in Swift our method explode has throws clause.
Now our swift developer can catch the exception as a NSError use the kotlinException extension to recover the original Kotlin Exception
Enum & Sealed classes. Whoever used Swift Enums know that they are the perfect mix between Kotlin enums and sealed. Therefore in order to convince ios devs that KMP is a good idea we must strive for Kotlin enums and sealed classe to behave like Swift enums. Let’s see how they behave.
The problem for Kotlin enums is that Objective-C only has the old C-Style enums, in which every element of enum is mapped to integer constant. However the Konan does a good job of emulating Swift enums by create a class in which every element of the enums is a immutable static instance of the class. We can see on the example that the usage is pretty close to swift enums, but because there’s no way for swift to know that loading, success and failures are the only possibility of our “enum class”, Switch in Swift must be exhaustive, we need to add a useless default clause, that never would be hit.
And things are not better for Sealed classes. Sealed classes are the closest thing to swift enums,. But Swift we see our sealed classes as a ordinary class hierarchy . We can see in the example testSealedClass that our Swift developer will have a hard time trying to downcast to the right class.So what is the solution here? We already saw when there is a feature in both kotlin and swift but is missing in objective we will need to write some swift code
The solution is write a Swift enum that will wrap our Kotlin enum or sealed classes. The enum Swift will also have loading, success, and failures elements. The craziest thing I learned about Swift is that you can define new constructor to a class thru extensions and that new constructor can even return null, something unthinkable for Kotlin.
So we define a constructor that will convert our sealed class to the swift enum. Now we can see on the right panel that is business as usual for our iOS colleague.
But do you really need to write Swift code? Well, there are libraries that can do that for you: Moko Kswift and Touchlab SKIE.
Generics were not supported by Objective-C until 2015, when Apple introduced lightweight Generics, with the sole purpose of improving the interoperability between objective-c collections and swift collections.
So Before Generics you could declare an NSArray but you could not tell what that NSArray contains. It would be mapped in swift to a List of Any, and the iOS developer would have the hurdle of forcibly downcast it to the right type. Similar thing to other collection like NSDictionaries and NSSet
After Generics of course you can specify the contained type. But because the sole goal of Lightweight generics was improve the interoperability of collection classes, generics are only supported by classes
So we see that for GenericInterace, data has type Any in Swift because generics are not supported for protocols.
Generic class is fine, but in BoundClass, we see that the bounds are ignore. The bound for the generic type is Number in Kotlin, but still AnyObject in Swift. Methods and functions do not support generics, but there’s something curious. For types defined by us, Konan does some type erasure. We can on the right panel that the argument’s type of the identity function is mapped to MyInterface, a type defined by us. Where as for the toInt function, uses Any instead of Number, a type defined by java
So we see that for GenericInterace, data has type Any in Swift because generics are not supported for protocols.
Generic class is fine, but in BoundClass, we see that the bounds are ignore. The bound for the generic type is Number in Kotlin, but still AnyObject in Swift. Methods and functions do not support generics, but there’s something curious. For types defined by us, Konan does some type erasure. We can on the right panel that the argument’s type of the identity function is mapped to MyInterface, a type defined by us. Where as for the toInt function, uses Any instead of Number, a type defined by java
So we see that for GenericInterace, data has type Any in Swift because generics are not supported for protocols.
Generic class is fine, but in BoundClass, we see that the bounds are ignore. The bound for the generic type is Number in Kotlin, but still AnyObject in Swift. Methods and functions do not support generics, but there’s something curious. For types defined by us, Konan does some type erasure. We can on the right panel that the argument’s type of the identity function is mapped to MyInterface, a type defined by us. Where as for the toInt function, uses Any instead of Number, a type defined by java
So suspend fuctions are exported to Object-C as function with a completion handler which is the fancy name apple gave to callbacks. Swift since version 5.5 supports asynchronous functions with async/await. CompletionHandler in Objective-C are mapped to async fucntions in Swift.
So that is why we see two genInteger methods in swift, the original from Objective-C using callbacks and the bridged async one. We don’t want to use the callback version because we don’t want to create the pyramid of doom , the callback hell. We see on the right panel that using the async version is pretty similar to using coroutines . You have to create a Task which primity to a couroutine scope and a Job. MainAction is like the main dispatcherYou have to use await keyword to start the async function witch is pretty similar to launch and async methods. But the gotcha here is that canceling the task won’t cancel the coroutine. Remember the async method was generated by xcode who has no idea that you are using kotlin coroutines.
And things for flow . Beside not being able to cancel the collection of the flow. Flow, SharedFlow, Stateflow are all interfaces. And just learned that interfaces pare mapped to protocols which don’t support generics. So wee that in Swift genIntegers just returns a flow not a flow of integer like in kotlin. So even if your swift developer tries to use genInterger, she will have to forcebly cast every emission from Any to Integer.
So how we fix the mess. The solution is quite convoluted. On the kotlin side you need to create a wrapper that allow you to start the coroutine or flow and to cancel it. On the Swift side you need methods that know how to open the wrapper and they propagate the task cancellation, so we can stop the flow or coroutine.
Fortunely there are libraries that do that for your. I am gonna talk about KMP-NativeCoroutine which recently received a grant from Kotlin Foundation. Rick the maintainer of the library is awesome. I submitted a bug report and he fixed and released a new version in matter of couple hours.
KMP-NativeCouroutines is easy to use. All you you need to do is to annotate the couroutine scope you want to use with the NativeCouroutineScope and annotate your suspend functions and flow with the NativeCoroutines annotation . The NativeCoroutines annotation does two things: * It prevents the annoted suspend function and flow from being exported to objective-c much like the hiddenfromobjectivec annotation.
* During the annotation processing phase with KSP it will generation ios specific extensions. We can see the generated code on the right panel. See that the extensions have the ObjetiveCName annotation, so they are effectively replacing you original method with wrapped versions. NativeSuspend and NativeFlow the wrapper types, although they look generic classes, they are actually types alias for a function that receive functions that as arguments. It sounds complicates because it is as we will see in the next slide in which I show the exported swift code.
Despite the complicated return type we can see on the panel at the right that things remain quite the same for our iOS developers. All they need to do is to import the KMPNativeCoroutinesAsync framework to be able use the asyncFunction function that will start our coroutine and stablish the connection between the Swift task and the Kotlin coroutine. So now when the task is cancelled the coroutine will be cancelled as well.
KMP-NativeCoutines has a framework flavor that allows to convert our flow to RxSwift. But the same way that we are moving from RxJava to Flow, swift devs are moving from RxSwift to Combine, a publisher-subscriber framework. Thus I am gonna show how to use KMP-NativeCoutines with Combine.
Our ios Dev then need to import Combine and KMPNativeCouroutineCombine. The function createPublisher converts our flow to the Swift pubisher. Then he can call sink on the publisher that is the same thing as collect or subscribe. And again, canceling the task will cancel the flow.
And if you found everything too complicate just use Compose Multiplatform. You can write an entire iOS app just using Kotin and forget about the Swift developers.