SwiftUI vs Jetpack Compose 비교하기

JaeEun Lee·2023년 7월 30일

SwiftUI & Jetpack compose

목록 보기
1/10
post-thumbnail

들어가기전에

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
설명
TextText텍스트 표시
ImageImage이미지표시
ButtonButton버튼액션 제어
VStack/HStack/ZStackColumn/Row/Box레이아웃(세로, 가로 , 중첩)
TextFieldTextField텍스트입력 제어
SliderSlider슬라이더입력 제어
ToggleSwitch토글스위치 제어
PickerDropdownMenu선택리스트 제어
ListLazyColumn스크롤 가능한 리스프표시
NavigationViewNavHost네이게이션 제어
TabViewTabRow탭 제어

Jetpack Compose만 있는것들

Jetpack
Component
설명
ScaffoldMaterial Design기본적인 레이아웃구조를 제공
툴바, Drawer(옆으로 나타나는 메뉴), 플로팅 액션버튼, 스낵바등 을 관리
TopAppBar앱 상단에 배치되는 툴바
BottomAppBar네비게이션이나 액션을 위한 앱 하단에 배치되는 툴바
FloatingActionButton주요액션을 실행하도록 배치된 특수버튼
Snackbar일시적인 메세지를 표시하기위한 위젯
BackdropScaffoldMaterial Design에서 Drawer(옆 메뉴표시)제공하는 콤포넌트
BottomSheetScaffold앱바를 포함한 Material Design 템플릿 컴포넌트

기본 콤포넌트 사용법(Jetpack Compose)

Text


Text(
    text = "Hello, World!", // 출력할 텍스트를 지정합니다.
    fontSize = 20.sp, // 텍스트의 크기를 지정합니다.
    fontWeight = FontWeight.Bold, // 텍스트의 두께를 지정합니다.
    textAlign = TextAlign.Center // 텍스트를 중앙 정렬합니다.
)

Image

// 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
)

Button

val count = remember { mutableStateOf(0) }
Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
) {
    Button(onClick = { count.value++ }) {
    Text(text = "Button Clicked: ${count.value} times!")
    }
}

Column/Row/Box

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")
        }
    }
}

TextField

// 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") }
    )
}

Slider

// 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}")
}

Switch

// 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)
                }
            }
        }
    }

LazyColumn

// 항목들을 리스트로 생성합니다.
 val items = List(100) { "Item #$it" }

LazyColumn {
    items(items) { item ->
        Text(text = item)
    }
}

TabRow

// 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가 네스트가 깊어지는데 읽기 어려워지기도 하고 성능면에서도 좋지 않다고 하니 부품화(별도 클래스로 분리)하여 사용하는것이 좋다고 합니다.

Modifier체이닝(.으로 계속 연결하며 사용하는거)이 가능하다.

주로 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"
   )
}

Component기본(default)위치가 다르다

SwiftUI는 중앙에, Jetpack Composes는 좌측상단에 대치됩니다.

・VStack -> 중앙
・Column -> 좌측상단

Modifier사용법

  • SwiftUI는Modifier를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)
   }
}

  • Jetpack Compose는 체이닝한 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를 관리는?

정의된 콤포넌트는 직접 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()에서 ViewModelComposable연결합니다.

@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!!")
       }
   }
}

State변화에 따른 UI자동갱신

각 화면의Component는State갱신에 따라 몇번이고 재갱신될수 있습니다. 이런 반복처리를 효율적으로 처리하기 위해 State에 의존하는 Component는 변경된 부분만을 처리하게 되어 있습니다.
다만, 몇가지 주의할점은
1. Component를 다시그릴때를 위해 경량화, 즉 외부 API, DB 관련처리는 별도 스래드로 처리해야 합니다.
2. 몇번이도 재실행되더라도 Component의 표시결과는 변하지 않도록 해야합니다.
3. 다만, 만약 라이프사이클에 의존하는 처리(실행시 결과가 달라지는)는 별도 처리가 필요합니다.

라이프사이클을 다루는 API가 존재한다.

경우에 따라Component를 표시, 숨김등의 이벤트조건에 따라 처리를 해줘야 할 필요가 있다면 아래와 각각 아래와 같이 처리합니다.

SwiftUI

  • View가 보여질때: onAppear()
  • View 안보여질때: onDisappear()
  • CombinePublisher가 갱신될 때: onReceive()
    • 유저조작에 의해 state를 변경시키거나 외부로부터 값(state)이 변경되었을때를 말한다.
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,
   )
}

참고링크

profile
공업철학프로그래머

0개의 댓글