
SwiftUI, Jetpack Compose는 Apple, Google에서 각각 제공하는 모던한 UI개발 프레임워크로서 선언형 프로그래밍을 제공합니다. 이미 SwiftUI로 개발한 경험이 있다는 기준으로 설명하려 합니다.(제가 그렇거든요..)
kotlin과 swift의 문법적 차이점은 이 글에선 생략하고 가장 많이 사용할듯한 UI콤포넌트에 대해서 비교정리를 해보겠습니다. 그리고 상태처리에 관련된 내용도 일부 알아보겠습니다.
Component
・앱내에 표시되는 UI정의된 소스코드입니다.
・SwiftUI에서는 View, Jetpack Compose에서는Composable 이라고 합니다.
State
・앱의 상태를 나타냅니다.
・감시대상Observable이고 State갱신가 갱신되면 UI를 다시 그리는(reDraw)하는 트리거역할을 합니다.
| SwiftUI Component | Jetpack Compose Component | 설명 |
|---|---|---|
| Text | Text | 텍스트 표시 |
| Image | Image | 이미지표시 |
| Button | Button | 버튼액션 제어 |
| VStack/HStack/ZStack | Column/Row/Box | 레이아웃(세로, 가로 , 중첩) |
| TextField | TextField | 텍스트입력 제어 |
| Slider | Slider | 슬라이더입력 제어 |
| Toggle | Switch | 토글스위치 제어 |
| Picker | DropdownMenu | 선택리스트 제어 |
| List | LazyColumn | 스크롤 가능한 리스프표시 |
| NavigationView | NavHost | 네이게이션 제어 |
| TabView | TabRow | 탭 제어 |
| Jetpack Component | 설명 |
|---|---|
| Scaffold | Material Design기본적인 레이아웃구조를 제공 툴바, Drawer(옆으로 나타나는 메뉴), 플로팅 액션버튼, 스낵바등 을 관리 |
| TopAppBar | 앱 상단에 배치되는 툴바 |
| BottomAppBar | 네비게이션이나 액션을 위한 앱 하단에 배치되는 툴바 |
| FloatingActionButton | 주요액션을 실행하도록 배치된 특수버튼 |
| Snackbar | 일시적인 메세지를 표시하기위한 위젯 |
| BackdropScaffold | Material Design에서 Drawer(옆 메뉴표시)제공하는 콤포넌트 |
| BottomSheetScaffold | 앱바를 포함한 Material Design 템플릿 컴포넌트 |
Text(
text = "Hello, World!", // 출력할 텍스트를 지정합니다.
fontSize = 20.sp, // 텍스트의 크기를 지정합니다.
fontWeight = FontWeight.Bold, // 텍스트의 두께를 지정합니다.
textAlign = TextAlign.Center // 텍스트를 중앙 정렬합니다.
)
// Image 컴포넌트를 사용하여 로컬 리소스의 이미지를 출력합니다.
Image(
painter = painterResource(R.drawable.my_image), // Replace 'my_image' with your actual image resource
contentDescription = "This is a sample image" // Provide a description for the image for accessibility
)
val count = remember { mutableStateOf(0) }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(onClick = { count.value++ }) {
Text(text = "Button Clicked: ${count.value} times!")
}
}
Column {
Text("Column Item 1")
Text("Column Item 2")
Row {
Text("Row Item 1")
Text("Row Item 2")
Box(contentAlignment = Alignment.Center) {
Text("Boxed Item")
}
}
}
// remember를 사용하여 사용자 입력을 저장하는 상태를 생성합니다.
val text = remember { mutableStateOf("") }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
TextField(
value = text.value,
onValueChange = { newValue -> text.value = newValue },
label = { Text("Enter text here") }
)
}
// remember를 사용하여 슬라이더의 값을 저장하는 상태를 생성합니다.
val sliderValue = remember { mutableStateOf(0f) }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Slider(
value = sliderValue.value,
onValueChange = { newValue -> sliderValue.value = newValue },
valueRange = 0f..100f,
)
Text(text = "Slider value: ${sliderValue.value}")
}
// remember를 사용하여 스위치의 상태를 저장하는 상태를 생성합니다.
val switchState = remember { mutableStateOf(false) }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Switch(
checked = switchState.value,
onCheckedChange = { newState -> switchState.value = newState },
)
Text(text = if(switchState.value) "Switch is on" else "Switch is off")
}
// remember를 사용하여 드롭다운 메뉴의 오픈 상태와 선택된 항목을 저장하는 상태를 생성합니다.
val isOpen = remember { mutableStateOf(false) }
val selectedItem = remember { mutableStateOf("") }
val items = listOf("Item 1", "Item 2", "Item 3")
Box(
modifier = Modifier
.fillMaxSize()
.clickable(onClick = { isOpen.value = true }),
contentAlignment = Alignment.Center
) {
Text(text = if(selectedItem.value.isNotEmpty()) "Selected: ${selectedItem.value}" else "Open dropdown")
DropdownMenu(
expanded = isOpen.value,
onDismissRequest = { isOpen.value = false }
) {
items.forEach { label ->
DropdownMenuItem(onClick = {
selectedItem.value = label
isOpen.value = false
}) {
Text(text = label)
}
}
}
}
// 항목들을 리스트로 생성합니다.
val items = List(100) { "Item #$it" }
LazyColumn {
items(items) { item ->
Text(text = item)
}
}
// remember를 사용하여 현재 선택된 탭의 인덱스를 저장하는 상태를 생성합니다.
val selectedTabIndex = remember { mutableStateOf(0) }
val tabTitles = listOf("Tab 1", "Tab 2")
Column {
TabRow(selectedTabIndex = selectedTabIndex.value) {
tabTitles.forEachIndexed { index, title ->
Tab(
selected = selectedTabIndex.value == index,
onClick = { selectedTabIndex.value = index }
) {
Text(text = title)
}
}
}
when (selectedTabIndex.value) {
0 -> Text("This is Tab 1 content")
1 -> Text("This is Tab 2 content")
}
}
복잡한 화면의 경우 Component가 네스트가 깊어지는데 읽기 어려워지기도 하고 성능면에서도 좋지 않다고 하니 부품화(별도 클래스로 분리)하여 사용하는것이 좋다고 합니다.
주로 Component를 꾸며야할때 사용하며 이것또한 복잡해질경우 부품화하여 사용하는것이 좋습니다.
SwiftUI
struct ContentView: View {
var body: some View {
Text("Text")
.background(Color.blue)
.border(.red)
}
}
Jetpack Compose
@Composable
fun Content() {
Text(
modifier = Modifier
.background(Color.Blue)
.border(BorderStroke(width = 1.dp, color = Color.Red)),
text = "Text"
)
}
SwiftUI는 중앙에, Jetpack Composes는 좌측상단에 대치됩니다.
・VStack -> 중앙
・Column -> 좌측상단

