SwiftUI to Jetpack Compose (and vice vera): Reference Guide

Omar Mujtaba
5 min readSep 18, 2024

--

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 a List or ScrollView 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 to material3). You would then create rememberPullRefreshState 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! 💻

--

--

Omar Mujtaba
Omar Mujtaba

Written by Omar Mujtaba

Mobile Developer from Sydney, specializing in Native Android (Kotlin/Java) and iOS (Swift) apps, with experience in Python for backend development.