Writing Swift-Friendly Kotlin Multiplatform APIs — Part II
Learn how to code libraries that your iOS teammates will not frown upon using it. In this chapter: name clashing
This is the second article in the series. I advise you to read the first one before proceeding.
You wrote your first Kotlin Multiplatform library. You are now happy and proud of yourself. You even merged that PR using your Android app’s new library. And then, all of a sudden, you received this message from your iOS teammate:
Why is there an underscore in that class name?
You are confused: “I didn’t put any underscores in my library.”
Underscores in the exported Objective-C headers indicate some naming conflict. In the previous article, we saw that:
- Swift does not implement proper namespacing
- Parameter names are part of the method signature
Thus, name clashing may be more common in Swift than in Kotlin. Let’s see some examples of how they can occur.
Different Packages, but the Same Class Name
Suppose you created a class Item to represent the payload of a network response and another class Item to implement a domain model. Despite the same name, they are in different packages:
// KOTLIN API CODE
package io.aoriani.network
class Item
...
package io.aoriani.models
class Item
// EXPORTED OBJ-C HEADER
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Item")))
@interface SharedItem : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Item_")))
@interface SharedItem_ : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end
// SWIFT CLIENT CODE
let item1 = Item()
let item2 = Item_()
The problem
Although the classes are in different packages, there is a single namespace within an Objective-C framework, and consequently, the names will clash. The Kotlin native compiler preventively adds an underscore to one of the classes. But another problem is that the package information was lost, so which class implements the domain model? The one with an underscore or the one without?
The solution
The solution consists in renaming one of the classes, either in Kotlin or in Objective-C/Swift, by using @ObjCName() annotation, which we introduced in the first article of this series.
// KOTLIN API CODE
package io.aoriani.network
import kotlin.experimental.ExperimentalObjCName
import kotlin.native.ObjCName
@OptIn(ExperimentalObjCName::class)
@ObjCName("ItemResponse")
class Item
...
package io.aoriani.Models
class Item
// EXPORTED OBJ-C HEADER
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("ItemResponse")))
@interface SharedItemResponse : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Item")))
@interface SharedItem : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end
// SWIFT CLIENT CODE
let itemDomainModel = Item()
let itemNetworkResponse = ItemResponse()
Function Overloading
Now, suppose you are overloading a sort function to work with different data types:
// KOTLIN API CODE
fun sort(data: List<Int>){}
fun sort(data: Map<String, Int>){}
// EXPORTED OBJ-C HEADER
+ (void)sortData:(NSArray<SharedInt *> *)data __attribute__((swift_name("sort(data:)")));
+ (void)sortData_:(NSDictionary<NSString *, SharedInt *> *)data __attribute__((swift_name("sort(data_:)")));
// SWIFT CLIENT CODE
ExampleKt.sort(data: [1, 2, 3])
ExampleKt.sort(data_: ["A":1, "B": 2])
The problem
As said before, parameter names are part of the function signature, and thus they cannot match in Swift.
The solution
Rename the parameters to avoid conflicts.
// KOTLIN API CODE
fun sort(listOfInts: List<Int>){}
fun sort(mapOfStringToInt: Map<String, Int>){}
// EXPORTED OBJ-C HEADER
+ (void)sortListOfInts:(NSArray<SharedInt *> *)listOfInts __attribute__((swift_name("sort(listOfInts:)")));
+ (void)sortMapOfStringToInt:(NSDictionary<NSString *, SharedInt *> *)mapOfStringToInt __attribute__((swift_name("sort(mapOfStringToInt:)")));
// SWIFT CLIENT CODE
ExampleKt.sort(listOfInts: [1, 2, 3])
ExampleKt.sort(mapOfStringToInt: ["A" : 1, "B": 2])
Interfaces With Similarly Named Properties
Kotlin allows a single class to implement two interfaces with properties with the same name as long as they agree on the type.
// KOTLIN API CODE
interface Animal {
val id: String
}
interface Person {
val id: String
}
interface Product {
val id: Long
}
//Compiler Error: Conflicting declarations: public open val id: String, public open val id: Long
//class Android: Person, Product {
// override val id: String
// get() = TODO("Not yet implemented")
// override val id: Long
// get() = TODO("Not yet implemented")
//}
class ScoobyDoo: Animal, Person {
override val id: String
get() = TODO("Not yet implemented")
}
// EXPORTED OBJ-C HEADER
__attribute__((swift_name("Animal")))
@protocol SharedAnimal
@required
@property (readonly) NSString *id __attribute__((swift_name("id")));
@end
__attribute__((swift_name("Person")))
@protocol SharedPerson
@required
@property (readonly) NSString *id __attribute__((swift_name("id")));
@end
__attribute__((swift_name("Product")))
@protocol SharedProduct
@required
@property (readonly) int64_t id_ __attribute__((swift_name("id_")));
@end
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("ScoobyDoo")))
@interface SharedScoobyDoo : SharedBase <SharedAnimal, SharedPerson>
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@property (readonly) NSString *id __attribute__((swift_name("id")));
@end
// SWIFT CLIENT CODE
class Android: Person, Product {
let id: String = ""
let id_: Int64 = 0
}
The problem
Even though Swift has similar restrictions to Kotlin, the Kotlin native compiler will again act preemptively and add an underscore, even as it will be unlikely that a Swift class will try to implement both interfaces/protocols.
The solution
Unfortunately, like the other cases, the solution is to rename one of the properties.
// KOTLIN API CODE
interface Animal {
val id: String
}
interface Person {
val id: String
}
interface Product {
val identifier: Long
}
// EXPORTED OBJ-C HEADER
__attribute__((swift_name("Animal")))
@protocol SharedAnimal
@required
@property (readonly) NSString *id __attribute__((swift_name("id")));
@end
__attribute__((swift_name("Person")))
@protocol SharedPerson
@required
@property (readonly) NSString *id __attribute__((swift_name("id")));
@end
__attribute__((swift_name("Product")))
@protocol SharedProduct
@required
@property (readonly) int64_t identifier __attribute__((swift_name("identifier")));
@end
// SWIFT CLIENT CODE
class Android: Person, Product {
let id: String = ""
let identifier: Int64 = 0
}
And this is the end of Part II. We dealt with situations that caused name clashes in Swift. Check the next chapter, in which I will tell you why the types are disappearing in Swift:
Writing Swift-friendly Kotlin Multiplatform APIs — Part III
Writing Swift-Friendly Kotlin Multiplatform APIs — Part II was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.