JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.17k stars 1.11k forks source link

No way to create a UIKitView with transparent background #3154

Open alexzhirkevich opened 1 year ago

alexzhirkevich commented 1 year ago

Describe the bug

I want to place UIKIt view with transparent background on top of my Compose content, but this line cuts underlying Compose content and exposes the white root view

Versions

igordmn commented 1 year ago

This is a limitation of the current implementation. We will look in the future, if it is possible to fully support transparency.

I add enhancement, and keep bug until we add a check into code:

require(!background.isSpecified || background.alpha == 1.0) {
  "Transparent background isn't supported at the moment. Follow https://github.com/JetBrains/compose-multiplatform/issues/3154"
}
alexzhirkevich commented 1 year ago

Hope you find a way some day!

artsmvch commented 11 months ago

Is there a workaround for this? This is really a blocking issue as I cannot use UIKitView with UIImageView to display png icons.

alexzhirkevich commented 11 months ago

@alexeiartsimovich Are you sure you need to use a uiimageview for that? What about compose image/icon?

artsmvch commented 11 months ago

@alexzhirkevich I need to display a local png icon on iOS. Can compose image/icon do this? I didn't find an example

alexzhirkevich commented 11 months ago

Like from resources or from file system? There are many libraries for common resources. Official one is used in template. You can also display image from file system by decoding byte array to skia image

alexzhirkevich commented 11 months ago

Also if you want to display iOS system icons in compose you can use this trick (there is also an example of decoding UIImage to compose bitmap)

artsmvch commented 11 months ago

Wow I didn't know we can draw vectors on iOS. Thanks!

alexzhirkevich commented 11 months ago

This can be any image (png, jpeg). Not only xml

JamshedAlamQaderi commented 7 months ago

Hi @dima-avdeev-jb , I am also facing same thing. is there any workaround until this issue resolved?

rustamsmax commented 7 months ago

@JamshedAlamQaderi Here's a temporary workaround

The main difference from original source is: telegram-cloud-photo-size-2-5354879068065091101-y

The last compose version is used

JamshedAlamQaderi commented 7 months ago

@rustamsmax thanks for the workaround. I'm gonna try it

LaatonWalaBhoot commented 7 months ago

@rustamsmax Doing this UiKitView still adds a flicker to the view before setting the background color The flicker is still white. This is how I am doing it.

var animation by remember { mutableStateOf<CompatibleAnimation?>(null) }

    LaunchedEffect(Unit) {
        animation = CompatibleAnimation(
            name = animationRes.fileName,
            subdirectory = null,
            animationRes.bundle
        )
    }

    when (val value = animation) {
        null -> {}
        else -> {
            UIKitView(
                modifier = modifier,
                factory = {
                    UIView().apply {
                        backgroundColor = UIColor.clearColor
                        opaque = true
                        setClipsToBounds(true)
                    }
                },
                background = MaterialTheme.colorScheme.surface,
                update = {
                    val view = CompatibleAnimationView()
                    view.translatesAutoresizingMaskIntoConstraints = false
                    it.addSubview(view)

                    NSLayoutConstraint.activateConstraints(
                        listOf(
                            view.widthAnchor.constraintEqualToAnchor(it.widthAnchor),
                            view.heightAnchor.constraintEqualToAnchor(it.heightAnchor)
                        )
                    )

                    view.setAnimationSpeed(speed.toDouble())
                    view.setCompatibleAnimation(value)
                    view.setLoopAnimationCount(if (isInfinite) -1.0 else 1.0)
                    view.setContentMode(UIViewContentMode.UIViewContentModeScaleAspectFit)
                    view.playWithCompletion { completed ->
                        if (completed) onComplete?.invoke()
                    }
                }
            )
        }
    }
JamshedAlamQaderi commented 7 months ago

Hi @rustamsmax , it's not working.

elijah-semyonov commented 7 months ago

@LaatonWalaBhoot

I couldn't replicate reported behavior using the snippet below. Could you please make a minimal reproducible one which I can investigate?

