SwiftUI Layout

SwiftUI 레이아웃 시스템

SwiftUI에서 크기가 정해지는 원리 — "부모 제안 → 자식 결정 → 부모 배치" 3단 협상. frame·Stack·GeometryReader·fixedSize·Layout까지.

0. ⭐ 핵심 — 3단계 협상

① 부모가 자식에게 "이만큼 써도 돼" 크기 제안(proposed size)
② 자식이 제안 + 자기 콘텐츠를 보고 "난 이만큼 쓸게" 자기 크기 결정
③ 부모가 자식을 자기 좌표에 배치(place)

프레임을 "설정"하는 게 아니라 이 협상으로 크기가 정해진다. 부모는 강제 못 하고 제안만, 최종 크기는 자식이 결정. 레이아웃이 이상하면 "누가 제안하고 누가 결정했나"를 따라가면 풀린다.

1. .frame도 하나의 뷰다

.frame(width:height:)는 자식을 감싸는 또 다른 뷰. frame이 자식에게 그 크기를 제안하고, frame 자신은 그 크기를 부모에게 보고.

Text("Hi").frame(width: 100, height: 100)
// frame은 100×100 차지, Text는 그 안에서 자기 크기(가운데 배치)

2. 뷰별 사이징 행동 (제안에 어떻게 반응하나)

제안에 대한 반응
Text필요한 만큼만(제안 내에서 줄바꿈). 제안보다 작을 수 있음
Image(기본)고유 크기(intrinsic) 고정, 제안 무시. .resizable() 하면 제안 따름
Color·Rectangle·Shapeflexible — 제안 전부 차지
Spacerflexible — 남는 공간 차지
Stack자식들에게 제안 나눠주고 합쳐서 자기 크기

이 차이가 "왜 Color는 화면 꽉 채우고 Text는 딱 글자만큼인가"의 답.

3. Stack의 공간 분배

HStack/VStack이 자식에게 공간 나눠 제안하는 순서: ① 덜 유연한(고정) 자식 먼저 크기 확정 → ② 남은 공간을 유연한 자식(Spacer·flexible frame)에 분배.

.layoutPriority(_:): 높은 우선순위 자식이 먼저 제안받아 원하는 만큼 가져감(긴 텍스트 truncation 조절 등).

4. frame — 고정 vs 유연 / GeometryReader / fixedSize

.frame(width: 100)              // 고정: 자식에 100 제안, 자기도 100
.frame(maxWidth: .infinity)     // 유연: 부모가 준 만큼 전부 차지
.frame(minWidth:idealWidth:maxWidth:) // 범위

5. alignment · Layout 프로토콜(iOS 16+)

한 줄: SwiftUI 레이아웃 = "부모 제안 → 자식 결정 → 부모 배치" 3단 협상. .frame도 뷰. Text=필요한 만큼, Color/Spacer=다 차지, Image=고정(resizable 전). .frame 고정/유연, GeometryReader=greedy, fixedSize=ideal.

✅ 체크 질문

  1. Text("Hi").frame(width: 200)에서 Text 폭? frame 폭? → Text=글자 폭, frame=200(그 안에서 Text 가운데)
  2. Color.red는 꽉 채우는데 Text는 딱 글자만큼인 이유? → Color=flexible(제안 다 차지), Text=필요한 만큼만
  3. GeometryReader가 레이아웃을 자주 깨는 이유? → greedy라 제안된 공간을 다 차지(부모 자리 밀어냄)

📌 후속 Q&A — UIKit 비교·ViewBuilder·intrinsic·Layout 프로토콜

Q. UIKit Auto Layout은 어떤 시스템? SwiftUI와 차이

UIKit Auto LayoutSwiftUI
방식제약(constraint) + 솔버제안→결정 협상
계산제약 솔버(Cassowary)가 선형 방정식·부등식 전역으로 한 번에 풀어 모든 frame 계산부모 제안→자식 결정→부모 배치 재귀(솔버 없음)
크기 원천제약 + intrinsicContentSize + hugging/compression 우선순위제안 + 뷰별 사이징 행동

