티스토리 뷰
SwiftUI와 UIKit은 iOS 애플리케이션을 개발하는 데 있어 각각 다른 철학과 접근 방식을 가지고 있다.
UIKit에서는 Auto Layout이 UI 개발의 핵심이었다. Auto Layout은 제약 조건을 사용하여 뷰의 크기와 위치를 동적으로 설정하고 다양한 화면 크기와 기기에서 레이아웃을 유연하게 구성하는 방식이었다. Auto Layout은 특히 스토리보드와 코드로 제약 조건을 설정하는 데 있어 기본 중의 기본이라 할 수 있다.
UIKit에서 Auto Layout은 복잡한 레이아웃을 처리하는 데 매우 유용했지만, 그만큼 제약 조건을 설정하는 과정이 복잡해졌다. 제약 조건을 일일이 정의해야 하는 번거로움 때문에 유지보수도 어렵고, 직관적인 UI 개발이 쉽지 않은 경우도 있었다.
SwiftUI는 선언적 프로그래밍 패러다임을 채택하여 Auto Layout이라는 개념을 사용하지 않는다고 한다. SwiftUI에서는 개발자가 직접 제약 조건을 정의하는 대신, SwiftUI 자체가 내부적으로 레이아웃을 자동으로 계산하고 관리한다. 이 선언적 방식을 통해 UI 요소들이 자연스럽게 부모 뷰와 자식 뷰 간의 상호작용을 통해 크기와 위치가 결정되고, UIkit보다 간단하고 명료한 코드를 유지한다.
그럼 SwiftUI에서는 Auto Layout 없이 어떻게 레이아웃을 구현하는지에 대해 알아보자.
1. SwiftUI와 Auto Layout의 차이
SwiftUI
다음과 같이 엄청 간단한 예제로 이해해 보자.
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
Text만 사용했는데 Hello World가 정중앙에 표시되는 걸 확인할 수 있다.
여기서 SwiftUI의 레이아웃 방식에 대해 한번 알아보고 가보자.
✏️ SwiftUI가 그리는 레이아웃 방식
1. 부모 뷰는 자식 뷰에게 자신의 사이즈를 제안한다.
2. 자식 뷰는 부모의 사이즈와 함께 자신의 display contents를 고려해서 자신의 사이즈를 정한다. 그리고 부모에게 알려준다.
3. 부모는 자식이 결정한 사이즈를 가지고 자식 뷰를 자신의 좌표공간에 놓는다. 기본적으로 가운데에 놓는다.
그럼 위의 예제를 적용해서 다시 확인해 보면,
- 부모(ContentView)는 전체 화면의 크기를 자식(Text("Hello World"))에게 제안한다.
- 자식(Text)은 자신의 컨텐츠("Hello World")를 표시할 수 있는 최소한의 크기를 계산하고 부모에게 알려준다.
- 부모는 자식이 결정한 크기를 바탕으로 자식을 자신의 좌표 공간에서 중앙에 배치한다.
UIkit + AutoLayout
UIkit에서는 자식 뷰의 크기나 위치를 명시적으로 설정하거나, 제약 조건을 수동으로 설정해야 한다.
뷰를 중앙에 배치하는 간단한 작업도 여러 단계의 코드를 작성해야 하며, 선언적 방식("무엇을")이 아니라 "어떻게" 레이아웃을 구성할지 구체적으로 명시해야 한다.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 1. UILabel 생성
let label = UILabel()
label.text = "Hello World"
label.translatesAutoresizingMaskIntoConstraints = false
// 2. UILabel을 부모 뷰에 추가
view.addSubview(label)
// 3. 제약 조건 추가: 화면 중앙에 배치
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor), // 가로 중앙
label.centerYAnchor.constraint(equalTo: view.centerYAnchor) // 세로 중앙
])
}
}
- Auto Layout에서는 UILabel을 생성한 후에, 제약 조건을 수동으로 추가해야 한다. centerXAnchor, centerYAnchor를 사용해 UILabel을 부모 뷰의 중앙에 배치해야 한다.
- translatesAutoresizingMaskIntoConstraints = false를 명시적으로 설정하여 UIKit의 자동 제약 해제를 수행한다.(Auto Layout을 사용하는 경우에 false로 설정해야 함)
SwiftUI vs UIkit + AutoLayout
차이점 1: 레이아웃 계산 방식
- SwiftUI: 자식 뷰가 자신의 컨텐츠에 맞춰 자동으로 크기를 결정
이 과정은 부모가 자식에게 공간을 제안하고, 자식이 적절한 크기를 선택한 후 부모가 다시 그 뷰를 배치하는 방식으로 진행된다. SwiftUI는 이러한 과정을 자동으로 처리하여, 크기와 위치를 결정한다.
- UIKit (Auto Layout): Auto Layout은 제약 조건을 해결하는 수학적 시스템을 사용
제약 조건은 부모-자식 간의 관계뿐만 아니라, 여러 뷰들 사이의 관계를 나타내며, 이는 때때로 예기치 못한 결과를 초래할 수 있다.
부모는 자식 뷰의 크기와 위치를 제약 조건에 따라 배치한다.
차이점 2: 코드의 양과 복잡성
SwiftUI에서는 단일한 코드 블록으로 UI를 간결하게 정의할 수 있지만, UIKit의 Auto Layout에서는 여러 단계를 거쳐야 레이아웃을 완성할 수 있다. 위의 UIKit 예제에서는 UILabel을 화면 중앙에 배치하기 위해 NSLayoutConstraint를 사용하고, 각 앵커(centerXAnchor, centerYAnchor)에 수동으로 제약을 설정해야 했다.
SwiftUI에서 간단하게 한 줄로 쓸 수 있었던 Text("Hello World")가 UIKit에서는 여러 줄의 코드를 필요로 하는 것을 확인할 수 있다.
2. SwiftUI Layout 시스템
다음과 같은 뷰를 만든다고 해보자.
윗부분을 ProfileTopView, 아래 부분을 ProfileBottomView로 나누고 루트 뷰는 ContentView이다.
- 전체 코드 확인
struct ContentView: View {
var body: some View {
VStack {
// 상단에 사용자 정보와 프로필 이미지
ProfileTopView()
// 내 게시글 섹션
ProfileBottomView()
}
}
}
struct ProfileTopView: View {
var body: some View {
VStack(spacing: 16) {
// 프로필 이미지와 수정 버튼
ZStack {
Image(systemName: "person.crop.circle.fill") // 기본 아이콘
.resizable()
.frame(width: 100, height: 100)
Image(systemName: "camera.fill") // 카메라 아이콘
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(.blue)
.background(Circle().fill(Color.white))
.offset(x: 35, y: 35)
}
// 사용자 별명과 이름 수정 버튼
Text("붕어빵")
.font(.title2)
.fontWeight(.semibold)
Button(action: {
// 이름 수정 액션
}) {
Text("이름 수정하기")
.font(.subheadline)
.padding(8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
.padding(.top, 32)
// 게시물, 팔로워, 팔로잉 통계
HStack(spacing: 34) {
VStack {
Text("17")
.font(.title)
Text("게시물")
.font(.caption)
}
Divider()
.frame(height: 36)
VStack {
Text("20")
.font(.title)
Text("팔로워")
.font(.caption)
}
Divider()
.frame(height: 36)
VStack {
Text("20")
.font(.title)
Text("팔로잉")
.font(.caption)
}
}
.padding(.vertical, 16)
}
}
struct ProfileBottomView: View {
var body: some View {
VStack(spacing: 16) {
Text("내 게시글")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(20)
Spacer()
// 게시글 없음 표시
VStack(spacing: 20){
Image(systemName: "face.smiling")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.gray)
Text("아직 작성된 글이 없어요")
.font(.subheadline)
.foregroundColor(.black)
}
Spacer()
}
.background(Color(.lightGray))
}
}
SwiftUI가 그리는 레이아웃 방식에 따라 이해해 보자.
1. 부모 뷰는 자식 뷰에게 자신의 사이즈를 제안한다.
ContentView에서 최상위 부모 뷰는 VStack이다.
VStack은 화면 전체를 가로지르는 레이아웃을 차지하고 있고, 자식 뷰인 ProfileTopView와 ProfileBottomView에게 부모로서 전체 화면의 가능한 크기를 제안한다. 이 가용 크기는 주로 화면의 너비와 높이이다.
부모 뷰인 VStack은 두 자식 뷰에게 각각 사용할 수 있는 전체 가로 공간을 제안하며, 각 자식 뷰는 이 공간에서 자신의 컨텐츠에 맞는 크기를 정하게 된다.
struct ContentView: View {
var body: some View {
VStack {
// 상단에 사용자 정보와 프로필 이미지
ProfileTopView()
// 내 게시글 섹션
ProfileBottomView()
}
}
}
2. 자식 뷰는 부모의 사이즈와 함께 자신의 display contents를 고려해서 자신의 사이즈를 정한다. 그리고 부모에게 알려준다.
ProfileTopView 내부에서:
- ZStack: 두 개의 이미지를 겹쳐서 배치한다. 첫 번째 이미지는 크기가 100x100이고, 두 번째 카메라 아이콘은 24x24 크기로 설정되었다. 자식 뷰인 이 이미지들은 자신의 컨텐츠 크기에 맞춰 부모가 제안한 가로 공간 중 일부만 차지한다.
- Text("붕어빵"): 텍스트는 자신의 컨텐츠 크기(텍스트의 길이와 폰트 크기)를 기준으로 적절한 크기를 결정한다.
- Button: 버튼도 내부의 텍스트 컨텐츠와 패딩 값에 따라 버튼의 크기가 결정된다. SwiftUI는 자동으로 적절한 크기를 설정한 후 부모에게 이를 알려준다.
// 프로필 이미지와 수정 버튼
ZStack {
Image(systemName: "person.crop.circle.fill") // 기본 아이콘
.resizable()
.frame(width: 100, height: 100)
Image(systemName: "camera.fill") // 카메라 아이콘
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(.blue)
.background(Circle().fill(Color.white))
.offset(x: 35, y: 35)
}
ProfileBottomView에서:
- Text("내 게시글"): 마찬가지로 부모가 제안한 전체 가로 공간을 받고, 텍스트 컨텐츠에 맞춰 적절한 크기로 텍스트를 그린다.
- VStack: 그 안에 이미지(Image(systemName: "face.smiling"))와 텍스트("아직 작성된 글이 없어요")를 배치하며, 자식 뷰인 이미지도 자신의 컨텐츠 크기(100x100)에 맞춰 크기를 결정하고, 텍스트는 자체 길이와 폰트 크기에 맞춰 최종 크기를 선택한다.
VStack(spacing: 16) {
Text("내 게시글")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(20)
Spacer()
// 게시글 없음 표시
VStack(spacing: 20){
Image(systemName: "face.smiling")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.gray)
Text("아직 작성된 글이 없어요")
.font(.subheadline)
.foregroundColor(.black)
}
Spacer()
}
3. 부모는 자식이 결정한 사이즈를 가지고 자식 뷰를 자신의 좌표 공간에 놓는다. 기본적으로 가운데에 놓는다.
ContentView에서 VStack의 자식인 ProfileTopView와 ProfileBottomView는 2번 단계에서 각자 자신의 컨텐츠에 맞는 사이즈를 결정하고, 그 정보를 부모 뷰인 VStack에 전달한다. 이후, 부모 뷰인 VStack은 자식 뷰들이 결정한 사이즈를 바탕으로 ProfileTopView를 위에, ProfileBottomView를 아래에 배치한다.
VStack은 수직으로 뷰를 정렬하는 레이아웃 방식이기 때문에, 각 자식 뷰는 결정된 사이즈만큼의 공간을 차지하고, 순서대로 쌓여서 화면에 표시된다.
📍 만약 UIkit + Auto Layout으로 구현한다면?
SwiftUI에서는 VStack 안에 ProfileTopView와 ProfileBottomView를 넣으면 자동으로 수직으로 정렬되었다.
그래서 수직 정렬하기 위한 제약조건을 명시할 필요가 없었다.
VStack {
ProfileTopView()
ProfileBottomView()
}
근데 Auto Layout을 사용해서 구현했다면, 개발자가 각 뷰의 크기, 위치, 그리고 제약 조건을 명시적으로 설정해야 한다. 레이아웃이 복잡해질수록 각 뷰에 대한 제약을 수동으로 추가해야 하므로, 코드가 더 길고 복잡해진다.
예를 들어, ProfileTopView와 ProfileBottomView를 수직으로 정렬하려면, 각 뷰의 위치를 명시적으로 정의해야 하며, 각각의 뷰에 대해 상하 제약 조건을 설정해야 한다.
🔎 Auto Layout을 사용할 때 필요한 작업:
1. 각 뷰의 크기를 설정하거나, 부모 뷰의 크기에 맞게 유연한 크기를 지정해야 한다.
2. 뷰들의 위치를 설정하기 위해, 상단, 하단, 좌우 여백에 대한 제약을 설정해야 한다.
3. 부모 뷰와의 상대적인 위치(예: 다른 뷰와의 간격)를 정의해야 한다.
// Auto Layout 방식으로 ProfileTopView를 배치
view.addSubview(profileTopView)
profileTopView.translatesAutoresizingMaskIntoConstraints = false
profileTopView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
profileTopView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
profileTopView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// ProfileBottomView를 배치
view.addSubview(profileBottomView)
profileBottomView.translatesAutoresizingMaskIntoConstraints = false
profileBottomView.topAnchor.constraint(equalTo: profileTopView.bottomAnchor, constant: 16).isActive = true
profileBottomView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
profileBottomView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
와~~ 코드 엄청 길어졌다,,,, ㅎㅎㅎㅎ
확실히 SwiftUI로 구현하는 것보다 UIkit가 수동으로 구현해야 하는 것이 많다는 걸 느꼈다.
참고
https://kean.blog/post/swiftui-layout-system
https://developer.apple.com/videos/play/wwdc2019/237/
'스위프트' 카테고리의 다른 글
[Swift] - RxSwift가 무엇인가? (0) | 2024.10.16 |
---|---|
[Swift] - UnitTest 예제(Given-When-Then 패턴) (0) | 2024.10.01 |
[Swift] - TestFlight로 배포해보기 (0) | 2024.08.23 |
[Swift] - iOS 프로젝트 배포 환경별 build 세팅 (2) | 2024.08.23 |
[Swift] - UserDefaults, Keychain, Core Data (0) | 2024.05.07 |