Skip to content

Support sizing of Compose hosting views on iOS#2984

Draft
svastven wants to merge 7 commits intojb-mainfrom
svastven/sizing-compose-view
Draft

Support sizing of Compose hosting views on iOS#2984
svastven wants to merge 7 commits intojb-mainfrom
svastven/sizing-compose-view

Conversation

@svastven
Copy link
Copy Markdown

@svastven svastven commented Apr 19, 2026

This PR adds a sizing integration that lets a Compose-hosting UIView participate in UIKit/SwiftUI sizing loops by reporting Compose’s preferred size for a given sizing proposal.

Fixes CMP-5788 iOS investigate intrinsic sizing of ComposeUIViewController
Fixes CMP-5873 [Epic] iOS intrinsic sizing of interop elements

How it works

  1. UIKit/SwiftUI calls sizeThatFits(...) with a proposal (including UIViewNoIntrinsicMetric for unbounded axes).
  2. The container converts the proposal into Compose Constraints.
  3. After each Compose layout completes, we probe-measure the scene under the latest proposal constraints and compute the preferred size.
  4. If the preferred size changes, the hosting view invalidates its intrinsic content size, allowing UIKit/SwiftUI to re-run layout and apply the new size.

Fallback handling uses super.sizeThatFits for non-intrinsic axes to avoid 0 sizes before Compose has produced a measurement.

Public API changes

  • Add useSelfSizing to ComposeContainerConfiguration. Default is false to preserve existing behavior. When enabled, the container can size itself to fit Compose content under UIKit/SwiftUI size proposals.

Example usage

(SwiftUI, iOS 16+): sizeThatFits

On the Kotlin side, enable the feature (defaults to false):

val view = ComposeUIView(
    configure = { useSelfSizing = true },
) { FooComposable() }

On the SwiftUI side, wrap the returned UIView/UIViewController and forward SwiftUI’s proposal to UIKit sizeThatFits:

import SwiftUI
import UIKit

struct ComposeView: UIViewRepresentable {
    // Provided by the Kotlin/Swift bridge
    let makeComposeUIView: () -> UIView

    func makeUIView(context: Context) -> UIView {
        makeComposeUIView()
    }

    func updateUIView(_ uiView: UIView, context: Context) {}

    @available(iOS 16.0, *)
    func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIView, context: Context) -> CGSize? {
        let proposed = CGSize(
            width: proposal.width ?? UIView.noIntrinsicMetric,
            height: proposal.height ?? UIView.noIntrinsicMetric
        )
        // Calls into ComposeHostingView/ComposeContainerView.sizeThatFits, which uses the latest
        // proposal constraints to produce Compose’s preferred size.
        return uiView.sizeThatFits(proposed)
    }
}

(SwiftUI, iOS <16): sizeThatFits

The same mechanism works via intrinsic sizing alone: SwiftUI reacts to invalidateIntrinsicContentSize() and re-queries layout.

Testing

Adds instrumented tests that simulate the SwiftUI sizing loop (proposal → sizeThatFits → apply frame → wait for intrinsic invalidation → repeat) and validate that Compose scene size and hosting view frame converge as Compose content or size proposed by SwiftUI changes.

This should be tested by QA

Release Notes

Features - iOS

  • Support automatic sizing of Compose hosting views (ComposeHostingView / ComposeHostingViewController) used on the UIKit / SwiftUI side. Enabled by default; opt out via ComposeContainerConfiguration.useSelfSizing.

Breaking Changes - iOS

  • Compose hosting views now automatically adjust their size to match their content by default. This may result in different layout behavior than before. A new flag ComposeContainerConfiguration.useSelfSizing allows opting out. Set it to false to preserve the previous behavior. This flag is temporary and will be removed in version 1.13.0.

@svastven svastven force-pushed the svastven/sizing-compose-view branch 2 times, most recently from c3fcdd8 to 9d38020 Compare April 20, 2026 00:12
@svastven svastven changed the title Auto-sizing of Compose Hosting on iOS Sizing of Compose Hosting Views on iOS Apr 20, 2026
@svastven svastven changed the title Sizing of Compose Hosting Views on iOS Support sizing of Compose Hosting Views on iOS Apr 20, 2026
@svastven svastven changed the title Support sizing of Compose Hosting Views on iOS Support sizing of Compose hosting views on iOS Apr 20, 2026
@svastven
Copy link
Copy Markdown
Author

