.frame(width: 100)이 "안 먹는" 것처럼 보이는 순간, 배경색이 글자만큼만 칠해지는 순간, GeometryReader가 화면을 다 먹어 버리는 순간이 전부 이 규칙 하나에서 나온다. 제안 → 결정 → 배치. 1부는 이 협상 규칙 자체와, 제안값을 바꾸는 두 도구 frame·fixedSize를 다룬다.1부 목표 · 18분
.padding().background()한 줄을 보고 제안·응답이 어떤 순서로 오가는지 그림으로 그린다- Auto Layout 개념을 SwiftUI 쪽 짝과 맞춘다 — intrinsic content size ↔ ideal size, hugging/compression ↔
fixedSize/layoutPriority - 뷰를 "제안을 수용하는 뷰 / 자기 크기를 고집하는 뷰"로 분류하고,
frame(width:)·frame(maxWidth:)·fixedSize()중 맞는 도구를 고른다
1. 3단계 협상 — 제안 · 결정 · 배치 🔥 핵심
SwiftUI 레이아웃은 부모와 자식 사이의 짧은 대화가 계층 전체에서 재귀적으로 반복되는 것이다. WWDC19 · Building Custom Views with SwiftUI는 이 대화를 세 단계로 설명한다.
"In SwiftUI there's no way to force a size on your child, the parent has to respect that choice." — WWDC19 · Building Custom Views with SwiftUI
→ SwiftUI에는 자식에게 크기를 강제할 방법이 없다. 부모는 자식이 고른 크기를 그대로 받아들여야 한다.
한 줄짜리 코드로 협상 따라가기
세션이 쓰는 예제를 그대로 가져온다. 모디파이어 하나하나가 감싸는 뷰(Day 1)라는 점을 떠올리면, 이 코드는 뷰 3겹이다: background → padding → Text.
struct ToastView: View {
var body: some View {
Text("Avocado Toast")
.padding(10) // 자식에게 10씩 덜 제안, 자기는 10씩 더 크게
.background(Color.green) // 레이아웃 중립: 제안을 그대로 전달
}
}background는 자기 크기가 정해진 뒤에 그 크기를 Color에 제안한다 — 그래서 배경이 글자 크기에 딱 맞는다.Auto Layout과 방향이 다르다
Auto Layout의 제약은 양방향이었다. intrinsic content size가 부모를 밀어내고, 부모의 제약이 자식을 압축하고, priority가 그 싸움을 중재했다. SwiftUI는 역할이 나뉘어 있어 싸움 자체가 없다.
final class LabelVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = "Avocado Toast"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.widthAnchor.constraint(equalToConstant: 100), // 명령
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}| 상황 | UIKit (Auto Layout) | SwiftUI |
|---|---|---|
| 크기 결정권 | 제약 시스템 전체가 푼다 | 각 뷰가 스스로 고른다 |
| 콘텐츠 크기 | intrinsicContentSize | ideal size (nil 제안에 대한 응답) |
| 늘어나지 않게 / 줄지 않게 | hugging / compression resistance priority | fixedSize(), layoutPriority(_:) |
| 제약 부족·과잉 | ambiguous / unsatisfiable 경고 | 개념이 없다 — 항상 결과가 정의된다 |
| 좌우 반전(RTL) | leading/trailing 앵커 | 자동 — 좌표를 프레임워크가 뒤집는다 |
"There are no underconstrained or overconstrained systems in SwiftUI which means everything you can express has a well-defined effect." — WWDC19 · Building Custom Views with SwiftUI
→ SwiftUI에는 제약이 모자라거나 넘치는 상태가 없다. 쓴 코드는 무엇이든 정해진 결과를 낸다. "틀린 레이아웃"은 없고 "원하지 않은 결과"만 있다.
2. 제안값과 뷰의 성격 — frame · fixedSize 🔥 핵심
ProposedViewSize — 제안에는 특별한 값 세 개가 있다
제안은 ProposedViewSize다. 폭·높이가 각각 CGFloat?이고, 부모는 특수값을 던져 자식의 유연성을 측정한다.
"Layout containers typically measure their subviews by proposing several sizes and looking at the responses." — Apple 문서 · ProposedViewSize
→ 컨테이너는 여러 크기를 제안해 보고 그 답을 모아 공간을 나눈다. 레이아웃은 "한 번 묻고 끝"이 아니다.
nil은 "제약 없이 네가 원하는 만큼"이라는 뜻이다.같은 제안, 다른 반응 — 뷰의 세 부류
레이아웃 디버깅의 절반은 "이 뷰가 제안을 받아들이는 부류인가"를 아는 것이다. 아래 코드를 Preview에 붙이면 차이가 바로 보인다.
struct ProposalDemo: View {
var body: some View {
VStack(spacing: 16) {
// ① flexible: 제안을 그대로 수용 → 정확히 150×60
Color.blue
.frame(width: 150, height: 60)
// ② 내용 기반: 자기 크기를 고른다 → frame 안에서 가운데 정렬될 뿐
Text("짧음")
.frame(width: 150, height: 60)
.border(Color.red)
// ③ 이미지 기본값: 제안을 무시하고 원래 크기
Image(systemName: "star.fill")
.frame(width: 150, height: 60)
.border(Color.green)
// ④ resizable()을 붙이면 flexible 부류로 바뀐다
Image(systemName: "star.fill")
.resizable()
.scaledToFit()
.frame(width: 150, height: 60)
.border(Color.orange)
}
}
}| 뷰 | 제안에 대한 반응 | 실무 메모 |
|---|---|---|
Color · Rectangle 등 Shape · Spacer | 제안을 전부 수용 (flexible) | 제안한 만큼 정확히 채운다 |
Text | 제안 폭 안에서 줄바꿈 → 필요한 크기 선택 | "텍스트 범위는 표시된 줄의 폭·높이를 넘지 않는다" (WWDC19) |
Image (기본) | 제안 무시 — 고정 크기 | .resizable()을 붙여야 flexible |
.frame(width:height:) | 그 값을 자식에게 제안, 자신은 그 크기 | 강제가 아니다 — 자식은 여전히 자기 크기 |
.frame(maxWidth: .infinity) | 받은 제안 안에서 최대로 넓어짐 | "가로로 꽉 채우기" 표준 관용구 |
.fixedSize() | 자식에게 nil 제안 → ideal 크기 고정 | "자르지 말고 제 크기대로" |
"The frame is not a constraint in SwiftUI, it's just a view which you can think of like a picture frame." — WWDC19 · Building Custom Views with SwiftUI
→ frame은 제약이 아니라 액자 같은 뷰다. 액자는 그림(자식)에게 크기를 제안할 뿐, 그림을 잘라 맞추지 않는다.
frame(width:) vs frame(maxWidth:) — 순서까지 같이 본다
frame(maxWidth: .infinity)는 "넓게 제안받으면 넓어지는 껍질"을 하나 씌운다. 그 껍질 바깥에 붙인 배경만 넓어진다(Day 1의 모디파이어 순서).
Text("알림 설정")
.background(Color.yellow) // Text 크기의 배경
.frame(maxWidth: .infinity, alignment: .leading) // 투명한 넓은 껍질Text("알림 설정")
.frame(maxWidth: .infinity, alignment: .leading) // 넓은 껍질
.background(Color.yellow) // 넓은 껍질 크기의 배경fixedSize — 부모 제안에 대한 "역제안"
fixedSize() 문서는 이것을 "a counter proposal to the view size proposed to a view by its parent"라고 부른다. 부모 제안을 버리고 자식에게 ideal 크기를 묻는다. 한 축만 고정하는 fixedSize(horizontal:vertical:)이 실무에서 더 자주 쓰인다.
struct FixedSizeDemo: View {
var body: some View {
VStack(spacing: 40) {
// ① 200 폭 안에서 줄바꿈·말줄임
Text("A single line of text, too long to fit in a box.")
.frame(width: 200, height: 80)
.border(Color.gray)
// ② ideal 크기(한 줄 전체)로 고정 → 테두리 밖으로 넘친다
Text("A single line of text, too long to fit in a box.")
.fixedSize()
.frame(width: 200, height: 80)
.border(Color.gray)
// ③ 가로는 제안 수용(줄바꿈), 세로만 ideal → 잘리지 않고 여러 줄
Text("A single line of text, too long to fit in a box.")
.fixedSize(horizontal: false, vertical: true)
.frame(width: 200)
.border(Color.gray)
}
}
}fixedSize()는 부모 경계를 넘을 수 있다. 문서도 "This can result in the view exceeding the parent's bounds"라고 적는다. 좁은 화면·큰 Dynamic Type에서 가로 fixedSize()는 글자가 화면 밖으로 나가는 원인이 된다. 기본 선택은 ③ 세로만 고정이다.
✅ 복습 · 5분
SwiftUI 레이아웃의 3단계를 말하고, Auto Layout과 결정적으로 다른 단계를 짚어라.
부모가 자식에게 폭 0, .infinity, nil을 제안해 보는 이유는? 각각 무엇을 알아내나?
0 제안에 대한 답이 최소 크기, .infinity에 대한 답이 최대 크기, nil(미지정)에 대한 답이 ideal 크기다(Apple 문서 · ProposedViewSize). 스택 같은 컨테이너는 이 답들을 모아 공간을 나눈다. fixedSize()가 자식에게 보내는 것이 바로 nil 제안이다.VStack {
Text("Hi").frame(width: 200).background(Color.red) // (A)
Text("Hi").background(Color.red).frame(width: 200) // (B)
}🔮 코드 예측: 위 코드에서 (A)와 (B)의 빨간 영역 폭은 각각 얼마인가?
🔁 지난 Day 복습: UIHostingController를 자식 VC로 붙였을 때, SwiftUI 콘텐츠 크기 변화를 Auto Layout의 intrinsic size로 반영하게 하는 설정은?
hostingController.sizingOptions = .intrinsicContentSize (iOS 16+). 이 옵션을 켜면 SwiftUI 콘텐츠의 ideal size가 바뀔 때 호스팅 컨트롤러 뷰의 intrinsic content size가 갱신돼, 주변 Auto Layout 제약이 그 크기를 따른다. 오늘 배운 "ideal size = nil 제안에 대한 응답"이 바로 이 값이다.2부는 자식이 여럿일 때다 — 스택이 덜 유연한 자식부터 공간을 나누는 알고리즘과 Spacer·layoutPriority·정렬, 크기 협상에 끼지 않는 overlay/background, 정보가 아래로만 흐르는 GeometryReader, 그리고 흔한 실수를 다룬다.