UIKit=모두가 제약으로 묶여 솔버가 한 방에 해결(부모가 자식에 "제안" 없음). SwiftUI=부모-자식 재귀 협상.

Q. .frame은 modifier? / ViewBuilder란?

Q. intrinsic size — 어떤 뷰가 자동?

intrinsicContentSize=외부 제약 없이 콘텐츠만으로 정해지는 자연 크기(UIKit).

Q. 뷰별 사이징 — 자세히(제안 주면 뭘 반환)

제안 받으면
Text제안 폭 안에서 줄바꿈해 필요 크기(폭≤제안). 제안 nil/무한이면 ideal(한 줄 전체)
Image(비-resizable)제안 무시, intrinsic(픽셀) 크기
Image.resizable()제안 그대로 채움
Color·Rectangle·Shape제안 그대로(주는 만큼 다 차지)
Spacer스택 축 방향 남는 공간 다 차지
Stack자식에 (스페이싱 뺀) 공간 제안 → 자식 합+스페이싱

3부류: 제안 무시하고 자기 크기(Image·Text 일부) / 제안 다 먹음(Color·Spacer) / 제안 안에서 맞춤(Text·Stack).

Q. flexible frame / "전부 차지·alignment"

Text("Hi").frame(maxWidth: .infinity)                     // frame 폭 꽉 참, Text는 그 안 "가운데"
Text("Hi").frame(maxWidth: .infinity, alignment: .leading) // Text가 "왼쪽"에 붙음

flexible frame=크기를 숫자 고정 대신 범위(maxWidth:.infinity 등)로 줘 제안에 맞춰 늘어남. maxWidth:.infinity면 frame이 폭을 다 차지 → 그 안 자식은 기본 가운데. alignment 파라미터가 확장된 frame 안에서 자식 위치를 정함(.leading=왼쪽).

Q. alignment guide

정렬 기준선을 뷰별로 커스터마이즈. 각 정렬(.leading 등)마다 뷰의 기준값(guide)이 있고, .alignmentGuide(.leading) { d in ... }로 그 선을 특정 뷰에서 옮김(텍스트 baseline·배지 위치 맞추기). 커스텀 정렬은 AlignmentID. alignment="어느 선에 맞출까", alignment guide="그 선을 이 뷰에선 여기로".

Q. Layout 프로토콜(iOS 16+) 자세히

HStack/VStack 같은 컨테이너를 직접 만드는 프로토콜. 두 메서드:

struct FlowLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        // subviews[i].sizeThatFits(proposal)로 각 자식 크기 질의 → 배치 계산 → 총 크기 반환
    }
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        for sv in subviews { sv.place(at: point, anchor: .topLeading, proposal: ...) }
    }
}  // 사용: FlowLayout { Tag("a"); Tag("b") }

📌 심화 후속 Q&A (2차)

Q. hugging/compression resistance는 스택뷰 전용?

아니다. 모든 UIView에 있다. intrinsicContentSize 있는 뷰(Label·Button 등)에서 특히 의미. hugging=intrinsic보다 커지지 않으려는 저항(낮으면 늘어남), compression resistance=intrinsic보다 작아지지 않으려는 저항(낮으면 줄어듦/잘림). 스택뷰에서 자주 드러날 뿐 각 뷰의 속성.

Q. "frame 직접 설정 안 함"인데 .frame은?

UIKit view.frame = CGRect(절대 x,y,w,h 직접 지정)과 SwiftUI .frame(width:)은 다르다. SwiftUI .frame=자식에 크기 제안하는 래퍼 뷰(자식이 더 작을 수 있고, frame 자신도 부모에게 배치받음). "frame 직접 설정 안 함"=절대 좌표(x,y) 명령이 없다는 뜻.

Q. TupleView / _ConditionalContent

Q. intrinsic size = 콘텐츠만으로 정해지는 크기? / ideal?

