Skip to content

Instantly share code, notes, and snippets.

@chrisbanes
Last active April 23, 2024 11:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrisbanes/bdcf1d5d79c517020d5931524d0f3523 to your computer and use it in GitHub Desktop.
Save chrisbanes/bdcf1d5d79c517020d5931524d0f3523 to your computer and use it in GitHub Desktop.
Compose Multiplatform UIViewController with transparent background
fun myUIViewController(
content: @Composable () -> Unit,
): UIViewController = ComposeUIViewController {
val controller = LocalUIViewController.current
DisposableEffect(controller) {
controller.forceTransparentBackground()
onDispose { /* no-op */ }
}
// Rest of your Compose code
}
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
package dev.chrisbanes
import androidx.compose.ui.interop.UIKitInteropTransaction
import androidx.compose.ui.window.SkikoUIViewDelegate
import androidx.compose.ui.window.UITouchesEventPhase
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExperimentalForeignApi
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Color
import org.jetbrains.skiko.SkikoKeyboardEvent
import platform.CoreGraphics.CGPoint
import platform.Foundation.NSTimeInterval
import platform.UIKit.UIColor
import platform.UIKit.UIEvent
import platform.UIKit.UIView
import platform.UIKit.UIViewController
/**
* This function forces a [UIViewController] created by `ComposeUIViewController` to use a transparent background.
*
* It uses a number of internal functions so beware of that (the suppress annotation at the top is important).
*/
internal fun UIViewController.forceTransparentBackground() {
val view = this.view
view.backgroundColor = UIColor.clearColor
view.opaque = false
view.subviews
.filterIsInstance<androidx.compose.ui.window.SkikoUIView>()
.onEach { skikoUIView ->
skikoUIView.backgroundColor = UIColor.clearColor
skikoUIView.opaque = false
val currentDelegate = skikoUIView.delegate
if (currentDelegate !== null && currentDelegate !is ClearCanvasSkikoUIViewDelegate) {
skikoUIView.delegate = ClearCanvasSkikoUIViewDelegate(currentDelegate)
}
}
}
@OptIn(ExperimentalForeignApi::class)
private class ClearCanvasSkikoUIViewDelegate(
private val delegate: SkikoUIViewDelegate,
) : SkikoUIViewDelegate {
override fun render(canvas: Canvas, targetTimestamp: NSTimeInterval) {
// This is the workaround. We need to clear the canvas before the real delegate renders and
// draws to the canvas
canvas.clear(Color.TRANSPARENT)
// Now let the real delegate render
delegate.render(canvas, targetTimestamp)
}
override fun onKeyboardEvent(event: SkikoKeyboardEvent) = delegate.onKeyboardEvent(event)
override fun pointInside(point: CValue<CGPoint>, event: UIEvent?): Boolean = delegate.pointInside(point, event)
override fun onTouchesEvent(view: UIView, event: UIEvent, phase: UITouchesEventPhase) {
delegate.onTouchesEvent(view, event, phase)
}
override fun retrieveInteropTransaction(): UIKitInteropTransaction = delegate.retrieveInteropTransaction()
override fun onAttachedToWindow() = delegate.onAttachedToWindow()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment