Table of Contents

    Book an Appointment

    What Is The Best Way To Embed Compose Multiplatform In A SwiftUI ScrollView?

    While working on a secure healthcare messaging platform, we faced a common but frustrating architectural challenge. Our goal was to share a complex chat interface across platforms using Compose Multiplatform, while keeping the broader application architecture rooted in native SwiftUI. This approach allowed us to reuse the heavy business logic and UI components of the chat system while maintaining the native look and feel of the surrounding iOS application.

    During the integration phase, we discovered a critical rendering issue. The shared Compose UI was embedded via a ComposeUIViewController inside a standard SwiftUI UIViewControllerRepresentable. Because the chat interface needed to scroll alongside native headers and footers, it was placed inside a SwiftUI ScrollView. However, upon launching the application, the entire Compose view collapsed to zero height, rendering the chat completely invisible to users.

    This problem matters deeply in production environments because user interfaces dynamically driven by data cannot rely on hardcoded dimensions. A collapsing view not only breaks the user experience but can stall cross-platform migration efforts entirely. This challenge inspired us to document our findings, so other engineering teams can avoid the same pitfall when bridging the gap between declarative native frameworks and multiplatform solutions. It is precisely these nuanced challenges that companies aim to solve when they hire software developer teams with deep cross-platform expertise.

    Why Does SwiftUI Fail To Detect The Height Of ComposeUIViewController?

    To understand the business and technical context, we must look at how layout engines communicate. In our healthcare platform, the chat view needed to display dynamic messages, images, and clinical attachments. The overarching iOS architecture relied on SwiftUI’s layout system, which asks its child views for their sizing requirements before rendering them.

    When placing a UIKit component inside SwiftUI, we use UIViewControllerRepresentable. SwiftUI expects the underlying UIViewController to declare its size requirements. If the view is placed in a loosely constrained environment—like a ScrollView, which allows infinite vertical expansion—SwiftUI relies heavily on the view’s intrinsic content size to determine how much vertical space to allocate.

    The core issue is that a ComposeUIViewController does not automatically calculate and expose the intrinsic height of its inner Compose hierarchy to UIKit. When SwiftUI queries the representable for its dimensions, the bridge fails to provide a concrete number, leading the layout engine to collapse the view to its minimum possible size: zero.

    How Did The View Collapse Issue Surface In Production?

    The symptom was immediate and glaring: the chat screen appeared completely blank below the native SwiftUI navigation bar. When we attached the view hierarchy debugger, we confirmed that the ComposeUIViewController was successfully instantiated and contained the chat nodes, but its bounding frame had a height of zero.

    To diagnose this, we inspected the metrics of the underlying view. We found that the system was returning the following layout values:

    intrinsicContentSize = (-1.0, -1.0)
    

    In UIKit terms, (-1.0, -1.0) corresponds directly to UIView.noIntrinsicMetrics. Because the intrinsic size was absent, applying the standard SwiftUI modifier .fixedSize(horizontal: false, vertical: true) had no effect. The modifier attempts to force the view to its ideal size, but since the ideal size was reported as nonexistent, the layout engine simply collapsed the container.

    We also reviewed the Compose Multiplatform release notes, specifically around version 1.10.x, which mentioned “Autosizing Interop Views.” However, explicit documentation on leveraging this for complex nested scroll scenarios was sparse, pushing us to engineer a deterministic sizing bridge manually.

    What Approaches Can Resolve Intrinsic Content Size Issues In Compose?

    When tackling a bridge layout issue, we evaluate several architectural paths to ensure performance and maintainability. It is during this evaluation phase that organizations benefit most when they hire app developer to create a mobile app with robust foundations. We considered the following approaches:

    Could We Hardcode The Height Using GeometryReader?

    Our initial thought was to use SwiftUI’s GeometryReader to measure the available screen space and force a height on the Compose container. However, this approach was immediately rejected. Chat views have dynamic content—keyboards appear, messages expand, and users scroll. Hardcoding heights would inevitably lead to clipping or awkward empty spaces.

    Could We Use A Custom UIView Wrapper With Auto Layout?

    Another option was to bypass UIViewControllerRepresentable directly and instead wrap the ComposeUIViewController.view inside a custom UIView. By overriding the intrinsicContentSize property and calling invalidateIntrinsicContentSize() whenever layout changes occurred, we could theoretically force UIKit to recalculate. While viable, this required extensive boilerplate and heavy manipulation of Auto Layout constraints, which often introduces performance bottlenecks during rapid UI updates.

    Could We Use Compose’s onGloballyPositioned Modifier?

    The most deterministic approach was to measure the exact height of the UI from within Compose and pass that metric up to SwiftUI. By attaching an onGloballyPositioned modifier to the root Compose element, we could capture the layout dimensions in pixels, convert them to standard iOS points, and feed them into a SwiftUI state variable. This approach guarantees that SwiftUI always knows the exact mathematical height required by the Compose content.

    How To Dynamically Calculate And Pass Compose Content Size To SwiftUI?

    We finalized our implementation using the state-binding approach. By creating a communication channel between Kotlin and Swift, we ensured that whenever the chat content changed, SwiftUI immediately received the updated height.

    First, we updated our Kotlin initialization function to accept a callback for size changes. Inside the Compose tree, we read the size and invoked the callback:

    // Kotlin Multiplatform Shared Code
    fun buildComposeViewController(
        onSizeChanged: (Double) -> Unit,
        content: @Composable () -> Unit
    ): UIViewController {
        return ComposeUIViewController {
            Box(
                modifier = Modifier.onGloballyPositioned { coordinates ->
                    // Convert pixel height to iOS points dynamically if needed, 
                    // or handle density conversion depending on your setup.
                    val height = coordinates.size.height.toDouble()
                    onSizeChanged(height)
                }
            ) {
                content()
            }
        }
    }
    

    Next, we modified our SwiftUI UIViewControllerRepresentable to utilize this callback. We tied the incoming size directly to a SwiftUI state binding:

    // Swift Implementation
    struct ComposeContainer: UIViewControllerRepresentable {
        @Binding var contentHeight: CGFloat
        func makeUIViewController(context: Context) -> UIViewController {
            return Main_iosKt.buildComposeViewController(onSizeChanged: { newHeight in
                DispatchQueue.main.async {
                    // Ensure state updates happen on the main thread
                    self.contentHeight = CGFloat(newHeight)
                }
            }, content: {
                // Compose Content
            })
        }
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
            // Handle updates if necessary
        }
    }
    

    Finally, we applied the calculated height back into the SwiftUI ScrollView setup. This specific pattern is exactly why teams choose to hire iOS developers for cross-platform integration, as it requires knowing the nuances of SwiftUI’s state engine.

    // SwiftUI Usage
    struct ChatViewWrapper: View {
        @State private var composeHeight: CGFloat = 0
        var body: some View {
            ScrollView {
                ComposeContainer(contentHeight: $composeHeight)
                    .frame(height: max(composeHeight, .zero)) // Apply the dynamic height
                    .clipped()
            }
        }
    }
    

    This implementation completely resolved the collapsing view issue. Performance remained smooth because onGloballyPositioned only triggers when layout dimensions actually change, avoiding unnecessary render loops.

    What Can Engineering Teams Learn From Cross-Platform UI Interop?

    Solving this layout discrepancy reinforced several architectural principles that are critical when blending native and multiplatform frameworks. When companies hire Kotlin developers for native interop, they expect the team to proactively address these exact system boundaries.

    • Interop Boundaries Require Manual Translation: Never assume layout paradigms translate automatically. SwiftUI and Compose use fundamentally different layout calculation phases.
    • Intrinsic Metrics Are Mandatory For ScrollViews: Any view placed inside a loosely constrained container must know how to calculate its own size. Without intrinsicContentSize, UIKit representations will default to zero.
    • State Synchronization Must Be Thread-Safe: When passing metrics from Compose to SwiftUI, always dispatch UI state updates to the main thread to prevent random crashes.
    • Density Conversion Is Key: Remember that layout coordinates in Compose might be represented differently than standard iOS points depending on your density configuration. Always verify the math.
    • Watch For Render Loops: When creating callbacks that update SwiftUI view heights, ensure you aren’t triggering infinite recompositions. Only update the state if the new height significantly differs from the old one.

    How Can We Summarize The Solution For ComposeUIViewController Sizing?

    Embedding a ComposeUIViewController inside a SwiftUI ScrollView often leads to collapsing views due to missing intrinsic content sizing. By leveraging Compose’s layout modifiers to measure the content and passing that data via a callback to a SwiftUI state binding, engineering teams can bridge the layout engines flawlessly. This ensures dynamic content like our healthcare chat application renders perfectly without sacrificing the overarching native architecture.

    If your team is struggling with complex cross-platform UI architectures, rendering bottlenecks, or migrating native applications to shared codebases, we can help streamline the process. Feel free to contact us to discuss how dedicated engineering expertise can accelerate your next initiative.

    Social Hashtags

    #ComposeMultiplatform #SwiftUI #KotlinMultiplatform #KMP #iOSDevelopment #MobileDevelopment #CrossPlatform #UIKit #ComposeUI #AndroidDev #AppleDeveloper #SoftwareEngineering #AppDevelopment #Kotlin #Swift

     

    Frequently Asked Questions

    Success Stories That Inspire

    See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.