Q. Text 폭/높이 · resizable Image 양방향

Q. "스페이싱 뺀 공간 제안 → 자식 합+스페이싱=자기 크기"

VStack(spacing: 10) { Text("A"); Text("B"); Text("C") }  // 높이 300 제안
① 300 - 스페이싱(10×2=20) = 280을 자식에 (하나씩) 제안
② 각 Text는 콘텐츠 높이만 씀(예: 각 20) — 280 다 안 씀. 합 60
③ VStack 자기 크기 = 자식 합(60) + 스페이싱(20) = 80

스택은 균등 분할이 아니다. 스페이싱을 먼저 떼고, 각 자식이 필요한 만큼 가져가며(남는 건 flexible 자식이 나눔), 자기 최종 크기는 자식들 크기+스페이싱. (3차 후속의 정정 참고.)

Q. idealWidth?

.frame(minWidth:idealWidth:maxWidth:)에서 min~max=허용 범위, idealWidth=제안 없을 때 쓸 기본 폭(=ideal). ScrollView 안(제안 무한)처럼 제안 애매할 때 이 값으로 자기 폭 결정.

Q. ⭐ GeometryReader가 레이아웃 흐트러뜨리는 예시

VStack {
    Text("제목")
    GeometryReader { geo in Text("\(geo.size.width)") }  // ← greedy
    Text("설명")
}

기대: 세 Text가 각자 높이만큼 쌓임. 실제: GeometryReader가 VStack이 준 세로 공간을 전부 먹어 "제목"과 "설명"이 화면 끝까지 벌어지고 안 Text는 좌상단에 붙음. 원인=자기 콘텐츠 크기가 아니라 "부모가 준 공간 전부"를 자기 크기로 삼음. → 크기만 읽으려면 .background/.overlay에 GeometryReader 넣거나 iOS 16+ onGeometryChange.

Q. ⭐ alignment guide (실무 정렬)

Stack의 alignment=자식들을 어느 정렬선에 맞출지 선택. alignmentGuide=각 뷰가 그 선에 자기 어느 지점을 댈지 재정의.

HStack(alignment: .lastTextBaseline) {   // 정렬선: 마지막 줄 baseline
    Text("가격").font(.caption)
    Text("29,000").font(.largeTitle)      // 폰트 크기 달라도 baseline 맞음
}
// 커스텀: 이 뷰의 leading 기준을 20 오른쪽으로
Text("Label").alignmentGuide(.leading) { d in d[.leading] + 20 }

d=ViewDimensions(d[.leading]·d.width·d[.firstTextBaseline]). 용도: 폰트 크기 다른 텍스트 baseline 맞추기, 다른 행의 배지·아이콘을 한 세로선 정렬, 커스텀 정렬(AlignmentID)로 스택 밖 뷰 정렬. 한 줄: alignment=맞출 선 선택, alignmentGuide=그 선에 댈 자기 지점을 뷰별 이동.

Q. sizeThatFits란?

"이 제안(크기)을 주면 넌 얼마가 필요해?"를 묻는 질의 함수. Layout 프로토콜의 sizeThatFits(proposal:subviews:)=자식 크기 물어본 뒤 총 필요 크기 반환. UIKit UIView.sizeThatFits(_:)도 같은 개념(UILabel.sizeThatFits(CGSize(width:100, height:.max))="폭 100일 때 필요한 높이", 배치 안 하고 계산만). SwiftUI 협상의 "자식이 크기 결정" 단계가 이것.

📌 심화 후속 Q&A (3차 — 정정 포함)

Q. hugging/compression을 스택 밖 어디서 쓰나

두 개 이상이 같은 축에서 공간을 두고 경쟁할 때 "누가 양보하냐"를 정한다. 스택 밖 예:

혼자 있는 뷰엔 의미 없음(경쟁이 없어서). 스택이 그 경쟁 상황을 자주 만들 뿐, leading/trailing 제약 직접 배치에도 동일.