val UIKitRenderSync = Screen.Example("UIKitRenderSync") {
    var text by remember { mutableStateOf("Type something") }
    var showUIViews by remember { mutableStateOf(true) }
    LazyColumn(Modifier.fillMaxSize().background(Color.Red)) {
        item {
            Button(onClick = {
                showUIViews = !showUIViews
            }) {
                Text("Click")
            }
        }
        items(100) { index ->
            when (index % 4) {
                0 -> Text("material.Text $index", Modifier.fillMaxSize().height(40.dp))
                1 -> {
                    if (showUIViews) {
                        UIKitView(
                            factory = {
                                val label = UILabel(frame = CGRectZero.readValue())
                                label.text = "UILabel $index"
                                label.textColor = UIColor.blackColor
                                label.backgroundColor = UIColor.clearColor
                                label
                            },
                            background = Color.Green,
                            modifier = Modifier.fillMaxWidth().height(40.dp)
                        )
                    }
                }
                2 -> TextField(text, onValueChange = { text = it }, Modifier.fillMaxWidth())
                else -> {
                    if (showUIViews) {
                        ComposeUITextField(text, onValueChange = { text = it }, Modifier.fillMaxWidth().height(40.dp))
                    }
                }
            }
        }
    }
}

/**
 * Compose wrapper for native UITextField.
 * @param value the input text to be shown in the text field.
 * @param onValueChange the callback that is triggered when the input service updates the text. An
 * updated text comes as a parameter of the callback
 * @param modifier a [Modifier] for this text field. Size should be specified in modifier.
 */
@Composable
private fun ComposeUITextField(value: String, onValueChange: (String) -> Unit, modifier: Modifier) {
    val latestOnValueChanged by rememberUpdatedState(onValueChange)

    UIKitView(
        factory = {
            val textField = object : UITextField(CGRectMake(0.0, 0.0, 0.0, 0.0)) {
                @ObjCAction
                fun editingChanged() {
                    latestOnValueChanged(text ?: "")
                }
            }
            textField.addTarget(
                target = textField,
                action = NSSelectorFromString(textField::editingChanged.name),
                forControlEvents = UIControlEventEditingChanged
            )
            textField
        },
        modifier = modifier,
        update = { textField ->
            textField.text = value
        }
    )
}

https://github.com/JetBrains/compose-multiplatform/assets/4167681/05153560-5866-4d92-957c-db4347c4e7ec

JamshedAlamQaderi commented 7 months ago

@elijah-semyonov could you change the background color to Color.Transparent and check if the LazyColumn background color Red is visible or not?

elijah-semyonov commented 7 months ago

It won't be visible, because currently interop views are drawn behind the compose view through the transparent holes.

JamshedAlamQaderi commented 7 months ago

@elijah-semyonov isn't it possible to stack it to top off all compose views by anyway?

elijah-semyonov commented 7 months ago

In absolute terms - yes, it is possible and we will probably do it that way in the future. It will require adapting all modifiers that affect masking (like round corners) for native views.

BoykoDmytro commented 7 months ago

@elijah-semyonov Hi, do u have approximate date or release when it can be on release? Thx

elijah-semyonov commented 6 months ago

@BoykoDmytro We currently don't work on this specific issue and it's not planned yet.

LaatonWalaBhoot commented 2 months ago

@elijah-semyonov Is there any particular reason why this issue will not worked on for the next release? Supporting dark mode is impossible with this because of white blinking which seemingly presents as a UI glitch Especially when using animations

elijah-semyonov commented 2 months ago

@LaatonWalaBhoot We actually did some work in this direction. Can you check if ComposeUIViewControllerConfiguration.opaque is sufficient for your case?

LaatonWalaBhoot commented 2 months ago

@elijah-semyonov this is available with what version of CMP?

elijah-semyonov commented 2 months ago

@LaatonWalaBhoot It should be present in 1.6.0

RazoTRON commented 1 month ago

@elijah-semyonov is there any workaround to avoid transparent background for UIKitView and use compose theme color?

Снимок экрана 2024-04-27 в 19 11 09

Here is a code:

fun SomeDialog() {
    Dialog {
        Scaffold(containerColor = MaterialTheme.colorScheme.background) { 
            Surface(color = MaterialTheme.colorScheme.surface) { 
                SomeTextField() 
            } 
        }
    }
}

@Composable
expect fun SomeTextField()
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
@Composable
actual fun SomeTextField() {

    var value by remember { mutableStateOf("") }

    UIKitView(
        factory = {
            val textField = object : UITextField(
                CGRectMake(0.0, 0.0, 0.0, 0.0)
            ) {
                @ObjCAction
                fun editingChanged() {
                    value = text ?: ""
                }
            }
            textField.addTarget(
                target = textField,
                action = NSSelectorFromString(textField::editingChanged.name),
                forControlEvents = UIControlEventEditingChanged
            )
//            textField.backgroundColor = whiteColor // I don't want to hardcode color
            textField
        },
        modifier = Modifier.background(Color.White).padding(16.dp),
        update = { textField -> textField.text = value },
        onRelease = { textField ->
            textField.removeTarget(
                target = textField,
                action = NSSelectorFromString(textField::editingChanged.name),
                forControlEvents = UIControlEventEditingChanged
            )
        }
    )
}
JamshedAlamQaderi commented 2 weeks ago

