Search code examples
iosmapkitkotlin-multiplatformcompose-multiplatform

Override MKMapViewDelegateProtocol in Compose Multiplatform to receive pin selected events


I'm trying to implement an app using Compose Multiplatform to target Android and iOS. The app contains a map and this part should be implemented per platform using Google Maps on Android and Apple Maps on iOS.

I have a Composable function that expects platform specific implementations for Android and iOS

@Composable
expect fun MyMap(items: List<Item>, onItemClicked: (Item) -> Unit)

The problem I'm seeing is that MKMapViewDelegateProtocol.mapView can be quite ambiguous in Kotlin. But I'm not sure how to implemented the protocol in KMP so that it is picked up by the iOS app and I get the pin selected event.

Currently the iOS implementation looks as follows

@Composable
actual fun MyMap(items: List<Item>, onItemClicked: (Item) -> Unit) {
  UIKitView(
    modifier = Modifier.fillMaxSize(),
    factory = {
      MKMapView().apply {
        setDelegate(MKDelegate { annotation ->
          annotation?.title?.let { title ->
            val item = items.find { it.title == title }
            onItemClicked(item)
          } ?: onItemClicked(null)
        })
      }
    },
    update = {
      val pins = items.map { item ->
        val pin = MKPointAnnotation()
        val coordinates = item.coordinates                 

        pin.setCoordinate(CLLocationCoordinate2DMake(coordinates.latitude.coordinate, coordinates.longitude.coordinate))
        pin.setTitle(vendingMachine.address)
        pin
      }
      it.addAnnotations(pins)
    }
  )
}

The pins are displayed correctly on the map, but I don't receive any tap events from the delegate when the user is selecting a pin.

I tried to implemented the MKMapViewDelegateProtocol needed by MKMapView.setDelegate like this:

private class MKDelegate(
  private val onAnnotationClicked: (MKPointAnnotation?) -> Unit
) : NSObject(), MKMapViewDelegateProtocol {
  override fun mapView(mapView: MKMapView, didSelectAnnotationView: MKAnnotationView) {
    val annotation = didSelectAnnotationView.annotation as MKPointAnnotation
    onAnnotationClicked(annotation)
  }
}

Solution

  • In the iOS world, the field named delegate usually stores a weak reference to an object - it means that this object is not responsible for the object's lifecycle, so when the object is destroyed, the property is automatically set to null. This is done to prevent retain cycles. See this question for some details. In case you're planning to implement more iOS features from Kotlin, check out some articles about Automatic Reference Counting (ARC).

    Kotlin has it's own memory model, but when you create ObjC objects even from kotlin code, they follow ObjC memory model.

    So when you create an object with setDelegate(MKDelegate ..., it gets destroyed right away, as this object reference is not stored in any strong reference. You need to store it separately.

    For example, you can use remember:

    val delegate = remember {
        MKDelegate { annotation ->
            // ...
        }
    }
    UIKitView(
        modifier = Modifier.fillMaxSize(),
        factory = {
            MKMapView().apply {
                setDelegate(delegate)
            }
        },