Permission Request Not Dispatching `onSuccess()` on first attempt on iOS
Hi,
I'm developing a Kotlin Multiplatform project with native Google Maps integration. The location permission request works perfectly on Android but has issues on iOS.
Devices tested:
- iPhone 15 Pro (iOS 17)
- iPhone 12 (iOS 17)
Issue:
When the location permission is requested on iOS in viewDidLoad of MapsViewController, the permission dialog is displayed: even if permission is granted by the user, the onSuccess() event is not dispatched during the first attempt. If the MapsViewController is reopened, permission is already granted, and onSuccess() is correctly triggered.
It seems that permissionsController.providePermission(permission) is being called in the PermissionViewModel without any exceptions, but eventsDispatcher.dispatchEvent { onSuccess() } is not executed.
Expected behavior:
- When the permission is granted,
onSuccess()should be dispatched immediately during the first open of theMapsViewController.
Current behavior:
-
onSuccess()is only dispatched on subsequent opens of theMapsViewController, after permissions have already been granted.
Logs:
- First open:
D/MapsViewController: viewDidLoad
D/MapsViewModel: requestPermission called
D/MapsViewModel: pre provide NotDetermined
- Second open:
D/MapsViewController: viewDidLoad
D/MapsViewModel: requestPermission called
D/MapsViewModel: pre provide Granted
D/MapsViewController: Maps permissions granted
D/MapsViewController: setupMapView called
D/MapsViewModel: post provide Granted
Relevant Code:
PermissionViewModel:
class PermissionViewModel(
override val eventsDispatcher: EventsDispatcher<EventListener>,
val permissionsController: PermissionsController,
private val permissionType: Permission
) : ViewModel(), EventsDispatcherOwner<PermissionViewModel.EventListener> {
val permissionState = MutableStateFlow(PermissionState.NotDetermined)
init {
viewModelScope.launch {
permissionState.update { permissionsController.getPermissionState(permissionType) }
println(permissionState)
}
}
fun onRequestPermission() {
requestPermission(permissionType)
}
private fun requestPermission(permission: Permission) {
LogHelper().logDebug("requestPermission called", "PermissionViewModel")
viewModelScope.launch {
try {
permissionsController.getPermissionState(permission)
.also {
LogHelper().logDebug("pre provide $it", "PermissionViewModel")
}
// Calls suspend function in a coroutine to request some permission.
permissionsController.providePermission(permission)
// If there are no exceptions, permission has been granted successfully.
eventsDispatcher.dispatchEvent { onSuccess() }
} catch (deniedAlwaysException: DeniedAlwaysException) {
eventsDispatcher.dispatchEvent { onDeniedAlways(deniedAlwaysException) }
} catch (deniedException: DeniedException) {
eventsDispatcher.dispatchEvent { onDenied(deniedException) }
} finally {
permissionState.update {
permissionsController.getPermissionState(permission)
.also {
LogHelper().logDebug("post provide $it", "PermissionViewModel")
}
}
}
}
}
interface EventListener {
fun onSuccess()
fun onDenied(exception: DeniedException)
fun onDeniedAlways(exception: DeniedAlwaysException)
}
}
MapsViewController:
@OptIn(ExperimentalForeignApi::class)
class MapsViewController : UIViewController(nibName = null, bundle = null), GMSMapViewDelegateProtocol {
private lateinit var mapView: GMSMapView
private val defaultLatitude = 41.12
private val defaultLongitude = 16.87
private val zoomLevel: Float = 15.0f
override fun viewDidLoad() {
super.viewDidLoad()
LogHelper().logDebug("viewDidLoad", "MapsViewController")
val permissionHandler = PermissionHandler(
onPermissionGranted = { onPermissionGranted() },
onPermissionDenied = { onPermissionDenied() },
onPermissionDeniedAlways = { onPermissionDeniedAlways() }
)
val viewModel = PermissionViewModel(
eventsDispatcher = EventsDispatcher(listener = permissionHandler),
permissionsController = PermissionsController(),
permissionType = Permission.LOCATION
)
viewModel.onRequestPermission()
}
private fun onPermissionGranted() {
LogHelper().logDebug("Maps permissions granted", "MapsViewController")
setupMapView()
}
private fun onPermissionDenied() {
LogHelper().logDebug("Maps permissions denied", "MapsViewController")
// TODO: handle permission denied
}
private fun onPermissionDeniedAlways() {
LogHelper().logDebug("Maps permissions denied always", "MapsViewController")
// TODO: handle permission denied always
}
private fun setupMapView() {
LogHelper().logDebug("setupMapView called", "MapsViewController")
val camera = GMSCameraPosition.cameraWithLatitude(defaultLatitude, defaultLongitude, zoomLevel)
mapView = GMSMapView.mapWithFrame(view.bounds, camera)
mapView.delegate = this
mapView.settings.myLocationButton = true
mapView.myLocationEnabled = true
view.addSubview(mapView)
mapView.autoresizingMask = (UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight)
}
}
PermissionHandler:
class PermissionHandler(
private val onPermissionGranted: () -> Unit,
private val onPermissionDenied: () -> Unit,
private val onPermissionDeniedAlways: () -> Unit
) : PermissionViewModel.EventListener {
override fun onSuccess() {
onPermissionGranted()
}
override fun onDenied(exception: DeniedException) {
onPermissionDenied()
}
override fun onDeniedAlways(exception: DeniedAlwaysException) {
onPermissionDeniedAlways()
}
}
Could you please help identify why onSuccess() is not dispatched on the first attempt?
Thank you!
Same thing here. Seems to be an internal crash or stalling of the library - cause the app is not proceeding after providePermission() call the first time. Checked the coroutine that's launching the flow - it's running normally and is not being killed.
@angelacassanelli I don't know if it's still relevant to you - but this is how I overcame the bug:
private suspend fun requestLocationPermissions(
onSuccess: suspend () -> Unit = {},
onDenied: () -> Unit = {}
) {
if (permissionsController.getPermissionState(Permission.COARSE_LOCATION) == PermissionState.Granted) {
onSuccess()
return
}
//TODO: Hack to overcome the moko libraries' bug for ios first-time permission requst
screenModelScope.launch(Dispatchers.IO) {
while (permissionsController.getPermissionState(Permission.COARSE_LOCATION) != PermissionState.Granted) {
delay(200)
}
onSuccess()
}
try {
permissionsController.providePermission(Permission.COARSE_LOCATION)
} catch (e: Exception) {
Logger.e("Error requesting location permissions: ${e.message}")
onDenied()
}
}
It seems like something is blocking the thread inside the library - so that's why it's not proceeding further.
I'm facing a similar issue: code after controller.providePermission(Permission.GALLERY) is never called, even when user grants access. This only happens on iOS
Same here, 0.19.1 not working for LOCATION for iOS for the first time.
Same here, not working for Location in iOS for the 1st time.
Same here, not working on first attempt