It's been so long. but still facing this issue. Please somehow help me. Background shows white if backgroundColor set to transparent

    UIKitView(
      factory = {
          UIView().apply {
              backgroundColor = UIColor.clearColor
              opaque = false
              clipsToBounds = true
          }
      },
      update = {
          it.backgroundColor = UIColor.clearColor
          it.opaque = false
          it.clipsToBounds = true
      },
      modifier = Modifier.size(250.dp).border(1.dp, Color.Transparent).padding(50.dp),
  )
LaatonWalaBhoot commented 2 weeks ago

@JamshedAlamQaderi This worked for me. Make sure you are on latest compose version


UIKitView(
        modifier = modifier,
        factory = {
            UIView().apply {
                backgroundColor = UIColor.clearColor
                opaque = false
                setClipsToBounds(true)
            }
        },
        background = Color.Transparent,
        update = {
            val view = CompatibleAnimationView()
            view.translatesAutoresizingMaskIntoConstraints = false
            it.addSubview(view)
            it.opaque = true

            NSLayoutConstraint.activateConstraints(
                listOf(
                    view.widthAnchor.constraintEqualToAnchor(it.widthAnchor),
                    view.heightAnchor.constraintEqualToAnchor(it.heightAnchor)
                )
            )

            view.setAnimationSpeed(speed.toDouble())
            view.setCompatibleAnimation(animation)
            view.setLoopAnimationCount(if (isInfinite) -1.0 else 1.0)
            view.setContentMode(contentMode)
            view.playWithCompletion { completed ->
                if (completed) onComplete?.invoke()
            }
        }
    )
JamshedAlamQaderi commented 2 weeks ago

@LaatonWalaBhoot thank you for the workaround. I'm gonna give it a try with your example. Compose: 1.6.10

JamshedAlamQaderi commented 2 weeks ago

Hi @LaatonWalaBhoot , could you tell me what is the package of CompatibleAnimationView()?

ismai117 commented 2 weeks ago

@LaatonWalaBhoot I tried your solution but it doesn't work for me?

JamshedAlamQaderi commented 2 weeks ago

@LaatonWalaBhoot neither work for me too

vickyleu commented 19 hours ago

i found a solutions

fun UIView.parent(count:Int):UIView?{
    if(count<=0)return null
    var c = count-1
    var v:UIView? = this.superview
    while ((c-- >0 && v!=null)){
        v =v.superview
    }
    return v
}