View에 직접 적용합니다. Modifier는 적용후 View를 반환해 주기 때문입니다. 그결과 Modifier를 쓴 순서대로 반영되는 이미지 입니다. (앞에 Modifier는 뒤에 Modifier에 영향을 받지 않습니다.)struct ContentView: View {
var body: some View {
Text("Text")
.background(Color.blue)
.frame(width: 100, height: 100)
.border(.red)
}
}

Modifier를 한꺼번에 Composable에 적용하기 때문에 앞에 Modifier는 뒤에 Modifier에 영향을 받게 됩니다.@Composable
fun Content() {
Text(
modifier = Modifier
.background(Color.Blue)
.size(100.dp)
.border(BorderStroke(width = 1.dp, color = Color.Red)),
text = "Text"
)
}

정의된 콤포넌트는 직접 State상태를 가지지 않습니다. Jetpack Compose는 메소드(@Composable)로 Component를 정의 합니다.SwiftUI는 구조체 (struct)로 정의합니다. 화면을 다시그릴때마다 이전 화면을 파기하고 다시 생성합니다. 그렇기때문에 State는 다른영역에 저장하며 그것을 "상태를 관리한다" 라고 합니다.
SwiftUI
View상에서 PropertyWrapper(@State, @StateObject, etc...)로 정의 합니다.
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack {
Text("\(count) Click")
Button("Click Me!") {
// update state
count += 1
}
}
}
}
Jetpack Compose
Composable상에서rememberComposable를 사용State<T>를 정의합니다.
*AAC에ViewModel를 사용하여State를 모아서 관리하는경우는 viewModel()에서 ViewModel를 Composable연결합니다.
@Composable
private fun Content() {
// rememberComposable를 사용하여State<T>를 별도 메모리에 보존
var count: Int by remember { mutableStateOf(0) }
Column {
Text(text = "${count} Click")
Button(
// update state
onClick = { count += 1 }
) {
Text(text = "Click Me!!")
}
}
}
각 화면의Component는State갱신에 따라 몇번이고 재갱신될수 있습니다. 이런 반복처리를 효율적으로 처리하기 위해 State에 의존하는 Component는 변경된 부분만을 처리하게 되어 있습니다.
다만, 몇가지 주의할점은
1. Component를 다시그릴때를 위해 경량화, 즉 외부 API, DB 관련처리는 별도 스래드로 처리해야 합니다.
2. 몇번이도 재실행되더라도 Component의 표시결과는 변하지 않도록 해야합니다.
3. 다만, 만약 라이프사이클에 의존하는 처리(실행시 결과가 달라지는)는 별도 처리가 필요합니다.
경우에 따라Component를 표시, 숨김등의 이벤트조건에 따라 처리를 해줘야 할 필요가 있다면 아래와 각각 아래와 같이 처리합니다.
SwiftUI
onAppear()onDisappear()Combine에Publisher가 갱신될 때: onReceive()struct ContentView: View {
var body: some View {
VStack {
// ...
}.onAppear {
print("show View")
}.onDisappear {
print("hidden View")
}.onReceive(publisher) { value in
print("update Publisher")
}
}
}
Jetpack Compose
・Composable보여질때: LaunchedEffect(Unit)
・Composable안보여질때: DisposableEffect(Unit) { onDispose {} }
@Composable
fun Content() {
LaunchedEffect(Unit) {
print("show Composable")
}
DisposableEffect(Unit) {
print("show Composable")
onDispose {
print("hidden Composable")
}
}
}
SwiftUI는 양방향 바인딩을 허용하는 반면, Jetpack Compose는 단방향바인딩을 목적으로 만들어져 있습니다.
TextField를 예로 들어보겟습니다.
SwiftUI
// 부모 View
struct ContentView: View {
// 입력값을 부모View에State보존
@State private var name = ""
var body: some View {
VStack {
//자식View
// text입력에 부모뷰가 가지고 있는State의 참조를($)넘김
// 키보드 입력으로 State의 값이 변경됨
TextField("Name", text: $name)
}
}
}
Jetpack Compose
// 부모 Composable
@Composable
fun Content() {
Column {
// 입력값을 부모Composable에서State보존
var name by remember { mutableStateOf("") }
// 자식Composable
TextField(
// 입력란 문자지정
value = name,
// 콜백에서 새로운 입력값을 받음
// 콜백이 오지 않으면 입력이 되더라도 State값 갱신은 되지않음
// State의 갱신방법을 직접 해줌. (자식은 관여하지 않음)
onValueChange = { changedName
name = changedName
},
label = { Text("Name") }
)
}
}
이러한 차이로 MVVM을 이용한ViewModel에서의 State관리클래스설계가 달라집니다.SwiftUI의 경우ViewModel에 보존하는State를 직접 갱신하기 때문에 대상 State를 외부로부터 변경가능하게 해야하지만, Jetpack Compose는 대상State가 외부에서는 읽기전용으로 해두고 콜백에서 갱신하는 별도 처리가 필요합니다.
SwiftUI
final class SampleViewModel: ObservableObject {
// Publisher로State를 보존
@Published var name : String = ""
}
struct ContentView: View {
@StateObject var viewModel = SampleViewModel()
var body: some View {
TextField("Name", text: $viewModel.name)
}
}
Jetpack Compose
class SampleViewModel : ViewModel() {
// MutableStateFlow의State를Private로 보존
private val _name = MutableStateFlow("")
// 외부에선StateFlow를 읽기전용으로 함
val name: StateFlow<String> = _name
// 콜백 메소드는 공개
fun onNameChange(name: String) {
// 콜백으로 State를 갱신
_name.value = name
}
}
@Composable
private fun Content(
viewModel: SampleViewModel = viewModel()
) {
// StateFlow의State로 변환
val name: String by viewModel.name.collectAsState()
TextField(
value = name,
// 키보드 입력시 ViewModel의 콜백메소드를 호출
onValueChange = viewModel::onNameChange,
)
}