Q. VStack 안 뷰 개수별 타입 / "부모"의 정의

Q. UILabel intrinsic size 타이밍 (래스터화와 별개)

측정(intrinsic) ≠ 래스터화(비트맵). intrinsic은 텍스트 설정 즉시 글자 메트릭으로 계산되고, 래스터화는 나중(디스플레이 패스).

label.text = "Hi"
 → invalidateIntrinsicContentSize (자연 크기 바뀜 표시)
 → 레이아웃 패스: Auto Layout이 intrinsic으로 프레임 계산 → layoutSubviews로 배치
 → 디스플레이 패스: 글리프 래스터화 → layer.contents 채움

intrinsic size는 layoutSubviews보다 앞(측정). layoutSubviews는 그 크기로 배치하는 단계. 래스터화는 그보다 뒤.

Q. aspectRatio도 모디파이어(래퍼)?

그렇다. .aspectRatio(16/9, contentMode: .fit)=래퍼 뷰가 "부모 공간 안에서 16:9 유지한 최대 크기"를 자식(이미지)에 제안. .fill이면 넘치게 채움(+.clipped()). resizable 이미지에 붙임.

Q. ⭐ 스택은 균등 분할? (280÷3 정정)

정정: 스택은 균등 분할이 아니다. 각 자식에게 제안 → 자식이 필요한 만큼 가져가고, 남는 건 flexible 자식(Spacer·Color·maxHeight:.infinity)이 나눔. (이전 판 "각 90" 오류.)
VStack(spacing: 10) { Text("A"); Text("B"); Text("C") }  // 높이 300 제안
① 300 - 스페이싱 20 = 280을 자식에 (하나씩) 제안
② 각 Text는 "난 20이면 돼"(콘텐츠 높이) → 각 20, 합 60 (280 다 안 씀)
③ VStack 크기 = 60 + 20 = 80  ← 300이 아니라 콘텐츠만큼
// 자식이 flexible이면 그때 남는 공간을 나눠 가짐(그때야 균등 비슷)

Q. GeometryReader가 받는 "부모 공간" = 전체? 뺀 부분?

제목·설명을 뺀 나머지. VStack이 고정 자식(제목·설명) 크기를 먼저 확정하고, 남은 세로 공간 전부를 GeometryReader(flexible·greedy)에 제안.

VStack 높이 800
 ├ Text("제목")   → 20 확정
 ├ GeometryReader → 남은 760 다 차지  ← geo.size.height = 760
 └ Text("설명")   → 20 확정
// 제목·설명 사이가 760만큼 벌어져 보임

Q. ⭐ .alignmentGuide(.leading) { d in d[.leading] + 20 } 그림으로

"이 뷰가 정렬선에 자기 어느 지점을 댈지"를 바꾼다. 기본 .leading=자기 왼쪽 끝(d[.leading]=0)을 선에 댐.

정렬선 |
기본:   |[뷰]           ← d[.leading]=0(왼끝)을 선에 댐
+20:  [뷰]|             ← 기준점을 뷰 안 20 지점으로 → 그 점을 선에 대려고 뷰가 왼쪽으로 20 밀림

기준점을 오른쪽(+)으로 옮기면 뷰는 왼쪽으로 밀린다. d=ViewDimensions(d.width·d[.leading]·d[.firstTextBaseline]). 쓸모: 특정 뷰만 들여쓰기, 다른 뷰의 특정 내부 지점을 한 세로선에 정렬.

Q. sizeThatFits 더 자세히

let label = UILabel(); label.text = "긴 문장…"; label.numberOfLines = 0
let fit = label.sizeThatFits(CGSize(width: 200, height: .greatestFiniteMagnitude))
// 입력=제안(최대), 출력=그 안에서 실제 필요 크기(폭≤200, 높이=줄 수만큼). 배치 안 함(계산만)

관련 문서

SwiftUI some View 심화 (identity·상태·렌더링·Observation)
UIKit 레이아웃 과정 심화 (비교)