update = { view ->
                        val parent = view.parent(4) ?: return@UIKitView
                        if(parent.tag?.toInt()!=10086){
                            parent.opaque = true
                            parent.backgroundColor = UIColor.clearColor
                            parent.findViewController()?. apply controller@ {
                                coroutineScope.launch {
                                    var notStop=true
                                    while (notStop){
                                        withContext(uoocDispatchers.io){
                                            delay(100)
                                            withContext(uoocDispatchers.main){
                                                if(this@controller.isViewLoaded()){
                                                    notStop=false
                                                    this@controller.view.opaque = true
                                                    this@controller.view.backgroundColor = UIColor.clearColor
                                                    this@controller.view.setTag(10086)
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }

view.parent(4) is ComposeContainer UIController root view, normally is black systemBackgroundColor

Printing description of $13:
<ComposeContainer: 0x1087ec740>
Printing description of $14:
<UIView_IgnoreSafeArea: 0x1087ea340; frame = (0 0; 390 844); clipsToBounds = YES; opaque = NO; autoresize = W+H; backgroundColor = <UIDynamicSystemColor: 0x3012faf80; name = systemBackgroundColor>; layer = <CALayer: 0x30079ae40>>

so you just need change UIView_IgnoreSafeArea backgroundColor

ismai117 commented 18 hours ago

@vickyleu can you show the full UIKitView code please

vickyleu commented 18 hours ago

@vickyleu can you show the full UIKitView code please


fun UIView.parent(count:Int):UIView?{
    if(count<=0)return null
    var c = count-1
    var v:UIView? = this.superview
    while ((c-- >0 && v!=null)){
        v =v.superview
    }
    return v
}
fun UIView.findViewController(): UIViewController? {
    var nextResponder: UIResponder? = this
    while (nextResponder != null) {
        if (nextResponder is UIViewController) {
            return nextResponder
        }
        nextResponder = nextResponder.nextResponder
    }
    return null
}
fun UIView.removeControllerColor(scope: CoroutineScope){
    if(tag.toInt()!=10086){
        opaque = true
        backgroundColor = UIColor.clearColor
        findViewController()?. apply controller@ {
            scope.launch {
                var notStop=true
                while (notStop){
                    withContext(uoocDispatchers.io){
                        delay(100)
                        withContext(uoocDispatchers.main){
                            if(this@controller.isViewLoaded()){
                                notStop=false
                                this@controller.view.opaque = true
                                this@controller.view.backgroundColor = UIColor.clearColor
                                this@controller.view.setTag(10086)
                            }
                        }
                    }
                }
            }
        }
    }
}

UIKitView(
      factory = {
                    webview
      },
      interactive = true,
      background = Color.Transparent,
      modifier = Modifier.fillMaxWidth().height(height),
      update = { view ->
           val parent = view.parent(4) ?: return@UIKitView
           parent.removeControllerColor(coroutineScope)
      })
ismai117 commented 17 hours ago

It still shows a white background, am I missing something?

@OptIn(ExperimentalForeignApi::class)
@Composable
fun TestAnimation(
    modifier: Modifier,
) {
    val scope = rememberCoroutineScope()
    when (composition as? CompatibleAnimationView) {
        null -> {}
        else -> {
            UIKitView(
                modifier = modifier,
                factory = {
                    val view = UIView()
                    view
                },
                background = Color.Transparent,
                update = { view ->
                    val parent = view.parent(4)
                    parent?.removeControllerColor(scope)
                }
            )
        }
    }
}

fun UIView.parent(count:Int):UIView?{
    if(count<=0)return null
    var c = count-1
    var v:UIView? = this.superview
    while ((c-- >0 && v!=null)){
        v =v.superview
    }
    return v
}
fun UIView.findViewController(): UIViewController? {
    var nextResponder: UIResponder? = this
    while (nextResponder != null) {
        if (nextResponder is UIViewController) {
            return nextResponder
        }
        nextResponder = nextResponder.nextResponder
    }
    return null
}
fun UIView.removeControllerColor(scope: CoroutineScope) {
    if (tag.toInt() != 10086) {
        opaque = true
        backgroundColor = UIColor.clearColor
        findViewController()?.apply controller@{
            scope.launch {
                var notStop = true
                while (notStop) {
                    withContext(Dispatchers.IO) {
                        delay(100)
                        withContext(Dispatchers.Main) {
                            if (this@controller.isViewLoaded()) {
                                notStop = false
                                this@controller.view.opaque = true
                                this@controller.view.backgroundColor = UIColor.clearColor
                                this@controller.view.setTag(10086)
                            }
                        }
                    }
                }
            }
        }
    }
}
vickyleu commented 17 hours ago

It still shows a white background, am I missing something?


@OptIn(ExperimentalForeignApi::class)

@Composable

fun TestAnimation(

    modifier: Modifier,

) {

    val scope = rememberCoroutineScope()

    when (composition as? CompatibleAnimationView) {

        null -> {}

        else -> {

            UIKitView(

                modifier = modifier,

                factory = {

                    val view = UIView()

                    view

                },

                background = Color.Transparent,

                update = { view ->

                    val parent = view.parent(4)

                    parent?.removeControllerColor(scope)

                }

            )

        }

    }

}

fun UIView.parent(count:Int):UIView?{

    if(count<=0)return null

    var c = count-1

    var v:UIView? = this.superview

    while ((c-- >0 && v!=null)){

        v =v.superview

    }

    return v

}

fun UIView.findViewController(): UIViewController? {

    var nextResponder: UIResponder? = this

    while (nextResponder != null) {

        if (nextResponder is UIViewController) {

            return nextResponder

        }

        nextResponder = nextResponder.nextResponder

    }

    return null

}

fun UIView.removeControllerColor(scope: CoroutineScope) {

    if (tag.toInt() != 10086) {

        opaque = true

        backgroundColor = UIColor.clearColor

        findViewController()?.apply controller@{

            scope.launch {

                var notStop = true

                while (notStop) {

                    withContext(Dispatchers.IO) {

                        delay(100)

                        withContext(Dispatchers.Main) {

                            if (this@controller.isViewLoaded()) {

                                notStop = false

                                this@controller.view.opaque = true

                                this@controller.view.backgroundColor = UIColor.clearColor

                                this@controller.view.setTag(10086)

                            }

                        }

                    }

                }

            }

        }

    }

}

your UIView background not set

ismai117 commented 15 hours ago

I've set the background color but it's still not going away

Screenshot 2024-06-17 at 17 17 22

@OptIn(ExperimentalComposeApi::class)
fun MainViewController() = ComposeUIViewController() {
    val modifier: Modifier = Modifier
   Box(
       modifier = modifier.fillMaxSize().background(Color.Red),
       contentAlignment = Alignment.Center
   ){
       Box(
           modifier = modifier
               .size(200.dp)
       ){
           TestAnimation(
               modifier = modifier
           )
       }
   }
}

@OptIn(ExperimentalForeignApi::class)

@Composable

fun TestAnimation(
    modifier: Modifier,
    ) {

    val scope = rememberCoroutineScope()

    UIKitView(

        modifier = modifier.fillMaxSize(),

        factory = {

            UIView().apply { 
                backgroundColor = UIColor.clearColor
                opaque = false
            }

        },

        background = Color.Transparent,

        update = { view ->

            val parent = view.parent(4)

            parent?.removeControllerColor(scope)

        }

    )

}

fun UIView.parent(count:Int):UIView?{

    if(count<=0)return null

    var c = count-1

    var v:UIView? = this.superview

    while ((c-- >0 && v!=null)){

        v =v.superview

    }

    return v

}

fun UIView.findViewController(): UIViewController? {

    var nextResponder: UIResponder? = this

    while (nextResponder != null) {

        if (nextResponder is UIViewController) {

            return nextResponder

        }

        nextResponder = nextResponder.nextResponder

    }

    return null

}

fun UIView.removeControllerColor(scope: CoroutineScope) {

    if (tag.toInt() != 10086) {

        opaque = true

        backgroundColor = UIColor.clearColor

        findViewController()?.apply controller@{

            scope.launch {

                var notStop = true

                while (notStop) {

                    withContext(Dispatchers.IO) {

                        delay(100)

                        withContext(Dispatchers.Main) {

                            if (this@controller.isViewLoaded()) {

                                notStop = false

                                this@controller.view.opaque = true

                                this@controller.view.backgroundColor = UIColor.clearColor

                                this@controller.view.setTag(10086)

                            }

                        }

                    }

                }

            }

        }

    }

}
vickyleu commented 13 hours ago

@ismai117 maybe the factory method of UIKitView is recreat, now add Observer for CMPInteropWrappingView's backgroundColor

WX20240618-022750@2x

update = { view ->
                        val parent = view.parent(4) ?: return@UIKitView
                        view.removeInteropWrappingViewColor(coroutineScope)
                        parent.removeControllerColor(coroutineScope)
}
/**
 * UIKitView factory maybe reattach to window,Observe backgroundColor change
 */
class InteropWrappingViewWatching(val view:UIView) : NSObject(), ObserverProtocol {
    override fun observeValueForKeyPath(
        keyPath: String?,
        ofObject: Any?,
        change: Map<Any?, *>?,
        context: COpaquePointer?
    ) {
        if(keyPath=="backgroundColor"){
            if(view.backgroundColor!=UIColor.clearColor){
                view.opaque = true
                view.backgroundColor = UIColor.clearColor
                view.setTag(10088)
                view.removeObserver(this,"backgroundColor")
            }
        }
    }
}

fun UIView.removeInteropWrappingViewColor(scope: CoroutineScope){
    if(tag.toInt()!=10087 && tag.toInt()!=10088){
        opaque = true
        backgroundColor = UIColor.clearColor
        val parent = superview?:return
        val obs= InteropWrappingViewWatching(parent)
        findViewController()?. apply controller@ {
            scope.launch {
                var notStop=true
                while (notStop){
                    withContext(uoocDispatchers.io){
                        delay(100)
                        withContext(uoocDispatchers.main){
                            if(this@controller.isViewLoaded()){
                                notStop=false
                                parent.apply {
                                    opaque = true
                                    backgroundColor = UIColor.clearColor
                                    setTag(10087)
                                    addObserver(obs, forKeyPath = "backgroundColor", options = NSKeyValueObservingOptionNew, context = null)
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

observer.def

language = Objective-C
---
#import <Foundation/Foundation.h>

@protocol Observer
@required
- (void)observeValueForKeyPath:(NSString *)keyPath
    ofObject:(id)object
    change:(NSDictionary<NSKeyValueChangeKey, id> *)change
    context:(void *)context;
@end;
targets.withType<KotlinNativeTarget> {
        val path = projectDir.resolve("src/nativeInterop/cinterop/observer")
        binaries.all {
            linkerOpts("-F $path")
            linkerOpts("-ObjC")
        }
        compilations.getByName("main") {
            cinterops.create("observer") {
                compilerOpts("-F $path")
            }
        }
    }