Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.github.vinceglb.filekit.dialogs.FileKitDialog.documentPickerDelegate
import io.github.vinceglb.filekit.dialogs.FileKitDialog.phPickerDelegate
import io.github.vinceglb.filekit.dialogs.FileKitDialog.phPickerDismissDelegate
import io.github.vinceglb.filekit.dialogs.util.CameraControllerDelegate
import io.github.vinceglb.filekit.dialogs.util.CameraPresenterWindow
import io.github.vinceglb.filekit.dialogs.util.DocumentPickerDelegate
import io.github.vinceglb.filekit.dialogs.util.PhPickerDelegate
import io.github.vinceglb.filekit.dialogs.util.PhPickerDismissDelegate
Expand Down Expand Up @@ -287,37 +288,45 @@ public actual suspend fun FileKit.openCameraPicker(
null
}
}
val presentation = prepareAppleCameraPresentation(
sourceAvailable = UIImagePickerController.isSourceTypeAvailable(cameraSource),
presenter = openCameraSettings.presenterViewController(),
requestedCamera = requestedCamera,
)

suspendCancellableCoroutine<UIImage?> { continuation ->
cameraControllerDelegate = CameraControllerDelegate(
onImagePicked = { image ->
try {
continuation.resume(
requireAppleCameraImage(image),
)
} catch (failure: FileKitDialogException) {
continuation.resumeWithException(failure)
}
},
onPickerCancelled = { continuation.resume(null) },
val presenterWindow = when (openCameraSettings.presenter) {
null -> CameraPresenterWindow()
else -> null
}
try {
val presentation = prepareAppleCameraPresentation(
sourceAvailable = UIImagePickerController.isSourceTypeAvailable(cameraSource),
presenter = openCameraSettings.presenter ?: presenterWindow?.attach(),
requestedCamera = requestedCamera,
)

val pickerController = UIImagePickerController()
pickerController.sourceType = cameraSource
pickerController.delegate = cameraControllerDelegate
suspendCancellableCoroutine<UIImage?> { continuation ->
cameraControllerDelegate = CameraControllerDelegate(
onImagePicked = { image ->
try {
continuation.resume(
requireAppleCameraImage(image),
)
} catch (failure: FileKitDialogException) {
continuation.resumeWithException(failure)
}
},
onPickerCancelled = { continuation.resume(null) },
)

presentation.cameraDevice?.let { pickerController.cameraDevice = it }
val pickerController = UIImagePickerController()
pickerController.sourceType = cameraSource
pickerController.delegate = cameraControllerDelegate

presentation.presenter.presentViewController(
pickerController,
animated = true,
completion = null,
)
presentation.cameraDevice?.let { pickerController.cameraDevice = it }

presentation.presenter.presentViewController(
pickerController,
animated = true,
completion = null,
)
}
} finally {
presenterWindow?.detach()
}
} ?: return null

Expand Down Expand Up @@ -525,9 +534,6 @@ private fun FileKitDialogSettings.presenterViewController(
activeViewController: () -> UIViewController? = ::activeAppleViewController,
): UIViewController? = presenter ?: activeViewController()

private fun FileKitOpenCameraSettings.presenterViewController(): UIViewController? =
presenter ?: UIApplication.sharedApplication.topMostViewController()

private fun FileKitShareSettings.presenterViewController(): UIViewController? =
presenter ?: UIApplication.sharedApplication.topMostViewController()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import platform.UIKit.UIViewController
/**
* iOS implementation of [FileKitOpenCameraSettings].
*
* @property presenter The view controller used to present the camera picker.
* @property presenter The view controller used to present the camera picker. When null, FileKit
* presents the camera from a dedicated window placed above the app's windows, which keeps the
* picker compatible with hosts whose dialogs live in their own window, such as Compose
* Multiplatform 1.11+.
*/
public actual class FileKitOpenCameraSettings(
public val presenter: UIViewController? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.github.vinceglb.filekit.dialogs.util

import kotlinx.cinterop.ExperimentalForeignApi
import platform.UIKit.UIApplication
import platform.UIKit.UIColor
import platform.UIKit.UIScreen
import platform.UIKit.UIViewController
import platform.UIKit.UIWindow
import platform.UIKit.UIWindowLevelAlert
import platform.UIKit.UIWindowScene

/**
* Hosts the camera presentation in a dedicated transparent [UIWindow].
*
* Since Compose Multiplatform 1.11, Compose dialogs and popups live in their own window placed
* above modally presented view controllers. Presenting the fullscreen camera from the top-most
* view controller of the main window puts it underneath such windows, which corrupts touch
* handling app-wide after the dismissal. Presenting from a dedicated key window above alerts
* avoids that; the previous key window is restored once the capture flow finishes.
*/
internal class CameraPresenterWindow {
private val hostViewController = UIViewController()
private var window: UIWindow? = null
private var previousKeyWindow: UIWindow? = null

@OptIn(ExperimentalForeignApi::class)
fun attach(): UIViewController {
val application = UIApplication.sharedApplication
previousKeyWindow = application.keyWindow
val scene = application.connectedScenes.firstNotNullOfOrNull { it as? UIWindowScene }
val newWindow = scene?.let(::UIWindow) ?: UIWindow(frame = UIScreen.mainScreen.bounds)
newWindow.rootViewController = hostViewController
newWindow.windowLevel = UIWindowLevelAlert + 1.0
newWindow.backgroundColor = UIColor.clearColor
newWindow.makeKeyAndVisible()
window = newWindow
return hostViewController
}

fun detach() {
window?.setHidden(true)
window?.rootViewController = null
window = null
previousKeyWindow?.makeKeyAndVisible()
previousKeyWindow = null
}
}