The following recent Twitter poll captures what I believe are the main options available when using Compose in an iOS SwiftUI application. About a year ago I tried out early version of Compose for iOS in PeopleInSpace sample for the complete iOS app. More recently I explored using it for sharing an individual screen in the Confetti iOS application. Now, after recent announcement at KotlinConf that Compose for iOS had reached alpha, I was curious about what would be involved in sharing a particular UI component (implemented using Compose) within a SwiftUI screen. This is work in progress and still some issues/open questions but writing this article to capture what I’ve tried so far and potentially trigger some discussions (will update if/when there are better approaches for doing anything shown here).


This is the screen that this exploration is based on. Most of it is created using SwiftUI with Compose then being used for rendering the information for a particular speaker.

confetti screenshot

The followiing is the SwiftUI struct representing the above screen. The code to show the UI for the session itself isn’t shown as it’s not really relavent here. What we are showing is code to render the UI for the list of speakers. The fact that we’re using ScrollView here (which would be very common for screen like this) causes, as we’ll see shortly, some challenges around the sizing of the Compose based component and addressing this is why some of the additional complexity outlined below is needed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct SessionDetailsView: View {
    var session: SessionDetails

    @State var measuredHeights: [SessionDetails.Speaker: CGFloat] = [:]

    var body: some View {
        ScrollView {
            ...

            ForEach(session.speakers, id: \.self) { speaker in
                SessionSpeakerInfoViewShared(speaker: speaker.speakerDetails, measuredHeight: binding(for: speaker))
                    .frame(height: binding(for: speaker).wrappedValue)
            }
        }
    }

    private func binding(for key: SessionDetails.Speaker) -> Binding<CGFloat> {
        return .init(
            get: { self.measuredHeights[key, default: 1000] },
            set: { self.measuredHeights[key] = $0 })
    }
}

And shown below is the implementation of SessionSpeakerInfoViewShared. It delegates rendering to SessionSpeakerInfoViewController which is located within Kotlin Multiplatform shared code and implements the UI using Compose. As we’ll see shortly it invokes callback when social link button is pressed and, importantly in this context, with the measured size of the component. This is stored in the measuredHeight variable which the above SessionDetailsView code binds to and which is used then when calling frame (which ensures it’s appropriately sized within the SwiftUI screen). Important to note at this point is that the reason this is all required is due to general issue in SwiftUI that UIViewControllerRepresentable based UI requires that explicit sizing as outlined in this article.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct SessionSpeakerInfoViewShared: UIViewControllerRepresentable {
    var speaker: SpeakerDetails
    @Environment(\.openURL) var openURL
    @Binding var measuredHeight: CGFloat

    func makeUIViewController(context: Context) -> UIViewController {
        let content = SharedViewControllersKt.SessionSpeakerInfoViewController(speaker: speaker,
            onSocialLinkClicked: { urlString in
                if let url = URL(string: urlString) {
                    openURL(url)
                }
            }, heightChanged: { height in
                let scale = UIScreen.main.scale
                measuredHeight = CGFloat(height)/scale
            })

        return content
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    }
}

And finally this is the Kotlin implementation of SessionSpeakerInfoViewController which uses ComposeUIViewController to wrap around the SessionSpeakerInfo composable which is shared with the other clients. The important code here is use of onGloballyPositioned which allows us to get notified about layout information of the content which we then use to notify the SwiftUI wrapper as mentioned above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun SessionSpeakerInfoViewController(speaker: SpeakerDetails,
        onSocialLinkClicked: (String) -> Unit,
        heightChanged: (Int) -> Unit
) = ComposeUIViewController {
    MaterialTheme {
        Column(modifier = Modifier
            .onGloballyPositioned { coordinates ->
                heightChanged(coordinates.size.height)
            }
        ) {
            SessionSpeakerInfo(speaker, onSocialLinkClicked)
        }
    }
}

The full code for this exploration is included in this Confetti branch.


Featured in Android Weekly Issue #570