Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only the first UIKitView in a Box is displayed properly on iOS #4004

Closed
brewin opened this issue Dec 4, 2023 · 5 comments · Fixed by JetBrains/compose-multiplatform-core#1145
Assignees
Labels
bug Something isn't working ios

Comments

@brewin
Copy link

brewin commented Dec 4, 2023

Describe the bug
Multiple UIKitViews cannot be displayed in a Box on iOS. Only the first UIKitView is displayed properly.

Affected platforms

  • iOS

Versions

  • Kotlin version*: 1.9.21
  • Compose Multiplatform version*: 1.6.0-dev1296
  • OS version(s)* (required for Desktop and iOS issues): iOS 17.0
  • OS architecture (x86 or arm64): arm64

To Reproduce

Common

@Composable
expect fun DemoView()

iOS

@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun DemoView() {
    Box(modifier = Modifier.fillMaxSize().background(Color.Yellow)) {
        UIKitView(
            factory = {
                UIView().apply {
                    backgroundColor = UIColor.blueColor
                }
            },
            modifier = Modifier.size(100.dp),
        )
        UIKitView(
            factory = {
                UIView().apply {
                    backgroundColor = UIColor.redColor
                }
            },
            modifier = Modifier.size(100.dp).offset(50.dp, 50.dp),
        )
    }
}

Android

@Composable
actual fun DemoView() {
    Box(modifier = Modifier.fillMaxSize().background(Color.Yellow)) {
        AndroidView(
            factory = { context ->
                View(context).apply {
                    setBackgroundColor(Color.Blue.toArgb())
                }
            },
            modifier = Modifier.size(100.dp)
        )
        AndroidView(
            factory = { context ->
                View(context).apply {
                    setBackgroundColor(Color.Red.toArgb())
                }
            },
            modifier = Modifier.size(100.dp).offset(50.dp, 50.dp)
        )
    }
}

Expected behavior
On iOS, only the blue square is displayed properly, with the other square appearing to be rendered behind it and without color. I expect a red square to be displayed overlapping a blue square, like on Android.

@brewin brewin added bug Something isn't working submitted labels Dec 4, 2023
@brewin brewin changed the title Only the first UIKitView in a Box is displayed on iOS Only the first UIKitView in a Box is displayed properly on iOS Dec 4, 2023
@m-sasha
Copy link
Contributor

m-sasha commented Dec 5, 2023

Hi. This is a known limitation due to how we draw native views. Unfortunately we don't currently plan to fix this. You can work around it by placing both native (UIKitView) views inside a single native view.

@m-sasha m-sasha closed this as not planned Won't fix, can't repro, duplicate, stale Dec 5, 2023
@m-sasha m-sasha added wontfix This will not be worked on and removed submitted labels Dec 5, 2023
@m-sasha
Copy link
Contributor

m-sasha commented Dec 5, 2023

On 2nd thought maybe we can do this. It does work on the desktop:

fun main() = singleWindowApplication {
    Column(Modifier.fillMaxSize()) {
        SwingPanel(
            modifier = Modifier.height(200.dp).fillMaxWidth(),
            factory = {
                JPanel().also {
                    it.background = java.awt.Color.RED
                }
            }
        )
        SwingPanel(
            modifier = Modifier.height(200.dp).fillMaxWidth(),
            factory = {
                JPanel().also {
                    it.background = java.awt.Color.YELLOW
                }
            }
        )
        SwingPanel(
            modifier = Modifier.height(200.dp).fillMaxWidth(),
            factory = {
                JPanel().also {
                    it.background = java.awt.Color.GREEN
                }
            }
        )
    }
}
image

@m-sasha m-sasha reopened this Dec 5, 2023
@m-sasha m-sasha added ios and removed wontfix This will not be worked on labels Dec 5, 2023
@m-sasha
Copy link
Contributor

m-sasha commented Dec 5, 2023

Hmm, looks like there are several issues here. I'm getting this:

    Box(modifier = Modifier.fillMaxSize().background(Color.Yellow)) {
        UIKitView(
            factory = {
                UIView().apply {
                    backgroundColor = UIColor.blueColor
                }
            },
            modifier = Modifier.size(100.dp),
        )
        UIKitView(
            factory = {
                UIView().apply {
                    backgroundColor = UIColor.redColor
                }
            },
            modifier = Modifier.size(100.dp).offset(50.dp, 50.dp),
        )
    }
