compose-multiplatform icon indicating copy to clipboard operation
compose-multiplatform copied to clipboard

No way to create a UIKitView with transparent background

Open alexzhirkevich opened this issue 1 year ago • 43 comments

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

  • Compose Multiplatform version*: 1.4.0

alexzhirkevich avatar May 11 '23 13:05 alexzhirkevich

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"
}

igordmn avatar May 11 '23 17:05 igordmn

Hope you find a way some day!

alexzhirkevich avatar May 11 '23 19:05 alexzhirkevich

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

artsmvch avatar Jul 14 '23 21:07 artsmvch

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

alexzhirkevich avatar Jul 15 '23 10:07 alexzhirkevich

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

artsmvch avatar Jul 15 '23 10:07 artsmvch

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 avatar Jul 15 '23 11:07 alexzhirkevich

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)

alexzhirkevich avatar Jul 15 '23 11:07 alexzhirkevich

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

artsmvch avatar Jul 15 '23 11:07 artsmvch

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

alexzhirkevich avatar Jul 15 '23 11:07 alexzhirkevich

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

JamshedAlamQaderi avatar Nov 07 '23 12:11 JamshedAlamQaderi

@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

rsktash avatar Nov 07 '23 13:11 rsktash

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

JamshedAlamQaderi avatar Nov 07 '23 15:11 JamshedAlamQaderi

@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()
                    }
                }
            )
        }
    }

LaatonWalaBhoot avatar Nov 07 '23 19:11 LaatonWalaBhoot

Hi @rustamsmax , it's not working.

JamshedAlamQaderi avatar Nov 11 '23 11:11 JamshedAlamQaderi

@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

elijah-semyonov avatar Nov 15 '23 11:11 elijah-semyonov

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

JamshedAlamQaderi avatar Nov 15 '23 11:11 JamshedAlamQaderi

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

elijah-semyonov avatar Nov 15 '23 12:11 elijah-semyonov

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

JamshedAlamQaderi avatar Nov 15 '23 13:11 JamshedAlamQaderi

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.

elijah-semyonov avatar Nov 15 '23 13:11 elijah-semyonov

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

BoykoDmytro avatar Nov 20 '23 17:11 BoykoDmytro

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

elijah-semyonov avatar Nov 21 '23 10:11 elijah-semyonov

@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

LaatonWalaBhoot avatar Apr 09 '24 08:04 LaatonWalaBhoot

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

elijah-semyonov avatar Apr 09 '24 12:04 elijah-semyonov

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

LaatonWalaBhoot avatar Apr 09 '24 12:04 LaatonWalaBhoot

@LaatonWalaBhoot It should be present in 1.6.0

elijah-semyonov avatar Apr 09 '24 13:04 elijah-semyonov

@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
            )
        }
    )
}

RazoTRON avatar Apr 27 '24 16:04 RazoTRON

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),
  )

JamshedAlamQaderi avatar May 30 '24 14:05 JamshedAlamQaderi

@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()
            }
        }
    )

LaatonWalaBhoot avatar May 30 '24 15:05 LaatonWalaBhoot

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

JamshedAlamQaderi avatar May 30 '24 15:05 JamshedAlamQaderi

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

JamshedAlamQaderi avatar May 30 '24 18:05 JamshedAlamQaderi