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).
If using Compose for iOS are you using it for:
— John O'Reilly (@joreilly) May 5, 2023
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.
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
Related tweet
Wrote a short article about recent exploration of using Compose for sharing a UI component within a SwiftUI screen. Still feel there might be better approach to doing this and perhaps article might help trigger further investigation. https://t.co/pj4aDXxKjs #iOSDev #KMM
— John O'Reilly (@joreilly) May 7, 2023