svastven commented Apr 20, 2026

Marked as draft as I still need to add tests for the UIKit/SwiftUI intrinsic sizing path.

@svastven svastven requested a review from m-sasha April 20, 2026 11:13
@svastven
Copy link
Copy Markdown
Author

@m-sasha Adding you as reviewer, too because desktop sources are involved and there is overlap with #2938

@svastven svastven force-pushed the svastven/sizing-compose-view branch from 5b3ada9 to 0ae5bce Compare April 21, 2026 14:54
Comment on lines +331 to +336
/**
* Returns the current content size (in pixels) measured with the provided [constraints].
*
* @throws IllegalStateException when [ComposeScene] content has lazy layouts without maximum
* size bounds (e.g. LazyColumn without maximum height) and the constraints are unbounded.
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is only correct if the relevant dimension is unconstrained (e.g. height=Infinity).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so "the constraints are unbounded" doesn't reflect the behavior correctly? It seems to me that the doc is the same as for unconstrainedSize() only specifies the unbounded constraints in addition which is exactly what happens in unconstrainedSize()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you're right. I didn't read the comment to the end, though it was just copy/pasted from unconstrainedSize.

I would just clarify in the comment that this applies on each axis separately.

* platform integrations to avoid retain cycles between a hosting container and
* the scene/root.
*/
fun registerOnLayoutCompletedListener(listener: () -> Unit): AutoCloseable
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, but I'd slightly prefer a dedicated class instead of AutoCloseable.
Existing examples:

  • PinnableContainer.PinnedHandle
  • DelegatableNode.RegistrationHandle (maybe even just use this)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a custom OnLayoutCompletedListenerHandle

@svastven svastven requested a review from m-sasha April 21, 2026 22:21
Copy link
Copy Markdown
Collaborator

@MatkovIvan MatkovIvan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's the right way to support it.
The root problem now is that compose stages are not aligned with the platform framework.
We already have an item to do it properly, let's combine the efforts to do it right instead of redoing things again later

* the listener. This is important for platform integrations to avoid retain cycles between
* a hosting container and the scene/root.
*/
fun registerOnLayoutCompletedListener(listener: () -> Unit): OnLayoutCompletedListenerHandle
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's supposed to be a separate "layout" call from the platform (TBD). The scene itself shouldn't trigger layout independently, so such event shouldn't exist/be required

Copy link
Copy Markdown
Author

@svastven svastven Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please clarify for me: We already have an item to do it properly ?

Do you mean

  • we already have a way to do this properly?
  • we have a TODO item to do it properly?

If the second, do we have some more description as to what is missing and what we require?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #3012 for phases’ split

* `sizeThatFits` / intrinsic sizing).
*/
@ExperimentalComposeUiApi
var useSelfSizing: Boolean = false
Copy link
Copy Markdown

@ASalavei ASalavei Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this option? I assumed that self-sizing (like, providing intrinsicContentSize) should be always ON.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this flag so we don't change behavior for existing users. If we turn it on by default, it will affect what they already have. Do we want that, or keep it opt-in for now?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm.. Previously we didn't have any constraints there - so users must configure them explicitly. I expect that transition should go smooth to the new state in most cases.
It looks like a safe option here is to make useSelfSizing = true by default, with "Breaking Change" release note and also with intent to remove the flag in the next version (1.13).


rootViewController.view.let {
if (configuration.useSelfSizing) {
it.addSubview(hostingView)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I get the point. If we're adding view with the translatesAutoresizingMaskIntoConstraints == true, it means the frame of this view must be manually adjusted somewhere (btw, I didn't find the place where it happened). The idea behind the "Self Sizing" Compose views was to make it work correctly inside Autolayout and in the SwiftUI system. No sure we should manually update the frame.

invalidateComposeSceneContainerSize = {
// SwiftUI observes the hosting view’s intrinsic size, not the internal Compose
// container view.
view.superview?.invalidateIntrinsicContentSize()
Copy link
Copy Markdown

@ASalavei ASalavei Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is true, but in case of ComposeHostingViewController, the view.superview will refer the user's view, not a compose-related one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants