Darron Schall
10/24/2022, 7:43 PMKotlinCValue<AnyObject>
from a KMM library to the actual CValue type when using it in Swift?
(I'll turn this into a thread; the question setup / background is a little complicated.)Darron Schall
10/24/2022, 7:43 PMLocation
Kotlin data class abstraction that looks like this:
// commonMain / Location.kt
data class Location(val longitude: Double, val latitude: Double)
I also have an iOS extension of Location
to help interact with `platform.CoreLocation`:
// iosMain / LocationExtensions.kt
fun Location.asCoordinate(): CValue<CLLocationCoordinate2D> = CLLocationCoordinate2DMake(latitude, longitude)
This works pretty well in the shared library. Any time the platform CoreLocation
or MapKit
needs a CLLocationCoordinate2D
, the CValue<CLLocationCoordinate2D>
is the right type to use in Kotlin. I've paired this simple data class with a simple abstracton for map annotations:
// commonMain / MapMarker.kt
expect class MapMarker(location: Location)
The MapMarker
is an expect
/ actual
so that I can feed these obejcts directly to Apple's MapKit
. The iOS actual
for MapMarker
looks somewhat like this (trimmed for brevity):
actual class MapMarker actual constructor(
private val location: Location
) : NSObject(), MKAnnotationProtocol {
override fun coordinate(): CValue<CLLocationCoordinate2D> {
return location.asCoordinate()
}
}
Usage in Swift
On the Swift side, I'm able to pair MapMarker
directly and easily without issue with MKMapView
, like this:
// Simple data setup for illustration purposes,
// this returns an array of `MapMarker` in the shared
// library that we're able to treat as an array of
// `[MKAnnotation]` from within the iosApp.
var annotations: [MKAnnotation] = MockData().mockAnnotations
// It's straightforward to display the annotations
let mapView = MKMapView()
let pointAnnotationViews = annotations.map {
let pointAnnotation = MKPointAnnotation()
// Note: MKAnnotation `coordinate` delegates to our
// shared `Location` `asCoordinate` helper, which
// returns a `CLLocationCoordinate2D`, as expected.
pointAnnotation.coordinate = $0.coordinate
return pointAnnotation
}
view.addAnnotations(pointAnnotationViews)
The problem I'm having is that the CValue<CLLocationCoordinate2D>
Kotlin return type is treated as a CLLocationCoordinate2D
in Swift from within the MKAnnotationProtocol
context (a class extending NSObject
). But, the same return type from a Kotlin class (not extending NSObject
), doesn't return a CLLocationCoordinate2D
. Instead, I (unexpectedly) get a `KotlinCValue<AnyObject>`:
// in Swift, this returns a KotlinCValue<AnyObject>
var value = MockData().defaultMapLocation.asCoordinate()
Again, that's not a CLLocationCoordinate2D
, and it won't cast to one. I can't figure out how turn it into one, either; I'm new to Kotlin/Native cinterop and my understanding is still incomplete. Maybe I need to use Unmanaged
like the comments from https://youtrack.jetbrains.com/issue/KT-39407/KotlinNative-Swift-interop-iOS-CGImage suggest... I tried a few different iterations of that approach, unsuccessfully.
Back to the original question
What's the best "right" way to get the CLLocationCoordinate2D
out of the KotlinCValue<AnyObject>
? Or, to ask it another way, how can I have Location.asCoordiante()
return a CLLocationCoordinate2D
that Swift can use?
I think this is related to improving Core Foundation support per https://youtrack.jetbrains.com/issue/KT-29256/Improve-support-for-Core-Foundation-types since Core Location is another C framework.
I think the "right" way would be to convert my Location
to an expect
/ actual
that extends NSObject
in iosMain
, and conforms to a protocol for "asCoordinate" that I need to make and declare in a nativeinterop
def
file (so that I can send the message to the NSObject
to prevent "unrecognized selector sent to instance" errors, since NSObject
subclasses appear to lose method definitions in the generated header file).
But, what I'd really like to have happen is have this "just work" as-is keeping Location
as a Kotlin data class, maybe with improved compiler support. It's unexpected to me that returning CValue<CLLocationCoordinate2D>
"works" sometimes, but not others, and the documentation and discoverability isn't good enough yet to sort through it.
For what it's worth, I've also found a different workaround doing a little more on the Kotlin side, but I don't think it's treating memory correctly (specifically, I'm not sure if the MemScope()
below gives me an auto-released object that ARC captures on the Swift side of things to keep it around):
// In commonMain LocationExtensions.kt - Exchange the CValue for a CPointer.
fun Location.asCoordinatePointer(): CPointer<CLLocationCoordinate2D> = asCoordinate().getPointer(MemScope())
I can get the CLLocationCoordinate2D
in Swift by calling this as location.asCoordinatePointer().load(as: CLLocationCoordinate2D.self)
.. but, again, this doesn't feel quite right to me.
Another workaround, which I don't like, is to re-declare the method in Swift:
// Location+Extensions.swift
extension Location {
func asCoordinate() -> CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}
This ends up making two asCoordiante()
methods; the Kotlin one returning KotlinCValue<AnyObject>
and the Swift one returning CLLocationCoordinate2D
, and the compiler picks the right one based on the return type. This also duplicates the code itself (making the CLLocationCoordinate2D
), and feels kind of gross.
I guess a final way of asking this is: What is special about the NSObject(), MKAnnotationProtocol
setup that let's me treat returning CValue<CLLocationCoordinate2D>
as a CLLocationCoordinate2D
in Swift, when a Kotlin data class that returns a CValue<CLLocationCoordinate2D>
returns it to Swift as KotlinCValue<AnyObject>
?Jonas Hiltl
01/05/2023, 9:05 PMCValue<CLLocationCoordinate2D>
from the asCoordinate
function inside the compiled objective-c code? I also posted my specific question here stackoverflow, I have pretty much the same setup as you did.Darron Schall
01/05/2023, 9:26 PMJonas Hiltl
01/05/2023, 9:32 PM