SwiftUI to Jetpack Compose (and vice vera): Reference Guide
I wrote a minimal guide on converting SwiftUI code to equivalent Jetpack Compose code (and vice versa) in my last article; “SwiftUI vs Jetpack Compose: A Guide for Cross-Platform UI Development”.
This will be a more comprehensive guide broken down into different sections which should be able to act as a cheat sheet when jumping between both frameworks.
Topics covered in this article are Layout Elements, Text + Styling, Lists, Images + Icons, Spacer, Button, making a view clickable, state management, triggering events on screen load and pull to refresh. The focus here again is on code rather than theory, lets get right to it.
Layout Elements
These are the UI building blocks on both frameworks, they work quite similarly (refer to my previous article for UI element code references).
Almost every app will need a scrollable view, in SwiftUI you can declare a ScrollView with the orientation in the constructor as shown in the table above, but in Jetpack Compose you can add the .verticalScroll or .horizontalScroll modifier to any Column, Row, or Box.
Text + Styling
You can declare a Text in both frameworks like this: Text(“Hello World”)
However, styling is done differently. In Jetpack Compose, most of the styling can be done via constructor parameters, but in SwiftUI, it is typically handled using modifiers.
Here is some code for better visualisation:
SwiftUI
Text("Styled Text")
.font(.system(size: 24))
.fontWeight(.bold)
.foregroundColor(.blue)
.italic()
.underline()
.multilineTextAlignment(.center)
Jetpack Compose
Text(
text = "Styled Text",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color.Blue,
fontStyle = FontStyle.Italic,
textDecoration = TextDecoration.Underline,
textAlign = TextAlign.Center
)
Lists
Both frameworks provide very easy implementations of handling Lists. A key to note here is that by default Lists are scrollable, so you don’t need to specify any scrolling modifiers to them.
SwiftUI
In SwiftUI you have the option of directly providing the array of items to the List
constructor. But what if you want to add a heading, or some other UI elements which doesn’t need to be repeated? In that case, you can user a ForEach
inside the List
.
// List only
struct ContentView: View {
let items = ["Apple", "Banana", "Cherry"]
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
}
}
// List view with heading
struct ContentView: View {
let items = ["Apple", "Banana", "Cherry"]
var body: some View {
List {
Text("Fruits")
.font(.headline)
ForEach(items, id: \.self) { item in
Text(item)
}
}
}
}
Jetpack Compose
In Compose, you have to provide data to a LazyColumn
via item { }
or items(list) { }
@Composable
fun ContentView() {
val items = listOf("Apple", "Banana", "Cherry")
LazyColumn {
item {
Text(
text = "Fruits",
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
}
items(items) { item ->
Text(item)
}
}
}
Images + Icons
Both frameworks provide powerful ways to display images and icons. They allow you to load system icons, local images, and apply common modifiers like resizing, coloring, and adding frames.
In case of Vectors, SwiftUI provides the rich SF Symbols collection which contains 100s of cool icons out of the box, but Material Icons are very minimal and limited. When I say Material Icons are limited, I mean LIMITED, there are hardly a handful of material icons which I ended up using.
Here is some code for good measure:
SwiftUI
// Icon
Image(systemName: "star.fill")
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.yellow)
// From resources
Image("my_image")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
Jetpack Compose
// Icon
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = Color.Yellow
)
// From resources
Image(
painter = painterResource(id = R.drawable.my_image),
contentDescription = null,
modifier = Modifier
.size(100.dp)
.aspectRatio(1f),
contentScale = ContentScale.Fit
)
Spacer
Spacer
can a pretty powerful component if used with care. Both frameworks provide a Spacer
with similar implementations although the Compose implementation always requires a Modifier
.
Button
Let’s explore how you can style buttons and handle actions.
SwiftUI
Button(action: {
print("Button tapped")
}) {
Text("Press Me")
.background(Color.blue)
.foregroundColor(.white)
}
Jetpack Compose
Button(
onClick = { println("Button tapped") },
modifier = Modifier.padding(16.dp)
) {
Text("Press Me")
}
Making a View Clickable
In both frameworks views can be made clickable by adding appropriate modifiers. While SwiftUI uses the .onTapGesture()
modifier, Jetpack Compose uses the Modifier.clickable()
.
SwiftUI
Text("Click Me")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.onTapGesture {
print("View clicked")
}
Jetpack Compose
Text(
text = "Click Me",
modifier = Modifier
.padding(16.dp)
.background(Color.Blue)
.clickable {
println("View clicked")
}
)
State Management
State management is a complex topic that varies depending on whether you’re handling local UI state, shared state management using a ViewModel, or app-wide state management with tools like @EnvironmentObject
in SwiftUI or Hilt/Dagger
in Jetpack Compose.
For this article I’ll stick with providing the closest equivalents. Let me know if you are interested in a more detailed article around state management.
Here is a minimal code example of UI state management in both
SwiftUI TextField
@State private var text: String = ""
TextField("Enter text", text: $text)
Jetpack Compose TextField
val text = remember { mutableStateOf("") }
TextField(
value = text.value,
onValueChange = { text.value = it },
label = { Text("Enter text") }
)
Side-Effects and One-Time Events
Handling side-effects (like API calls or one-time events) in both frameworks has parallels. In Jetpack Compose, you can use LaunchedEffect
and in SwiftUI, onAppear
is commonly used to trigger such effects.
Here is a minimal example on how you can trigger an action when the screen is launched. Keep in mind there are other things to consider here, an important one is that these will be called when navigating back to an already created view as well. So you need well defined state management for these scenarios, in most cases you would use a ViewModel
.
SwiftUI
struct ContentView: View {
@State private var data: String = ""
var body: some View {
Text(data)
.onAppear {
fetchData()
}
}
func fetchData() {
data = "Data fetched"
}
}
Jetpack Compose
@Composable
fun ContentView() {
var data by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
data = "Data fetched"
}
Text(data)
}
Pull to Refresh
Pull to Refresh is a common mobile pattern that allows users to refresh content by pulling down on a scrollable view.
- SwiftUI: As of iOS 15, SwiftUI provides native support for pull to refresh with the
.refreshable()
modifier. This modifier can be added to aList
orScrollView
to get pull to refresh functionality out of the box. - Jetpack Compose: Pull to refresh handling in Compose is a bit more complex, you need to add the
material
library dependency (in addition tomaterial3
). You would then createrememberPullRefreshState
and add a.pullRefresh(pullRefreshState)
modifier to a Box to achieve this. I would suggest you look at other articles for a step by step guide.
SwiftUI
List(items) { item in
Text(item)
}
.refreshable {
// Refresh action here
viewModel.loadData()
}
Jetpack Compose
val pullRefreshState = rememberPullRefreshState(
refreshing = isLoading,
onRefresh = { viewModel.refreshData() }
)
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
LazyColumn {
items(data)
}
PullRefreshIndicator(
refreshing = isLoading,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
Happy coding! 💻