image

Reordering the size and offset modifiers I get this:

image

So there are at least two problems:

  1. It seems the native views are added in reverse order. @dima-avdeev-jb is taking care of this.
  2. The order of modifiers somehow matters, although it seems it shouldn't.

Also @brewin are the screenshots above representative of what you're seeing? Because with the code you provided I do see a white rectangle for the 2nd view, whereas you mentioned that only the 1st view is visible.

@brewin
Copy link
Author

brewin commented Dec 5, 2023

@m-sasha Yes, originally I didn't see the white square. I edited the issue to add the yellow background which makes the white square visible.

Setting the blue square translucent shows the issue with the order.

    Box(modifier = Modifier.fillMaxSize().background(Color.Yellow)) {
        UIKitView(
            factory = {
                UIView().apply {
                    backgroundColor = UIColor.blueColor.colorWithAlphaComponent(0.5)
                }
            },
            modifier = Modifier.size(100.dp),
            background = Color.Transparent,
        )
        UIKitView(
            factory = {
                UIView().apply {
                    backgroundColor = UIColor.redColor
                }
            },
            modifier = Modifier
                .offset(50.dp, 50.dp)
                .size(100.dp),
            background = Color.Transparent,
        )
    }

When size comes before offset, the first square turns purple. It seems the colors are blended before the offset occurs.

dima-avdeev-jb added a commit to JetBrains/compose-multiplatform-core that referenced this issue Jan 22, 2024
@dima-avdeev-jb
Copy link
Contributor

dima-avdeev-jb commented Feb 27, 2024

Fixed in 1.6.0-rc03

MatkovIvan added a commit to JetBrains/compose-multiplatform-core that referenced this issue Feb 29, 2024
## Proposed Changes

Currently on both Desktop and iOS interop views are added to the view
hierarchy in order to add nodes to Compose. It works only if all
intersecting interop views were added at the same time (frame). So it's
basically last-added - above-displayed. This PR changes this behavior in
a way that it will respect the order inside Compose like regular compose
elements.

**It does NOT make any changes in the ability to display Compose content
above interop view on Desktop**, this fix was made in #915

Main changes:
- Unify a way to work with interop on Desktop (`SwingPanel`) and iOS
(`UIKitView`)
- `LocalInteropContainer` -> `LocalUIKitInteropContainer` on iOS
- `LocalLayerContainer` -> `LocalSwingInteropContainer` on Desktop
- Reduce copy-pasting by moving `OverlayLayout` and `EmptyLayout`
- Remove overriding `add` method on `ComposePanel` and
`ComposeWindowPanel` - it was required to redirect interop, but now it's
not required and it's better to avoid changing default AWT hierarchy
behaviour
- Do not use `JLayeredPane`'s layers anymore - it brings a lot of
transparency issues (faced with it on Windows too after unrelated
change). Sorting via indexes is used instead
- Add `InteropOrder` page to mpp demo

### How it works

It utilizes `TraversableNode` to traverse the tree in the right order
and calculate the index based on interop views count that placed before
the current node in the hierarchy. All interop nodes are marked via
`Modifier.trackSwingInterop`/`Modifier.trackUIKitInterop` modifier to
filter them from the `LayoutNode`s tree.

## Testing

Test: run reproducers from the issues or look at "InteropOrder" page in
mpp demo

Desktop | iOS
--- | ---
<img width="400" alt="Screenshot 2024-02-27 at 12 51 06"
src="https://github.com/JetBrains/compose-multiplatform-core/assets/1836384/534cbdc8-9671-4ab7-bd6d-b577d2004d1b">
| <img width="300" alt="Simulator Screenshot - iPhone 15 Pro -
2024-02-27 at 12 49 50"
src="https://github.com/JetBrains/compose-multiplatform-core/assets/1836384/ac7553db-c2a4-4c4a-a270-5d6dbf82fb79">


## Issues Fixed

### Desktop

Fixes JetBrains/compose-multiplatform#2926
Fixes
JetBrains/compose-multiplatform#1521 (comment)

### iOS

Fixes JetBrains/compose-multiplatform#4004
Fixes JetBrains/compose-multiplatform#3848
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working ios
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants