์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ“ฑ Auto Layout๊ณผ Size Classes๋กœ ๋งŒ๋“œ๋Š” ๋ฐ˜์‘ํ˜• UI ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

๐Ÿ“ฑ Auto Layout๊ณผ Size Classes๋กœ ๋งŒ๋“œ๋Š” ๋ฐ˜์‘ํ˜• UI ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

์นœ๊ตฌ์•ผ, ๋ชจ๋“  ๋””๋ฐ”์ด์Šค์—์„œ ์™„๋ฒฝํ•˜๊ฒŒ ๋ณด์ด๋Š” ์•ฑ UI ๋งŒ๋“ค์–ด๋ณผ๊นŒ? ๐ŸŽจโœจ

์•ผ, ์†”์งํžˆ ๋งํ•ด์„œ ์•ฑ ๊ฐœ๋ฐœํ•  ๋•Œ ๊ฐ€์žฅ ์งœ์ฆ๋‚˜๋Š” ๊ฒŒ ๋ญ”์ง€ ์•Œ์•„? ๐Ÿค” ๋ฐ”๋กœ ์•„์ดํฐ SE์—์„œ๋Š” ์™„๋ฒฝํ•˜๊ฒŒ ๋ณด์ด๋˜ ํ™”๋ฉด์ด ์•„์ดํŒจ๋“œ ํ”„๋กœ์—์„œ๋Š” ์™„์ „ ๋ง๊ฐ€์ ธ ๋ณด์ด๋Š” ๊ฑฐ์ง€. ๊ทธ๋ฆฌ๊ณ  ๊ฐ€๋กœ๋ชจ๋“œ๋กœ ๋Œ๋ฆฌ๋ฉด? ์•„... ๋ง๋„ ๋งˆ. ๐Ÿ˜ฑ

๊ทผ๋ฐ ์žˆ์ž–์•„, ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๋„๊ตฌ๊ฐ€ ์žˆ์–ด. ๋ฐ”๋กœ Auto Layout๊ณผ Size Classes์•ผ! ์ด ๋‘ ์นœ๊ตฌ๋งŒ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๋ฉด, ๋„ˆ์˜ ์•ฑ์€ ์–ด๋–ค ๋””๋ฐ”์ด์Šค์—์„œ๋“  ์™„๋ฒฝํ•˜๊ฒŒ ๋ณด์ผ ๊ฑฐ์•ผ. ์ง„์งœ๋กœ! ๐Ÿ’ช

์˜ค๋Š˜์€ ๋‚ด๊ฐ€ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ๊ฒช์—ˆ๋˜ ์‚ฝ์งˆ๊ณผ ์„ฑ๊ณต ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ, ์ด ๋‘ ๊ฐœ๋…์„ ์™„์ „ํžˆ ์ •๋ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ค„๊ฒŒ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ UI/UX ๊ฐœ๋ฐœ ๋…ธํ•˜์šฐ๋ฅผ ๊ณต์œ ํ•˜๋Š” ์ „๋ฌธ๊ฐ€๋“ค์ด ๋งŽ์€๋ฐ, ์˜ค๋Š˜ ๋‚ด๊ฐ€ ๋„ˆํ•œํ…Œ ์•Œ๋ ค์ฃผ๋Š” ๋‚ด์šฉ๋„ ๊ทธ๋งŒํผ ์‹ค์ „์ ์ด๊ณ  ์œ ์šฉํ•  ๊ฑฐ์•ผ! ๐Ÿš€

๐ŸŽฏ Auto Layout์ด ๋ญ”๋ฐ? ๊ธฐ์ดˆ๋ถ€ํ„ฐ ํƒ„ํƒ„ํ•˜๊ฒŒ!

์ž, ์ผ๋‹จ Auto Layout๋ถ€ํ„ฐ ์ œ๋Œ€๋กœ ์ดํ•ดํ•ด๋ณด์ž. ์ด๊ฑฐ ์—†์ด iOS ๊ฐœ๋ฐœํ•œ๋‹ค๋Š” ๊ฑด... ๋งˆ์น˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ์—†์ด ์ฒ˜์Œ ๊ฐ€๋Š” ๊ณณ ์ฐพ์•„๊ฐ€๋Š” ๊ฒƒ๊ณผ ๊ฐ™์•„. ๊ฐ€๋Šฅ์€ ํ•œ๋ฐ ์—„์ฒญ ํž˜๋“ค์ง€. ๐Ÿ˜…

๐Ÿ’ก Auto Layout์˜ ํ•ต์‹ฌ ๊ฐœ๋…

Auto Layout์€ ๋ทฐ์˜ ์œ„์น˜์™€ ํฌ๊ธฐ๋ฅผ ์ œ์•ฝ ์กฐ๊ฑด(Constraints)์œผ๋กœ ์ •์˜ํ•˜๋Š” ์‹œ์Šคํ…œ์ด์•ผ. ์ ˆ๋Œ€์ ์ธ ์ขŒํ‘œ๊ฐ’(x=100, y=200 ์ด๋Ÿฐ ์‹)์œผ๋กœ ์ •ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, "์ด ๋ฒ„ํŠผ์€ ํ™”๋ฉด ์™ผ์ชฝ์—์„œ 20ํฌ์ธํŠธ ๋–จ์–ด์ ธ ์žˆ๊ณ , ์ƒ๋‹จ์—์„œ 50ํฌ์ธํŠธ ๋–จ์–ด์ ธ ์žˆ์–ด"๋ผ๊ณ  ๊ด€๊ณ„๋กœ ์ •์˜ํ•˜๋Š” ๊ฑฐ์ง€.

Auto Layout ์ž‘๋™ ์›๋ฆฌ ์ƒ๋‹จ 20pt ๋ฒ„ํŠผ ์ขŒ์ธก 20pt ์ œ์•ฝ์กฐ๊ฑด์œผ๋กœ ์ž๋™ ๋ฐฐ์น˜ โœ“ ์ œ์•ฝ์กฐ๊ฑด ์ข…๋ฅ˜ โ€ข Leading (์™ผ์ชฝ) โ€ข Trailing (์˜ค๋ฅธ์ชฝ) โ€ข Top (์ƒ๋‹จ) โ€ข Bottom (ํ•˜๋‹จ) โ€ข Width (๋„ˆ๋น„) โ€ข Height (๋†’์ด) โ€ข CenterX (์ค‘์•™X) โ€ข CenterY (์ค‘์•™Y)

๐Ÿ”ง ์ œ์•ฝ์กฐ๊ฑด(Constraints)์˜ ์„ธ๊ณ„

์ œ์•ฝ์กฐ๊ฑด์€ Auto Layout์˜ ํ•ต์‹ฌ์ด์•ผ. ์ด๊ฑธ ์ดํ•ดํ•˜์ง€ ๋ชปํ•˜๋ฉด ์ ˆ๋Œ€ ๋ฐ˜์‘ํ˜• UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์—†์–ด. ๊ทผ๋ฐ ๊ฑฑ์ • ๋งˆ, ์ƒ๊ฐ๋ณด๋‹ค ๊ฐ„๋‹จํ•ด! ๐Ÿ˜Š

์œ„์น˜ ์ œ์•ฝ์กฐ๊ฑด (Position Constraints)

๋ทฐ๊ฐ€ ์–ด๋””์— ์œ„์น˜ํ• ์ง€ ์ •ํ•˜๋Š” ๊ฑฐ์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด:

โ€ข Leading: ์™ผ์ชฝ ์—ฌ๋ฐฑ (์˜์–ด๊ถŒ์—์„œ๋Š” ์™ผ์ชฝ์ด ์‹œ์ž‘์ ์ด๋‹ˆ๊นŒ)
โ€ข Trailing: ์˜ค๋ฅธ์ชฝ ์—ฌ๋ฐฑ
โ€ข Top: ์œ„์ชฝ ์—ฌ๋ฐฑ
โ€ข Bottom: ์•„๋ž˜์ชฝ ์—ฌ๋ฐฑ

ํฌ๊ธฐ ์ œ์•ฝ์กฐ๊ฑด (Size Constraints)

๋ทฐ๊ฐ€ ์–ผ๋งˆ๋‚˜ ํด์ง€ ์ •ํ•˜๋Š” ๊ฑฐ์•ผ:

โ€ข Width: ๋„ˆ๋น„๋ฅผ ๊ณ ์ •ํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๋ทฐ์™€ ๋น„์œจ๋กœ ์„ค์ •
โ€ข Height: ๋†’์ด๋ฅผ ๊ณ ์ •ํ•˜๊ฑฐ๋‚˜ ๋‚ด์šฉ์— ๋งž์ถฐ ์ž๋™ ์กฐ์ ˆ
โ€ข Aspect Ratio: ๊ฐ€๋กœ์„ธ๋กœ ๋น„์œจ ์œ ์ง€ (์˜ˆ: 16:9)

์ •๋ ฌ ์ œ์•ฝ์กฐ๊ฑด (Alignment Constraints)

์—ฌ๋Ÿฌ ๋ทฐ๋ฅผ ์–ด๋–ป๊ฒŒ ์ •๋ ฌํ• ์ง€ ์ •ํ•˜๋Š” ๊ฑฐ์•ผ:

โ€ข Center X/Y: ์ค‘์•™ ์ •๋ ฌ
โ€ข Baseline: ํ…์ŠคํŠธ ๊ธฐ์ค€์„  ์ •๋ ฌ
โ€ข First Baseline: ์ฒซ ์ค„ ๊ธฐ์ค€์„  ์ •๋ ฌ

๐Ÿ’ก ํ”„๋กœ ํŒ: ์ œ์•ฝ์กฐ๊ฑด์„ ์„ค์ •ํ•  ๋•Œ๋Š” ํ•ญ์ƒ "์ด ๋ทฐ๊ฐ€ ํ™”๋ฉด ํฌ๊ธฐ๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•ด์•ผ ํ•˜์ง€?"๋ฅผ ๋จผ์ € ์ƒ๊ฐํ•ด๋ด. ๊ทธ๋Ÿฌ๋ฉด ์–ด๋–ค ์ œ์•ฝ์กฐ๊ฑด์ด ํ•„์š”ํ•œ์ง€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์–ด!

๐Ÿ“ ์ œ์•ฝ์กฐ๊ฑด ์šฐ์„ ์ˆœ์œ„ (Priority)

์ž, ์ด์ œ ์ข€ ๋” ๊ณ ๊ธ‰ ๊ฐœ๋…์œผ๋กœ ๋“ค์–ด๊ฐ€๋ณผ๊นŒ? ์ œ์•ฝ์กฐ๊ฑด์—๋Š” ์šฐ์„ ์ˆœ์œ„๋ผ๋Š” ๊ฒŒ ์žˆ์–ด. ์ด๊ฒŒ ์ง„์งœ ์ค‘์š”ํ•ด! ๐ŸŽฏ

์šฐ์„ ์ˆœ์œ„ ๊ฐ’ ์ด๋ฆ„ ์„ค๋ช… ์‚ฌ์šฉ ์˜ˆ์‹œ
1000 Required ๋ฐ˜๋“œ์‹œ ์ง€์ผœ์ ธ์•ผ ํ•จ ์•ˆ์ „ ์˜์—ญ ์ œ์•ฝ, ํ•„์ˆ˜ ๊ฐ„๊ฒฉ
750 High ๊ฐ€๋Šฅํ•˜๋ฉด ์ง€ํ‚ค๋˜ ํ•„์ˆ˜๋Š” ์•„๋‹˜ ์„ ํ˜ธํ•˜๋Š” ํฌ๊ธฐ
250 Low ๋‹ค๋ฅธ ์ œ์•ฝ์ด ์—†์„ ๋•Œ๋งŒ ์ตœ์†Œ ํฌ๊ธฐ ์ œ์•ˆ
1-999 Custom ์ง์ ‘ ์„ค์ •ํ•œ ์šฐ์„ ์ˆœ์œ„ ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ ์กฐ์ •

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๋ฒ„ํŠผ์˜ ๋„ˆ๋น„๋ฅผ 200ํฌ์ธํŠธ๋กœ ํ•˜๊ณ  ์‹ถ์€๋ฐ, ํ™”๋ฉด์ด ์ž‘์œผ๋ฉด ์ค„์–ด๋“ค์–ด๋„ ๊ดœ์ฐฎ๋‹ค๊ณ  ํ•˜์ž. ๊ทธ๋Ÿผ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ผ:

// Swift ์ฝ”๋“œ ์˜ˆ์‹œ
let widthConstraint = button.widthAnchor.constraint(equalToConstant: 200)
widthConstraint.priority = .defaultHigh // 750
widthConstraint.isActive = true

// ์ตœ์†Œ ๋„ˆ๋น„๋Š” ๋ฐ˜๋“œ์‹œ ์ง€์ผœ์•ผ ํ•จ
let minWidthConstraint = button.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
minWidthConstraint.priority = .required // 1000
minWidthConstraint.isActive = true

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฒ„ํŠผ์€ ๊ฐ€๋Šฅํ•˜๋ฉด 200ํฌ์ธํŠธ ๋„ˆ๋น„๋ฅผ ์œ ์ง€ํ•˜์ง€๋งŒ, ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•˜๋ฉด 100ํฌ์ธํŠธ๊นŒ์ง€ ์ค„์–ด๋“ค ์ˆ˜ ์žˆ์–ด. ๊ทผ๋ฐ 100ํฌ์ธํŠธ ์ดํ•˜๋กœ๋Š” ์ ˆ๋Œ€ ์•ˆ ์ค„์–ด๋“ค์ง€! ์™„๋ฒฝํ•˜์ง€? ๐Ÿ˜Ž

๐ŸŽจ Intrinsic Content Size - ๋ทฐ์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ํฌ๊ธฐ

์•ผ, ์ด๊ฑฐ ์ง„์งœ ์‹ ๊ธฐํ•œ ๊ฐœ๋…์ธ๋ฐ, Intrinsic Content Size๋ผ๋Š” ๊ฒŒ ์žˆ์–ด. ์ง์—ญํ•˜๋ฉด "๊ณ ์œ  ์ฝ˜ํ…์ธ  ํฌ๊ธฐ"์ธ๋ฐ, ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด ๋ทฐ๊ฐ€ ์ž๊ธฐ ๋‚ด์šฉ๋ฌผ์— ๋งž์ถฐ์„œ "๋‚˜๋Š” ์ด ์ •๋„ ํฌ๊ธฐ๋ฉด ๋ผ!"๋ผ๊ณ  ์ž๋™์œผ๋กœ ์•Œ๋ ค์ฃผ๋Š” ๊ฑฐ์•ผ. ๐Ÿค“

๐Ÿ“ Intrinsic Size๊ฐ€ ์žˆ๋Š” ๋ทฐ

UILabel
ํ…์ŠคํŠธ ๊ธธ์ด์— ๋งž์ถฐ ์ž๋™ ํฌ๊ธฐ ๊ฒฐ์ •

UIButton
ํƒ€์ดํ‹€๊ณผ ์ด๋ฏธ์ง€ ํฌ๊ธฐ์— ๋งž์ถค

UIImageView
์ด๋ฏธ์ง€ ์›๋ณธ ํฌ๊ธฐ ์‚ฌ์šฉ

UITextField
ํฐํŠธ์™€ ๋‚ด์šฉ์— ๋งž์ถฐ ์กฐ์ ˆ

๐Ÿ“ฆ Intrinsic Size๊ฐ€ ์—†๋Š” ๋ทฐ

UIView
๊ธฐ๋ณธ ๋ทฐ๋Š” ๊ณ ์œ  ํฌ๊ธฐ ์—†์Œ

UIScrollView
๋‚ด์šฉ๋ฌผ ํฌ๊ธฐ๋ฅผ ์•Œ ์ˆ˜ ์—†์Œ

Custom Views
์ง์ ‘ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์œผ๋ฉด ์—†์Œ

๐ŸŽฏ Content Hugging vs Compression Resistance

์ด ๋‘ ๊ฐœ๋…์€ Intrinsic Size์™€ ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋Š” ์ค‘์š”ํ•œ ์†์„ฑ์ด์•ผ:


Content Hugging Priority (์ฝ˜ํ…์ธ  ๊ปด์•ˆ๊ธฐ ์šฐ์„ ์ˆœ์œ„)
"๋‚˜๋Š” ๋‚ด ์ฝ˜ํ…์ธ  ํฌ๊ธฐ๋ณด๋‹ค ์ปค์ง€๊ธฐ ์‹ซ์–ด!" ๋ผ๋Š” ์˜๋ฏธ์•ผ. ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์„์ˆ˜๋ก ๋Š˜์–ด๋‚˜์ง€ ์•Š์œผ๋ ค๊ณ  ํ•ด.
โ†’ ๊ธฐ๋ณธ๊ฐ’: 250 (Low)


Compression Resistance Priority (์••์ถ• ์ €ํ•ญ ์šฐ์„ ์ˆœ์œ„)
"๋‚˜๋Š” ๋‚ด ์ฝ˜ํ…์ธ  ํฌ๊ธฐ๋ณด๋‹ค ์ž‘์•„์ง€๊ธฐ ์‹ซ์–ด!" ๋ผ๋Š” ์˜๋ฏธ์•ผ. ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์„์ˆ˜๋ก ์ค„์–ด๋“ค์ง€ ์•Š์œผ๋ ค๊ณ  ํ•ด.
โ†’ ๊ธฐ๋ณธ๊ฐ’: 750 (High)

์‹ค์ „ ์˜ˆ์‹œ๋ฅผ ๋ณด์ž:

// ๋‘ ๊ฐœ์˜ ๋ ˆ์ด๋ธ”์ด ๋‚˜๋ž€ํžˆ ์žˆ์„ ๋•Œ
let nameLabel = UILabel()
nameLabel.text = "์ด๋ฆ„"
nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) // 251

let valueLabel = UILabel()
valueLabel.text = "ํ™๊ธธ๋™์ž…๋‹ˆ๋‹ค๋งŒ ์ด๋ฆ„์ด ์ข€ ๊ธธ์–ด์š”"
valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) // 250

// ๊ฒฐ๊ณผ: nameLabel์€ "์ด๋ฆ„" ํฌ๊ธฐ๋งŒํผ๋งŒ ์ฐจ์ง€ํ•˜๊ณ ,
// valueLabel์ด ๋‚จ์€ ๊ณต๊ฐ„์„ ๋‹ค ์ฐจ์ง€ํ•จ

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด "์ด๋ฆ„"์ด๋ผ๋Š” ์งง์€ ํ…์ŠคํŠธ๋Š” ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ๊ณต๊ฐ„์„ ์ฐจ์ง€ํ•˜๊ณ , ๊ธด ์ด๋ฆ„ ๊ฐ’์ด ๋‚˜๋จธ์ง€ ๊ณต๊ฐ„์„ ์œ ์—ฐํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ์™„์ „ ์Šค๋งˆํŠธํ•˜์ง€? ๐Ÿง 

Content Hugging ๋™์ž‘ ์›๋ฆฌ Hugging Priority ๋†’์Œ ์ด๋ฆ„ ํ™๊ธธ๋™์ž…๋‹ˆ๋‹ค๋งŒ ์ด๋ฆ„์ด ์ข€ ๊ธธ์–ด์š” ์ตœ์†Œ ํฌ๊ธฐ ์œ ์ง€ Hugging Priority ๋‚ฎ์Œ ์ด๋ฆ„ ํ™๊ธธ๋™์ž…๋‹ˆ๋‹ค๋งŒ ์ด๋ฆ„์ด ์ข€ ๊ธธ์–ด์š” ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋Š˜์–ด๋‚จ ์šฐ์„ ์ˆœ์œ„ ์„ค์ • ๊ฐ€์ด๋“œ ๊ณ ์ • ํฌ๊ธฐ ๋ ˆ์ด๋ธ”: High (251) ์œ ์—ฐํ•œ ๋ ˆ์ด๋ธ”: Low (250) ๋ฒ„ํŠผ: Default High (750) ์ด๋ฏธ์ง€๋ทฐ: Medium (500) ํŒ: ์ค‘์š”ํ•œ ์ฝ˜ํ…์ธ ์ผ์ˆ˜๋ก ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋†’๊ฒŒ ์„ค์ •ํ•˜์„ธ์š”

๐Ÿ“ฑ Size Classes - ์ง„์งœ ๋ฐ˜์‘ํ˜•์˜ ์‹œ์ž‘

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Size Classes์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž! ์ด๊ฒŒ ์ง„์งœ ๊ฒŒ์ž„ ์ฒด์ธ์ €์•ผ. ๐ŸŽฎ

Size Classes๋Š” ์• ํ”Œ์ด ๋งŒ๋“  ์ถ”์ƒํ™” ๊ฐœ๋…์ธ๋ฐ, ๋””๋ฐ”์ด์Šค์˜ ํ™”๋ฉด ํฌ๊ธฐ๋ฅผ Compact์™€ Regular ๋‘ ๊ฐ€์ง€ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋‚˜๋ˆˆ ๊ฑฐ์•ผ. ๊ทธ๋ฆฌ๊ณ  ์ด๊ฑธ ๊ฐ€๋กœ(Width)์™€ ์„ธ๋กœ(Height)์— ๊ฐ๊ฐ ์ ์šฉํ•ด์„œ ์ด 4๊ฐ€์ง€ ์กฐํ•ฉ์ด ๋‚˜์˜ค์ง€.

๐ŸŽฏ Size Classes์˜ 4๊ฐ€์ง€ ์กฐํ•ฉ

Width Height ๋Œ€ํ‘œ ๋””๋ฐ”์ด์Šค ์ฃผ์š” ํŠน์ง•
Compact Compact iPhone ๊ฐ€๋กœ๋ชจ๋“œ (์ž‘์€ ๋ชจ๋ธ) ๊ฐ€์žฅ ์ œํ•œ์ ์ธ ๊ณต๊ฐ„
Compact Regular iPhone ์„ธ๋กœ๋ชจ๋“œ (๋ชจ๋“  ๋ชจ๋ธ) ์ผ๋ฐ˜์ ์ธ ํฐ ์‚ฌ์šฉ ํ™˜๊ฒฝ
Regular Compact iPhone Plus/Max ๊ฐ€๋กœ๋ชจ๋“œ ๋„“์ง€๋งŒ ๋†’์ด๋Š” ์ œํ•œ์ 
Regular Regular iPad (๋ชจ๋“  ๋ฐฉํ–ฅ) ๊ฐ€์žฅ ๋„“์€ ๊ณต๊ฐ„

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: Size Classes๋Š” ์ ˆ๋Œ€์ ์ธ ํ”ฝ์…€ ํฌ๊ธฐ๊ฐ€ ์•„๋‹ˆ์•ผ! iPhone 14 Pro Max์™€ iPhone SE๋Š” ํ™”๋ฉด ํฌ๊ธฐ๊ฐ€ ์™„์ „ ๋‹ค๋ฅด์ง€๋งŒ, ์„ธ๋กœ๋ชจ๋“œ์—์„œ๋Š” ๋‘˜ ๋‹ค (Compact Width, Regular Height)๋กœ ๋ถ„๋ฅ˜๋ผ. ์ด๊ฒŒ Size Classes์˜ ํ•ต์‹ฌ ์ฒ ํ•™์ด์•ผ - ๊ตฌ์ฒด์ ์ธ ํฌ๊ธฐ๊ฐ€ ์•„๋‹ˆ๋ผ "๊ณต๊ฐ„์˜ ์—ฌ์œ ๋„"๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ถ„๋ฅ˜ํ•˜๋Š” ๊ฑฐ์ง€.

๐ŸŽจ Size Classes ์‹ค์ „ ํ™œ์šฉ๋ฒ•

์ž, ์ด์ œ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ์“ฐ๋Š”์ง€ ๋ณด์ž. Interface Builder(์Šคํ† ๋ฆฌ๋ณด๋“œ)์—์„œ Size Classes๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋ถ€ํ„ฐ ์‹œ์ž‘ํ• ๊ฒŒ! ๐Ÿ“

Xcode์—์„œ Size Classes ์„ค์ •ํ•˜๊ธฐ

์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ์—ด๊ณ  ํ•˜๋‹จ์˜ "View as: iPhone 13" ๊ฐ™์€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด๋ด. ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์–‘ํ•œ ๋””๋ฐ”์ด์Šค์™€ ๋ฐฉํ–ฅ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋‰ด๊ฐ€ ๋‚˜์™€. ์—ฌ๊ธฐ์„œ "Vary for Traits" ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํŠน์ • Size Class์—๋งŒ ์ ์šฉ๋˜๋Š” ์ œ์•ฝ์กฐ๊ฑด์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด!

์กฐ๊ฑด๋ถ€ ์ œ์•ฝ์กฐ๊ฑด ๋งŒ๋“ค๊ธฐ

์˜ˆ๋ฅผ ๋“ค์–ด, iPad์—์„œ๋Š” ๋ฒ„ํŠผ์„ ๊ฐ€๋กœ๋กœ ๋ฐฐ์น˜ํ•˜๊ณ  ์‹ถ์€๋ฐ iPhone์—์„œ๋Š” ์„ธ๋กœ๋กœ ๋ฐฐ์น˜ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด?


1. "Vary for Traits"๋ฅผ ํด๋ฆญํ•˜๊ณ  Width๋ฅผ ์„ ํƒ
2. Regular Width์ผ ๋•Œ์˜ ๋ ˆ์ด์•„์›ƒ์„ ์„ค์ •
3. "Done Varying" ํด๋ฆญ
4. ์ด์ œ Compact Width์ผ ๋•Œ์˜ ๋ ˆ์ด์•„์›ƒ์„ ๋”ฐ๋กœ ์„ค์ •

์ฝ”๋“œ๋กœ๋„ ํ•  ์ˆ˜ ์žˆ์–ด! ์ด๊ฒŒ ๋” ์œ ์—ฐํ•˜๊ณ  ๊ฐ•๋ ฅํ•ด:

// Swift ์ฝ”๋“œ๋กœ Size Classes ๋Œ€์‘ํ•˜๊ธฐ
class ResponsiveViewController: UIViewController {
    
    let stackView = UIStackView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupStackView()
    }
    
    func setupStackView() {
        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)
        
        // ๊ธฐ๋ณธ ์ œ์•ฝ์กฐ๊ฑด
        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            stackView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 20),
            stackView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -20)
        ])
        
        updateLayoutForTraitCollection()
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        // Size Class๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋งŒ ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ
        if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass ||
           traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass {
            updateLayoutForTraitCollection()
        }
    }
    
    func updateLayoutForTraitCollection() {
        // Regular Width (iPad, iPhone ๊ฐ€๋กœ๋ชจ๋“œ ํฐ ๋ชจ๋ธ)
        if traitCollection.horizontalSizeClass == .regular {
            stackView.axis = .horizontal
            stackView.spacing = 30
            stackView.distribution = .fillEqually
        }
        // Compact Width (iPhone ์„ธ๋กœ๋ชจ๋“œ)
        else {
            stackView.axis = .vertical
            stackView.spacing = 15
            stackView.distribution = .fill
        }
        
        // ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝ
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
}

โœ… ์„ฑ๊ณต ํŒ: traitCollectionDidChange(_:) ๋ฉ”์„œ๋“œ๋Š” ๋””๋ฐ”์ด์Šค๋ฅผ ํšŒ์ „ํ•˜๊ฑฐ๋‚˜ Split View ํฌ๊ธฐ๋ฅผ ์กฐ์ ˆํ•  ๋•Œ ์ž๋™์œผ๋กœ ํ˜ธ์ถœ๋ผ. ์ด๊ฑธ ํ™œ์šฉํ•˜๋ฉด ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ˜์‘ํ•˜๋Š” UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด!

๐Ÿ”„ Adaptive Layout ํŒจํ„ด

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ ์ž์ฃผ ์“ฐ๋Š” ํŒจํ„ด๋“ค์„ ์†Œ๊ฐœํ• ๊ฒŒ. ์ด๊ฑฐ ์•Œ์•„๋‘๋ฉด ์ง„์งœ ์œ ์šฉํ•ด! ๐Ÿ’Ž

ํŒจํ„ด 1: ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ ์กฐ์ •

์ปฌ๋ ‰์…˜ ๋ทฐ์—์„œ Size Classes์— ๋”ฐ๋ผ ์—ด ๊ฐœ์ˆ˜๋ฅผ ์กฐ์ •ํ•˜๋Š” ๊ฑฐ์•ผ:

class AdaptiveCollectionViewController: UIViewController {
    
    var collectionView: UICollectionView!
    
    func numberOfColumns() -> Int {
        let horizontalClass = traitCollection.horizontalSizeClass
        let verticalClass = traitCollection.verticalSizeClass
        
        switch (horizontalClass, verticalClass) {
        case (.regular, .regular):
            // iPad - 4์—ด
            return 4
        case (.regular, .compact):
            // iPhone Plus/Max ๊ฐ€๋กœ๋ชจ๋“œ - 3์—ด
            return 3
        case (.compact, .regular):
            // iPhone ์„ธ๋กœ๋ชจ๋“œ - 2์—ด
            return 2
        default:
            // iPhone ๊ฐ€๋กœ๋ชจ๋“œ - 2์—ด
            return 2
        }
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        collectionView.collectionViewLayout.invalidateLayout()
    }
}

// UICollectionViewDelegateFlowLayout ๊ตฌํ˜„
extension AdaptiveCollectionViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, 
                       layout collectionViewLayout: UICollectionViewLayout,
                       sizeForItemAt indexPath: IndexPath) -> CGSize {
        
        let columns = CGFloat(numberOfColumns())
        let spacing: CGFloat = 10
        let totalSpacing = spacing * (columns - 1)
        
        let width = (collectionView.bounds.width - totalSpacing) / columns
        return CGSize(width: width, height: width)
    }
}

ํŒจํ„ด 2: ๋„ค๋น„๊ฒŒ์ด์…˜ ์Šคํƒ€์ผ ๋ณ€๊ฒฝ

iPad์—์„œ๋Š” Split View, iPhone์—์„œ๋Š” ์ผ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜:

class AdaptiveNavigationController: UIViewController {
    
    var splitViewController: UISplitViewController?
    var navigationController: UINavigationController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupAdaptiveNavigation()
    }
    
    func setupAdaptiveNavigation() {
        if traitCollection.horizontalSizeClass == .regular {
            // iPad: Split View ์‚ฌ์šฉ
            let masterVC = MasterViewController()
            let detailVC = DetailViewController()
            
            let split = UISplitViewController()
            split.viewControllers = [
                UINavigationController(rootViewController: masterVC),
                UINavigationController(rootViewController: detailVC)
            ]
            split.preferredDisplayMode = .oneBesideSecondary
            
            addChild(split)
            view.addSubview(split.view)
            split.didMove(toParent: self)
            
            splitViewController = split
        } else {
            // iPhone: ์ผ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜
            let masterVC = MasterViewController()
            let navController = UINavigationController(rootViewController: masterVC)
            
            addChild(navController)
            view.addSubview(navController.view)
            navController.didMove(toParent: self)
            
            navigationController = navController
        }
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        // Size Class ๋ณ€๊ฒฝ ์‹œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์Šคํƒ€์ผ ์žฌ๊ตฌ์„ฑ
        if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
            // ๊ธฐ์กด ๋ทฐ ์ œ๊ฑฐ
            children.forEach { child in
                child.willMove(toParent: nil)
                child.view.removeFromSuperview()
                child.removeFromParent()
            }
            
            // ์ƒˆ๋กœ์šด ์Šคํƒ€์ผ๋กœ ์žฌ๊ตฌ์„ฑ
            setupAdaptiveNavigation()
        }
    }
}
Size Classes ๊ธฐ๋ฐ˜ ์ ์‘ํ˜• ๋ ˆ์ด์•„์›ƒ iPhone ์„ธ๋กœ (Compact, Regular) ํ—ค๋” ์ฝ˜ํ…์ธ  1 ์ฝ˜ํ…์ธ  2 ์ฝ˜ํ…์ธ  3 ์„ธ๋กœ ์Šคํƒ ๋ฐฐ์น˜ iPad (Regular, Regular) ํ—ค๋” ์ฝ˜ํ…์ธ  1 ์ฝ˜ํ…์ธ  2 ์ฝ˜ํ…์ธ  3 ๊ฐ€๋กœ ๊ทธ๋ฆฌ๋“œ ๋ฐฐ์น˜ ์ž๋™ ์ „ํ™˜ ๐Ÿ’ก ํ•ต์‹ฌ: Size Classes๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํ•˜๋‚˜์˜ ์ฝ”๋“œ๋กœ ๋ชจ๋“  ๋””๋ฐ”์ด์Šค์— ์ตœ์ ํ™”๋œ ๋ ˆ์ด์•„์›ƒ ์ œ๊ณต!

๐Ÿ› ๏ธ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ๋ฐ˜์‘ํ˜• ๋กœ๊ทธ์ธ ํ™”๋ฉด ๋งŒ๋“ค๊ธฐ

์ž, ์ด์ œ ๋ฐฐ์šด ๊ฑธ ๋‹ค ํ•ฉ์ณ์„œ ์‹ค์ „ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๋ชจ๋“  ๋””๋ฐ”์ด์Šค์—์„œ ์™„๋ฒฝํ•˜๊ฒŒ ๋ณด์ด๋Š” ๋กœ๊ทธ์ธ ํ™”๋ฉด์„ ๋งŒ๋“ค ๊ฑฐ์•ผ. ๐ŸŽฏ

๐Ÿ“‹ ์š”๊ตฌ์‚ฌํ•ญ

โ€ข iPhone SE๋ถ€ํ„ฐ iPad Pro๊นŒ์ง€ ๋ชจ๋“  ๋””๋ฐ”์ด์Šค ์ง€์›
โ€ข ์„ธ๋กœ/๊ฐ€๋กœ ๋ชจ๋“œ ๋ชจ๋‘ ๋Œ€์‘
โ€ข ํ‚ค๋ณด๋“œ๊ฐ€ ์˜ฌ๋ผ์™€๋„ ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ๊ฐ€๋ ค์ง€์ง€ ์•Š์Œ
โ€ข ์ž‘์€ ํ™”๋ฉด์—์„œ๋Š” ๋กœ๊ณ  ํฌ๊ธฐ ์ถ•์†Œ
โ€ข ํฐ ํ™”๋ฉด์—์„œ๋Š” ์—ฌ๋ฐฑ์„ ํ™œ์šฉํ•œ ์—ฌ์œ ๋กœ์šด ๋ ˆ์ด์•„์›ƒ

import UIKit

class ResponsiveLoginViewController: UIViewController {
    
    // MARK: - UI Components
    
    private let scrollView: UIScrollView = {
        let scroll = UIScrollView()
        scroll.translatesAutoresizingMaskIntoConstraints = false
        scroll.showsVerticalScrollIndicator = false
        return scroll
    }()
    
    private let contentView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    private let logoImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(systemName: "person.circle.fill")
        imageView.tintColor = .systemBlue
        return imageView
    }()
    
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค"
        label.font = .systemFont(ofSize: 32, weight: .bold)
        label.textAlignment = .center
        return label
    }()
    
    private let subtitleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "๋กœ๊ทธ์ธํ•˜์—ฌ ๊ณ„์†ํ•˜์„ธ์š”"
        label.font = .systemFont(ofSize: 16, weight: .regular)
        label.textColor = .secondaryLabel
        label.textAlignment = .center
        return label
    }()
    
    private let emailTextField: UITextField = {
        let textField = UITextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.placeholder = "์ด๋ฉ”์ผ"
        textField.borderStyle = .roundedRect
        textField.keyboardType = .emailAddress
        textField.autocapitalizationType = .none
        return textField
    }()
    
    private let passwordTextField: UITextField = {
        let textField = UITextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.placeholder = "๋น„๋ฐ€๋ฒˆํ˜ธ"
        textField.borderStyle = .roundedRect
        textField.isSecureTextEntry = true
        return textField
    }()
    
    private let loginButton: UIButton = {
        let button = UIButton(type: .system)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("๋กœ๊ทธ์ธ", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
        button.backgroundColor = .systemBlue
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 12
        return button
    }()
    
    // MARK: - Constraints
    
    private var logoHeightConstraint: NSLayoutConstraint!
    private var contentWidthConstraint: NSLayoutConstraint!
    private var topSpacingConstraint: NSLayoutConstraint!
    
    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupConstraints()
        setupKeyboardObservers()
        updateLayoutForTraitCollection()
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass ||
           traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass {
            updateLayoutForTraitCollection()
        }
    }
    
    // MARK: - Setup
    
    private func setupUI() {
        view.backgroundColor = .systemBackground
        
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        
        contentView.addSubview(logoImageView)
        contentView.addSubview(titleLabel)
        contentView.addSubview(subtitleLabel)
        contentView.addSubview(emailTextField)
        contentView.addSubview(passwordTextField)
        contentView.addSubview(loginButton)
    }
    
    private func setupConstraints() {
        // ScrollView Constraints
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // ContentView Constraints
        NSLayoutConstraint.activate([
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
        ])
        
        // Content Width - Size Class์— ๋”ฐ๋ผ ๋ณ€๊ฒฝ๋  ์ œ์•ฝ์กฐ๊ฑด
        contentWidthConstraint = contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.9)
        contentWidthConstraint.isActive = true
        
        // Logo Constraints
        logoHeightConstraint = logoImageView.heightAnchor.constraint(equalToConstant: 120)
        topSpacingConstraint = logoImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 60)
        
        NSLayoutConstraint.activate([
            topSpacingConstraint,
            logoImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            logoHeightConstraint,
            logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor)
        ])
        
        // Title Label Constraints
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 30),
            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20)
        ])
        
        // Subtitle Label Constraints
        NSLayoutConstraint.activate([
            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
            subtitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            subtitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20)
        ])
        
        // Email TextField Constraints
        NSLayoutConstraint.activate([
            emailTextField.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 40),
            emailTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            emailTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            emailTextField.heightAnchor.constraint(equalToConstant: 50)
        ])
        
        // Password TextField Constraints
        NSLayoutConstraint.activate([
            passwordTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 16),
            passwordTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            passwordTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            passwordTextField.heightAnchor.constraint(equalToConstant: 50)
        ])
        
        // Login Button Constraints
        NSLayoutConstraint.activate([
            loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 30),
            loginButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            loginButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            loginButton.heightAnchor.constraint(equalToConstant: 56),
            loginButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -40)
        ])
    }
    
    // MARK: - Adaptive Layout
    
    private func updateLayoutForTraitCollection() {
        let horizontalClass = traitCollection.horizontalSizeClass
        let verticalClass = traitCollection.verticalSizeClass
        
        // Size Class์— ๋”ฐ๋ฅธ ๋ ˆ์ด์•„์›ƒ ์กฐ์ •
        switch (horizontalClass, verticalClass) {
        case (.regular, .regular):
            // iPad - ๋„“์€ ํ™”๋ฉด
            contentWidthConstraint.isActive = false
            contentWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: 500)
            contentWidthConstraint.isActive = true
            
            logoHeightConstraint.constant = 150
            topSpacingConstraint.constant = 80
            titleLabel.font = .systemFont(ofSize: 38, weight: .bold)
            subtitleLabel.font = .systemFont(ofSize: 18, weight: .regular)
            
        case (.compact, .regular):
            // iPhone ์„ธ๋กœ๋ชจ๋“œ
            contentWidthConstraint.isActive = false
            contentWidthConstraint = contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.9)
            contentWidthConstraint.isActive = true
            
            logoHeightConstraint.constant = 120
            topSpacingConstraint.constant = 60
            titleLabel.font = .systemFont(ofSize: 32, weight: .bold)
            subtitleLabel.font = .systemFont(ofSize: 16, weight: .regular)
            
        case (.regular, .compact), (.compact, .compact):
            // ๊ฐ€๋กœ๋ชจ๋“œ - ๊ณต๊ฐ„ ์ ˆ์•ฝ
            contentWidthConstraint.isActive = false
            contentWidthConstraint = contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 0.7)
            contentWidthConstraint.isActive = true
            
            logoHeightConstraint.constant = 80
            topSpacingConstraint.constant = 30
            titleLabel.font = .systemFont(ofSize: 28, weight: .bold)
            subtitleLabel.font = .systemFont(ofSize: 14, weight: .regular)
            
        default:
            break
        }
        
        // ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
    
    // MARK: - Keyboard Handling
    
    private func setupKeyboardObservers() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    
    @objc private func keyboardWillShow(_ notification: Notification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else {
            return
        }
        
        let keyboardHeight = keyboardFrame.height
        let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
        
        scrollView.contentInset = contentInsets
        scrollView.scrollIndicatorInsets = contentInsets
        
        // ํ™œ์„ฑํ™”๋œ ํ…์ŠคํŠธ ํ•„๋“œ๊ฐ€ ๋ณด์ด๋„๋ก ์Šคํฌ๋กค
        if let activeField = [emailTextField, passwordTextField].first(where: { $0.isFirstResponder }) {
            let rect = activeField.convert(activeField.bounds, to: scrollView)
            scrollView.scrollRectToVisible(rect, animated: true)
        }
    }
    
    @objc private func keyboardWillHide(_ notification: Notification) {
        scrollView.contentInset = .zero
        scrollView.scrollIndicatorInsets = .zero
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

โœ… ์™„์„ฑ! ์ด ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด:

1. ๋ชจ๋“  ๋””๋ฐ”์ด์Šค ํฌ๊ธฐ์— ์ž๋™ ๋Œ€์‘
2. Size Classes ๊ธฐ๋ฐ˜ ์ ์‘ํ˜• ๋ ˆ์ด์•„์›ƒ
3. ํ‚ค๋ณด๋“œ ํ‘œ์‹œ ์‹œ ์ž๋™ ์Šคํฌ๋กค
4. ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ „ํ™˜
5. Safe Area ์™„๋ฒฝ ๋Œ€์‘

๐ŸŽฏ ๊ณ ๊ธ‰ ํ…Œํฌ๋‹‰: Stack View ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

์•ผ, ์†”์งํžˆ ๋งํ•˜๋ฉด Auto Layout ์ œ์•ฝ์กฐ๊ฑด ์ผ์ผ์ด ๋‹ค ์„ค์ •ํ•˜๋Š” ๊ฑฐ ์ง„์งœ ๊ท€์ฐฎ์ง€ ์•Š์•„? ๐Ÿ˜… ๊ทธ๋ž˜์„œ ์• ํ”Œ์ด ๋งŒ๋“  ๊ฒŒ ๋ฐ”๋กœ UIStackView์•ผ! ์ด๊ฑฐ ์ง„์งœ ํ˜๋ช…์ ์ด์•ผ.

๐ŸŒŸ Stack View์˜ ์žฅ์ 

โ€ข ์ œ์•ฝ์กฐ๊ฑด์„ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•ด์คŒ
โ€ข ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•ด์ง
โ€ข ๋™์ ์œผ๋กœ ๋ทฐ๋ฅผ ์ถ”๊ฐ€/์ œ๊ฑฐํ•˜๊ธฐ ์‰ฌ์›€
โ€ข ์ค‘์ฒฉํ•ด์„œ ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ๋„ ๊ฐ€๋Šฅ
โ€ข Size Classes์™€ ์™„๋ฒฝํ•˜๊ฒŒ ํ˜ธํ™˜๋จ

๐Ÿ“š Stack View ์†์„ฑ ์™„์ „ ์ •๋ณต

์†์„ฑ ์„ค๋ช… ์ฃผ์š” ๊ฐ’
axis ๋ฐฐ์น˜ ๋ฐฉํ–ฅ .horizontal (๊ฐ€๋กœ), .vertical (์„ธ๋กœ)
distribution ๊ณต๊ฐ„ ๋ถ„๋ฐฐ ๋ฐฉ์‹ .fill, .fillEqually, .fillProportionally, .equalSpacing, .equalCentering
alignment ์ •๋ ฌ ๋ฐฉ์‹ .fill, .leading, .center, .trailing, .top, .bottom, .firstBaseline, .lastBaseline
spacing ๋ทฐ ์‚ฌ์ด ๊ฐ„๊ฒฉ CGFloat ๊ฐ’ (์˜ˆ: 10, 20)

์‹ค์ „ ์˜ˆ์‹œ๋ฅผ ๋ณด์ž:

class StackViewMasterViewController: UIViewController {
    
    // MARK: - ๊ธฐ๋ณธ Stack View ์„ค์ •
    
    func createBasicStackView() -> UIStackView {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // ๊ธฐ๋ณธ ์†์„ฑ ์„ค์ •
        stackView.axis = .vertical
        stackView.distribution = .fill
        stackView.alignment = .fill
        stackView.spacing = 16
        
        return stackView
    }
    
    // MARK: - Distribution ์˜ˆ์‹œ
    
    func demonstrateDistributions() {
        // 1. Fill - ํ•˜๋‚˜์˜ ๋ทฐ๊ฐ€ ๋‚จ์€ ๊ณต๊ฐ„์„ ๋‹ค ์ฐจ์ง€
        let fillStack = UIStackView()
        fillStack.distribution = .fill
        // Content Hugging Priority๊ฐ€ ๋‚ฎ์€ ๋ทฐ๊ฐ€ ๋Š˜์–ด๋‚จ
        
        // 2. Fill Equally - ๋ชจ๋“  ๋ทฐ๊ฐ€ ๊ฐ™์€ ํฌ๊ธฐ
        let equalStack = UIStackView()
        equalStack.distribution = .fillEqually
        // ๋ชจ๋“  ๋ทฐ์˜ ํฌ๊ธฐ๊ฐ€ ๋™์ผํ•˜๊ฒŒ ์กฐ์ •๋จ
        
        // 3. Fill Proportionally - ๋น„์œจ์— ๋งž์ถฐ ๋ถ„๋ฐฐ
        let proportionalStack = UIStackView()
        proportionalStack.distribution = .fillProportionally
        // Intrinsic Content Size ๋น„์œจ๋Œ€๋กœ ๊ณต๊ฐ„ ๋ถ„๋ฐฐ
        
        // 4. Equal Spacing - ๋ทฐ ์‚ฌ์ด ๊ฐ„๊ฒฉ์ด ๋™์ผ
        let spacingStack = UIStackView()
        spacingStack.distribution = .equalSpacing
        // ๋ทฐ๋Š” ๊ณ ์œ  ํฌ๊ธฐ ์œ ์ง€, ๊ฐ„๊ฒฉ๋งŒ ์กฐ์ •
        
        // 5. Equal Centering - ์ค‘์‹ฌ์  ์‚ฌ์ด ๊ฑฐ๋ฆฌ๊ฐ€ ๋™์ผ
        let centeringStack = UIStackView()
        centeringStack.distribution = .equalCentering
        // ๊ฐ ๋ทฐ์˜ ์ค‘์‹ฌ์  ์‚ฌ์ด ๊ฑฐ๋ฆฌ๊ฐ€ ๊ฐ™์Œ
    }
    
    // MARK: - ์ค‘์ฒฉ Stack View๋กœ ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ
    
    func createComplexLayout() -> UIView {
        // ๋ฉ”์ธ ์„ธ๋กœ ์Šคํƒ
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 20
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // ํ—ค๋” ์˜์—ญ
        let headerStack = UIStackView()
        headerStack.axis = .horizontal
        headerStack.spacing = 12
        headerStack.alignment = .center
        
        let profileImage = UIImageView()
        profileImage.widthAnchor.constraint(equalToConstant: 50).isActive = true
        profileImage.heightAnchor.constraint(equalToConstant: 50).isActive = true
        
        let nameLabel = UILabel()
        nameLabel.text = "ํ™๊ธธ๋™"
        nameLabel.font = .systemFont(ofSize: 18, weight: .bold)
        
        headerStack.addArrangedSubview(profileImage)
        headerStack.addArrangedSubview(nameLabel)
        
        // ์ฝ˜ํ…์ธ  ์˜์—ญ - ๊ฐ€๋กœ ์Šคํƒ
        let contentStack = UIStackView()
        contentStack.axis = .horizontal
        contentStack.spacing = 16
        contentStack.distribution = .fillEqually
        
        for i in 1...3 {
            let card = createCardView(title: "์นด๋“œ \(i)")
            contentStack.addArrangedSubview(card)
        }
        
        // ๋ฒ„ํŠผ ์˜์—ญ
        let buttonStack = UIStackView()
        buttonStack.axis = .horizontal
        buttonStack.spacing = 12
        buttonStack.distribution = .fillEqually
        
        let cancelButton = UIButton(type: .system)
        cancelButton.setTitle("์ทจ์†Œ", for: .normal)
        cancelButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
        
        let confirmButton = UIButton(type: .system)
        confirmButton.setTitle("ํ™•์ธ", for: .normal)
        confirmButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
        
        buttonStack.addArrangedSubview(cancelButton)
        buttonStack.addArrangedSubview(confirmButton)
        
        // ๋ชจ๋‘ ๋ฉ”์ธ ์Šคํƒ์— ์ถ”๊ฐ€
        mainStack.addArrangedSubview(headerStack)
        mainStack.addArrangedSubview(contentStack)
        mainStack.addArrangedSubview(buttonStack)
        
        return mainStack
    }
    
    func createCardView(title: String) -> UIView {
        let container = UIView()
        container.backgroundColor = .systemGray6
        container.layer.cornerRadius = 12
        
        let label = UILabel()
        label.text = title
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        
        container.addSubview(label)
        
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
            container.heightAnchor.constraint(equalToConstant: 100)
        ])
        
        return container
    }
    
    // MARK: - ๋™์  Stack View ๊ด€๋ฆฌ
    
    func dynamicStackViewManagement() {
        let stackView = UIStackView()
        
        // ๋ทฐ ์ถ”๊ฐ€
        let newView = UILabel()
        newView.text = "์ƒˆ๋กœ์šด ํ•ญ๋ชฉ"
        stackView.addArrangedSubview(newView)
        
        // ํŠน์ • ์œ„์น˜์— ์‚ฝ์ž…
        let insertView = UILabel()
        insertView.text = "์‚ฝ์ž…๋œ ํ•ญ๋ชฉ"
        stackView.insertArrangedSubview(insertView, at: 0)
        
        // ๋ทฐ ์ œ๊ฑฐ (์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜)
        UIView.animate(withDuration: 0.3, animations: {
            newView.isHidden = true
        }) { _ in
            stackView.removeArrangedSubview(newView)
            newView.removeFromSuperview()
        }
        
        // ๋ชจ๋“  ๋ทฐ ์ œ๊ฑฐ
        stackView.arrangedSubviews.forEach { view in
            stackView.removeArrangedSubview(view)
            view.removeFromSuperview()
        }
    }
    
    // MARK: - Size Classes์™€ Stack View ์กฐํ•ฉ
    
    func adaptiveStackView() -> UIStackView {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // ์ดˆ๊ธฐ ์„ค์ •
        updateStackViewForTraitCollection(stackView)
        
        return stackView
    }
    
    func updateStackViewForTraitCollection(_ stackView: UIStackView) {
        if traitCollection.horizontalSizeClass == .regular {
            // iPad: ๊ฐ€๋กœ ๋ฐฐ์น˜
            stackView.axis = .horizontal
            stackView.spacing = 30
            stackView.distribution = .fillEqually
        } else {
            // iPhone: ์„ธ๋กœ ๋ฐฐ์น˜
            stackView.axis = .vertical
            stackView.spacing = 16
            stackView.distribution = .fill
        }
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        // Stack View ์—…๋ฐ์ดํŠธ
        view.subviews.compactMap { $0 as? UIStackView }.forEach { stackView in
            updateStackViewForTraitCollection(stackView)
        }
    }
}

๐Ÿ’ก Stack View ๊ฟ€ํŒ:
1. isHidden ํ™œ์šฉ: arrangedSubview์˜ isHidden์„ true๋กœ ์„ค์ •ํ•˜๋ฉด Stack View๊ฐ€ ์ž๋™์œผ๋กœ ๋ ˆ์ด์•„์›ƒ์„ ์žฌ์กฐ์ •ํ•ด์ค˜. ๋ทฐ๋ฅผ ์ œ๊ฑฐํ•˜์ง€ ์•Š๊ณ ๋„ ์ˆจ๊ธธ ์ˆ˜ ์žˆ์–ด์„œ ํŽธ๋ฆฌํ•ด!

2. setCustomSpacing: ํŠน์ • ๋ทฐ ๋‹ค์Œ์—๋งŒ ๋‹ค๋ฅธ ๊ฐ„๊ฒฉ์„ ์ฃผ๊ณ  ์‹ถ๋‹ค๋ฉด stackView.setCustomSpacing(30, after: someView) ์‚ฌ์šฉ!

3. ์ค‘์ฒฉ ์ œํ•œ: Stack View๋ฅผ ๋„ˆ๋ฌด ๋งŽ์ด ์ค‘์ฒฉํ•˜๋ฉด ์„ฑ๋Šฅ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์–ด. 3-4๋‹จ๊ณ„ ์ด์ƒ์€ ํ”ผํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

๐Ÿ” ๋””๋ฒ„๊น…๊ณผ ๋ฌธ์ œ ํ•ด๊ฒฐ

Auto Layout ์ž‘์—…ํ•˜๋‹ค ๋ณด๋ฉด ์ œ์•ฝ์กฐ๊ฑด ์ถฉ๋Œ์ด๋‚˜ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ ˆ์ด์•„์›ƒ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๊ธฐ ๋งˆ๋ จ์ด์•ผ. ์ด๋Ÿด ๋•Œ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž! ๐Ÿ”ง

โš ๏ธ ํ”ํ•œ ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ๋ฒ•

๋ฌธ์ œ 1: ์ œ์•ฝ์กฐ๊ฑด ์ถฉ๋Œ (Constraint Conflict)

์ฝ˜์†”์— ์ด๋Ÿฐ ์—๋Ÿฌ๊ฐ€ ๋œฌ ์  ์žˆ์ง€?

Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want...


ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:
1. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ž์„ธํžˆ ์ฝ์–ด๋ด. ์–ด๋–ค ์ œ์•ฝ์กฐ๊ฑด๋“ค์ด ์ถฉ๋Œํ•˜๋Š”์ง€ ๋‚˜์™€ ์žˆ์–ด.
2. ๋ถˆํ•„์š”ํ•œ ์ œ์•ฝ์กฐ๊ฑด์„ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์กฐ์ •ํ•ด.
3. translatesAutoresizingMaskIntoConstraints = false๋ฅผ ๋นผ๋จน์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธ!

// ์ถฉ๋Œ ํ•ด๊ฒฐ ์˜ˆ์‹œ
class ConflictResolutionExample: UIViewController {
    
    func resolveConflict() {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(view)
        
        // ๋ฌธ์ œ: ๊ณ ์ • ๋„ˆ๋น„์™€ ์–‘์ชฝ ์—ฌ๋ฐฑ์„ ๋™์‹œ์— ์„ค์ •
        let widthConstraint = view.widthAnchor.constraint(equalToConstant: 300)
        let leadingConstraint = view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20)
        let trailingConstraint = view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20)
        
        // ํ•ด๊ฒฐ 1: ์šฐ์„ ์ˆœ์œ„ ์กฐ์ •
        widthConstraint.priority = .defaultHigh // 750
        trailingConstraint.priority = .defaultLow // 250
        
        NSLayoutConstraint.activate([
            widthConstraint,
            leadingConstraint,
            trailingConstraint,
            view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])
        
        // ๊ฒฐ๊ณผ: ๊ฐ€๋Šฅํ•˜๋ฉด 300 ๋„ˆ๋น„๋ฅผ ์œ ์ง€ํ•˜๋˜, 
        // ํ™”๋ฉด์ด ์ž‘์œผ๋ฉด trailing ์ œ์•ฝ์ด ์–‘๋ณดํ•จ
    }
    
    func betterApproach() {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(view)
        
        // ํ•ด๊ฒฐ 2: ์ตœ๋Œ€/์ตœ์†Œ ์ œ์•ฝ์กฐ๊ฑด ์‚ฌ์šฉ
        NSLayoutConstraint.activate([
            view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20),
            view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20),
            view.widthAnchor.constraint(lessThanOrEqualToConstant: 300), // ์ตœ๋Œ€ 300
            view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])
        
        // ๊ฒฐ๊ณผ: ํ™”๋ฉด์ด ๋„“์œผ๋ฉด 300, ์ข์œผ๋ฉด ์–‘์ชฝ 20 ์—ฌ๋ฐฑ ์œ ์ง€
    }
}

๋ฌธ์ œ 2: Ambiguous Layout (๋ชจํ˜ธํ•œ ๋ ˆ์ด์•„์›ƒ)

์ œ์•ฝ์กฐ๊ฑด์ด ๋ถ€์กฑํ•ด์„œ ๋ทฐ์˜ ์œ„์น˜๋‚˜ ํฌ๊ธฐ๋ฅผ ์ •ํ™•ํžˆ ๊ฒฐ์ •ํ•  ์ˆ˜ ์—†์„ ๋•Œ ๋ฐœ์ƒํ•ด.


ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:
1. ๋ชจ๋“  ๋ทฐ๋Š” X, Y ์œ„์น˜์™€ Width, Height๊ฐ€ ๋ช…ํ™•ํ•ด์•ผ ํ•ด.
2. Intrinsic Content Size๊ฐ€ ์žˆ๋Š” ๋ทฐ๋Š” ํฌ๊ธฐ ์ œ์•ฝ์ด ์„ ํƒ์‚ฌํ•ญ์ด์ง€๋งŒ, ์—†๋Š” ๋ทฐ๋Š” ํ•„์ˆ˜์•ผ.
3. hasAmbiguousLayout ์†์„ฑ์œผ๋กœ ํ™•์ธ ๊ฐ€๋Šฅํ•ด.

// Ambiguous Layout ๋””๋ฒ„๊น…
extension UIView {
    func debugAmbiguousLayout() {
        #if DEBUG
        if self.hasAmbiguousLayout {
            print("โš ๏ธ Ambiguous Layout ๋ฐœ๊ฒฌ: \(self)")
            print("ํ˜„์žฌ ํ”„๋ ˆ์ž„: \(self.frame)")
            print("์ œ์•ฝ์กฐ๊ฑด๋“ค:")
            self.constraints.forEach { constraint in
                print("  - \(constraint)")
            }
        }
        
        // ๋ชจ๋“  ์„œ๋ธŒ๋ทฐ๋„ ํ™•์ธ
        for subview in self.subviews {
            subview.debugAmbiguousLayout()
        }
        #endif
    }
    
    func exerciseAmbiguityInLayout() {
        #if DEBUG
        // ๊ฐ€๋Šฅํ•œ ๋ ˆ์ด์•„์›ƒ๋“ค์„ ์ˆœํ™˜ํ•˜๋ฉฐ ๋ณด์—ฌ์คŒ (๋””๋ฒ„๊น…์šฉ)
        if self.hasAmbiguousLayout {
            self.exerciseAmbiguityInLayout()
        }
        #endif
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    #if DEBUG
    view.debugAmbiguousLayout()
    #endif
}

๐Ÿ› ๏ธ ๋””๋ฒ„๊น… ๋„๊ตฌ ํ™œ์šฉํ•˜๊ธฐ

1. Visual Debugger

Xcode์˜ Debug View Hierarchy๋Š” ์ง„์งœ ๊ฐ•๋ ฅํ•ด! ์•ฑ ์‹คํ–‰ ์ค‘์— "Debug View Hierarchy" ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด 3D๋กœ ๋ทฐ ๊ณ„์ธต์„ ๋ณผ ์ˆ˜ ์žˆ์–ด. ์ œ์•ฝ์กฐ๊ฑด๋„ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๊ณ ! ๐ŸŽจ

2. Constraint Identifier

์ œ์•ฝ์กฐ๊ฑด์— ์ด๋ฆ„์„ ๋ถ™์—ฌ์„œ ๋””๋ฒ„๊น…์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด:

let constraint = view.widthAnchor.constraint(equalToConstant: 200)
constraint.identifier = "MainView.Width"
constraint.isActive = true

// ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์—์„œ "MainView.Width"๋กœ ํ‘œ์‹œ๋˜์–ด ์ฐพ๊ธฐ ์‰ฌ์›€!

3. ๋Ÿฐํƒ€์ž„ ์ œ์•ฝ์กฐ๊ฑด ์ˆ˜์ •

์•ฑ ์‹คํ–‰ ์ค‘์— ์ œ์•ฝ์กฐ๊ฑด์„ ๋™์ ์œผ๋กœ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์–ด:

class DynamicConstraintViewController: UIViewController {
    
    var widthConstraint: NSLayoutConstraint!
    
    func setupView() {
        let myView = UIView()
        myView.translatesAutoresizingMaskIntoConstraints = false
        myView.backgroundColor = .systemBlue
        view.addSubview(myView)
        
        widthConstraint = myView.widthAnchor.constraint(equalToConstant: 100)
        
        NSLayoutConstraint.activate([
            myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            widthConstraint,
            myView.heightAnchor.constraint(equalToConstant: 100)
        ])
    }
    
    func animateWidthChange(to newWidth: CGFloat) {
        // ์ œ์•ฝ์กฐ๊ฑด ๊ฐ’ ๋ณ€๊ฒฝ
        widthConstraint.constant = newWidth
        
        // ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ
        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
            self.view.layoutIfNeeded()
        }
    }
    
    func toggleConstraint() {
        // ์ œ์•ฝ์กฐ๊ฑด ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”
        widthConstraint.isActive.toggle()
        
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
}
Auto Layout ๋””๋ฒ„๊น… ํ”„๋กœ์„ธ์Šค ๋ฌธ์ œ ๋ฐœ๊ฒฌ ๋ ˆ์ด์•„์›ƒ ๊นจ์ง ์ฝ˜์†” ํ™•์ธ ์ œ์•ฝ์กฐ๊ฑด ์ถฉ๋Œ ๋ฉ”์‹œ์ง€ ๋ถ„์„ Visual Debugger 3D ๋ทฐ ๊ณ„์ธต ์ œ์•ฝ์กฐ๊ฑด ์‹œ๊ฐํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ ์ œ์•ฝ์กฐ๊ฑด ์ˆ˜์ • ๋””๋ฒ„๊น… ์ฒดํฌ๋ฆฌ์ŠคํŠธ translatesAutoresizingMaskIntoConstraints = false ์„ค์ • ํ™•์ธ ๋ชจ๋“  ๋ทฐ์˜ X, Y, Width, Height ์ œ์•ฝ์กฐ๊ฑด ์กด์žฌ ํ™•์ธ ์ œ์•ฝ์กฐ๊ฑด ์šฐ์„ ์ˆœ์œ„ ์ถฉ๋Œ ๊ฒ€ํ†  Content Hugging / Compression Resistance ์„ค์ • ํ™•์ธ hasAmbiguousLayout ์†์„ฑ์œผ๋กœ ๋ชจํ˜ธํ•œ ๋ ˆ์ด์•„์›ƒ ๊ฒ€์‚ฌ

๐ŸŽจ ์‹ค์ „ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์ž, ์ด์ œ ์ง„์งœ ํ”„๋กœ์ฒ˜๋Ÿผ Auto Layout์„ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ค„๊ฒŒ! ์ด๊ฑด ๋‚ด๊ฐ€ ์ˆ˜๋…„๊ฐ„ ์‚ฝ์งˆํ•˜๋ฉด์„œ ๋ฐฐ์šด ๋…ธํ•˜์šฐ์•ผ. ๐Ÿ˜Ž

๐Ÿ’Ž ํ™ฉ๊ธˆ ๊ทœ์น™ 10๊ฐ€์ง€

1. Safe Area๋ฅผ ํ•ญ์ƒ ๊ณ ๋ คํ•˜๋ผ

iPhone X ์ดํ›„๋กœ ๋…ธ์น˜์™€ ํ™ˆ ์ธ๋””์ผ€์ดํ„ฐ ๋•Œ๋ฌธ์— Safe Area๊ฐ€ ์ค‘์š”ํ•ด์กŒ์–ด. ์ ˆ๋Œ€ ๋ฌด์‹œํ•˜์ง€ ๋งˆ!

// โŒ ๋‚˜์œ ์˜ˆ
view.topAnchor.constraint(equalTo: superview.topAnchor)

// โœ… ์ข‹์€ ์˜ˆ
view.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor)

// ๋” ๋‚˜์€ ์˜ˆ - ์ƒํ™ฉ์— ๋”ฐ๋ผ ์„ ํƒ
if #available(iOS 11.0, *) {
    view.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor).isActive = true
} else {
    view.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
}

2. Layout Margins ํ™œ์šฉํ•˜๊ธฐ

์‹œ์Šคํ…œ ๊ธฐ๋ณธ ์—ฌ๋ฐฑ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ผ๊ด€์„ฑ ์žˆ๋Š” UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด:

// Layout Margins ์‚ฌ์šฉ
view.leadingAnchor.constraint(equalTo: superview.layoutMarginsGuide.leadingAnchor)

// ์ปค์Šคํ…€ ๋งˆ์ง„ ์„ค์ •
superview.directionalLayoutMargins = NSDirectionalEdgeInsets(
    top: 20,
    leading: 16,
    bottom: 20,
    trailing: 16
)

// Readable Content Guide (ํ…์ŠคํŠธ ๊ฐ€๋…์„ฑ ์ตœ์ ํ™”)
label.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor)
label.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor)
// iPad์—์„œ ํ…์ŠคํŠธ๊ฐ€ ๋„ˆ๋ฌด ๋„“๊ฒŒ ํŽผ์ณ์ง€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€!

3. ์ œ์•ฝ์กฐ๊ฑด์„ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ผ

๊ฐ™์€ ์ œ์•ฝ์กฐ๊ฑด์„ ์—ฌ๋Ÿฌ ๋ฒˆ ๋งŒ๋“ค์ง€ ๋ง๊ณ , ๋ณ€์ˆ˜๋กœ ์ €์žฅํ•ด์„œ ์žฌ์‚ฌ์šฉํ•ด:

class ReusableConstraintsViewController: UIViewController {
    
    // ์ œ์•ฝ์กฐ๊ฑด์„ ํ”„๋กœํผํ‹ฐ๋กœ ์ €์žฅ
    private var compactConstraints: [NSLayoutConstraint] = []
    private var regularConstraints: [NSLayoutConstraint] = []
    
    func setupConstraints() {
        let myView = UIView()
        myView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myView)
        
        // ๊ณตํ†ต ์ œ์•ฝ์กฐ๊ฑด
        let commonConstraints = [
            myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            myView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ]
        
        // Compact์šฉ ์ œ์•ฝ์กฐ๊ฑด
        compactConstraints = [
            myView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
            myView.heightAnchor.constraint(equalToConstant: 200)
        ]
        
        // Regular์šฉ ์ œ์•ฝ์กฐ๊ฑด
        regularConstraints = [
            myView.widthAnchor.constraint(equalToConstant: 400),
            myView.heightAnchor.constraint(equalToConstant: 300)
        ]
        
        // ๊ณตํ†ต ์ œ์•ฝ์กฐ๊ฑด์€ ํ•ญ์ƒ ํ™œ์„ฑํ™”
        NSLayoutConstraint.activate(commonConstraints)
        
        // Size Class์— ๋”ฐ๋ผ ์„ ํƒ์  ํ™œ์„ฑํ™”
        updateConstraintsForTraitCollection()
    }
    
    func updateConstraintsForTraitCollection() {
        if traitCollection.horizontalSizeClass == .compact {
            NSLayoutConstraint.deactivate(regularConstraints)
            NSLayoutConstraint.activate(compactConstraints)
        } else {
            NSLayoutConstraint.deactivate(compactConstraints)
            NSLayoutConstraint.activate(regularConstraints)
        }
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
            UIView.animate(withDuration: 0.3) {
                self.updateConstraintsForTraitCollection()
                self.view.layoutIfNeeded()
            }
        }
    }
}

4. ์„ฑ๋Šฅ์„ ๊ณ ๋ คํ•œ ์ œ์•ฝ์กฐ๊ฑด ๊ด€๋ฆฌ

์ œ์•ฝ์กฐ๊ฑด์„ ๋„ˆ๋ฌด ์ž์ฃผ ์ถ”๊ฐ€/์ œ๊ฑฐํ•˜๋ฉด ์„ฑ๋Šฅ์ด ๋–จ์–ด์ ธ. ๋Œ€์‹  ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”๋ฅผ ์‚ฌ์šฉํ•ด:

// โŒ ๋‚˜์œ ์˜ˆ - ๋งค๋ฒˆ ์ œ๊ฑฐํ•˜๊ณ  ๋‹ค์‹œ ์ถ”๊ฐ€
func updateLayout() {
    view.removeConstraints(view.constraints)
    // ์ƒˆ๋กœ์šด ์ œ์•ฝ์กฐ๊ฑด ์ถ”๊ฐ€...
}

// โœ… ์ข‹์€ ์˜ˆ - ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”
func updateLayout() {
    oldConstraint.isActive = false
    newConstraint.isActive = true
}

// ๋” ๋‚˜์€ ์˜ˆ - ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌ
func updateLayout() {
    NSLayoutConstraint.deactivate(oldConstraints)
    NSLayoutConstraint.activate(newConstraints)
}

5. ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ

์ œ์•ฝ์กฐ๊ฑด ๋ณ€๊ฒฝ์„ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ํ•˜๋Š” ๋ฐฉ๋ฒ•:

class AnimatedConstraintViewController: UIViewController {
    
    var heightConstraint: NSLayoutConstraint!
    
    func animateHeightChange() {
        // 1. ์ œ์•ฝ์กฐ๊ฑด ๊ฐ’ ๋ณ€๊ฒฝ
        heightConstraint.constant = 300
        
        // 2. ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ธ”๋ก ์•ˆ์—์„œ layoutIfNeeded() ํ˜ธ์ถœ
        UIView.animate(
            withDuration: 0.5,
            delay: 0,
            usingSpringWithDamping: 0.7,
            initialSpringVelocity: 0.5,
            options: .curveEaseInOut
        ) {
            self.view.layoutIfNeeded()
        } completion: { finished in
            print("์• ๋‹ˆ๋ฉ”์ด์…˜ ์™„๋ฃŒ!")
        }
    }
    
    func animateMultipleConstraints() {
        // ์—ฌ๋Ÿฌ ์ œ์•ฝ์กฐ๊ฑด์„ ๋™์‹œ์— ์• ๋‹ˆ๋ฉ”์ด์…˜
        widthConstraint.constant = 200
        heightConstraint.constant = 200
        centerXConstraint.constant = 50
        
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
    
    func animateWithKeyframes() {
        // ํ‚คํ”„๋ ˆ์ž„ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ๋‹จ๊ณ„์  ๋ณ€ํ™”
        UIView.animateKeyframes(
            withDuration: 1.0,
            delay: 0,
            options: []
        ) {
            // ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„ (0~0.5์ดˆ)
            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
                self.heightConstraint.constant = 200
                self.view.layoutIfNeeded()
            }
            
            // ๋‘ ๋ฒˆ์งธ ๋‹จ๊ณ„ (0.5~1.0์ดˆ)
            UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
                self.widthConstraint.constant = 300
                self.view.layoutIfNeeded()
            }
        }
    }
}

๐ŸŽฏ ์‹ค๋ฌด์—์„œ ์ž์ฃผ ์“ฐ๋Š” ํŒจํ„ด

ํŒจํ„ด 1: ๋™์  ๋†’์ด ํ…Œ์ด๋ธ” ๋ทฐ ์…€

๋‚ด์šฉ์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ๋†’์ด๊ฐ€ ์กฐ์ ˆ๋˜๋Š” ์…€:

class DynamicHeightTableViewCell: UITableViewCell {
    
    let titleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .systemFont(ofSize: 18, weight: .bold)
        label.numberOfLines = 0 // ์—ฌ๋Ÿฌ ์ค„ ํ—ˆ์šฉ
        return label
    }()
    
    let descriptionLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .systemFont(ofSize: 14)
        label.textColor = .secondaryLabel
        label.numberOfLines = 0
        return label
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        contentView.addSubview(titleLabel)
        contentView.addSubview(descriptionLabel)
        
        NSLayoutConstraint.activate([
            // Title Label
            titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            
            // Description Label
            descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
            descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            descriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            descriptionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16)
            // ์ค‘์š”: ๋งˆ์ง€๋ง‰ ๋ทฐ์˜ bottom์„ contentView.bottom์— ์—ฐ๊ฒฐ!
        ])
    }
}

// ViewController์—์„œ ์„ค์ •
class TableViewController: UITableViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ์ž๋™ ๋†’์ด ๊ณ„์‚ฐ ํ™œ์„ฑํ™”
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 100 // ์˜ˆ์ƒ ๋†’์ด (์„ฑ๋Šฅ ์ตœ์ ํ™”)
    }
}

ํŒจํ„ด 2: ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ํผ

ํ‚ค๋ณด๋“œ๊ฐ€ ์˜ฌ๋ผ์™€๋„ ๋ชจ๋“  ํ•„๋“œ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ํผ:

class ScrollableFormViewController: UIViewController {
    
    private let scrollView = UIScrollView()
    private let contentView = UIView()
    private var activeTextField: UITextField?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupScrollView()
        setupForm()
        setupKeyboardHandling()
    }
    
    private func setupScrollView() {
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
            // ์ค‘์š”: width๋Š” scrollView์™€ ๊ฐ™๊ฒŒ!
        ])
    }
    
    private func setupForm() {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.spacing = 20
        
        contentView.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
            stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
            // ์ค‘์š”: ๋งˆ์ง€๋ง‰ ๋ทฐ์˜ bottom์„ contentView.bottom์— ์—ฐ๊ฒฐ!
        ])
        
        // ํผ ํ•„๋“œ๋“ค ์ถ”๊ฐ€
        let fields = ["์ด๋ฆ„", "์ด๋ฉ”์ผ", "์ „ํ™”๋ฒˆํ˜ธ", "์ฃผ์†Œ", "์ƒ์„ธ์ฃผ์†Œ", "์šฐํŽธ๋ฒˆํ˜ธ"]
        fields.forEach { placeholder in
            let textField = UITextField()
            textField.placeholder = placeholder
            textField.borderStyle = .roundedRect
            textField.heightAnchor.constraint(equalToConstant: 50).isActive = true
            textField.delegate = self
            stackView.addArrangedSubview(textField)
        }
    }
    
    private func setupKeyboardHandling() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
        
        // ํ™”๋ฉด ํƒญํ•˜๋ฉด ํ‚ค๋ณด๋“œ ์ˆจ๊ธฐ๊ธฐ
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
        view.addGestureRecognizer(tapGesture)
    }
    
    @objc private func keyboardWillShow(_ notification: Notification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
              let activeTextField = activeTextField else {
            return
        }
        
        let keyboardHeight = keyboardFrame.height
        let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight + 20, right: 0)
        
        scrollView.contentInset = contentInsets
        scrollView.scrollIndicatorInsets = contentInsets
        
        // ํ™œ์„ฑ ํ…์ŠคํŠธ ํ•„๋“œ๊ฐ€ ๋ณด์ด๋„๋ก ์Šคํฌ๋กค
        let textFieldFrame = activeTextField.convert(activeTextField.bounds, to: scrollView)
        scrollView.scrollRectToVisible(textFieldFrame, animated: true)
    }
    
    @objc private func keyboardWillHide(_ notification: Notification) {
        scrollView.contentInset = .zero
        scrollView.scrollIndicatorInsets = .zero
    }
    
    @objc private func dismissKeyboard() {
        view.endEditing(true)
    }
}

extension ScrollableFormViewController: UITextFieldDelegate {
    func textFieldDidBeginEditing(_ textField: UITextField) {
        activeTextField = textField
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        activeTextField = nil
    }
}

โœ… ํ”„๋กœ ํŒ: ์Šคํฌ๋กค ๋ทฐ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” contentView์˜ ๋ชจ๋“  edge(top, leading, trailing, bottom)๋ฅผ scrollView์— ์—ฐ๊ฒฐํ•˜๊ณ , width๋Š” scrollView์™€ ๊ฐ™๊ฒŒ ์„ค์ •ํ•ด์•ผ ํ•ด. ๊ทธ๋ž˜์•ผ ์Šคํฌ๋กค์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•ด!

๐ŸŒŸ ์žฌ๋Šฅ๋„ท์—์„œ ๋ฐฐ์šฐ๋Š” ์‹ค์ „ ๋…ธํ•˜์šฐ

์žˆ์ž–์•„, ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์— ๊ฐ€๋ณด๋ฉด Auto Layout๊ณผ UI ๋””์ž์ธ ์ „๋ฌธ๊ฐ€๋“ค์ด ์ •๋ง ๋งŽ์•„. ๋‚˜๋„ ๊ฑฐ๊ธฐ์„œ ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ๊ฑฐ๋“ ! ๐ŸŽ“

ํŠนํžˆ ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ž์‹ ๋งŒ์˜ ๋…ธํ•˜์šฐ๋ฅผ ๊ณต์œ ํ•˜๋Š”๋ฐ, ์ฑ…์ด๋‚˜ ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋Š” ๋ฐฐ์šธ ์ˆ˜ ์—†๋Š” ์‹ค์ „ ํŒ๋“ค์ด ๊ฐ€๋“ํ•ด. ์˜ˆ๋ฅผ ๋“ค๋ฉด:

๐Ÿ’ก ์žฌ๋Šฅ๋„ท์—์„œ ๋ฐฐ์šด ๊ฟ€ํŒ๋“ค:
โ€ข ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ์€ ๋จผ์ € ์ข…์ด์— ์Šค์ผ€์น˜ํ•˜๊ณ  ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์ •๋ฆฌํ•˜๊ธฐ
โ€ข ์ œ์•ฝ์กฐ๊ฑด ์šฐ์„ ์ˆœ์œ„๋Š” 100 ๋‹จ์œ„๋กœ ์„ค์ •ํ•ด์„œ ๋‚˜์ค‘์— ์กฐ์ •ํ•˜๊ธฐ ์‰ฝ๊ฒŒ
โ€ข ์ž์ฃผ ์“ฐ๋Š” ๋ ˆ์ด์•„์›ƒ ํŒจํ„ด์€ ์ปค์Šคํ…€ ๋ทฐ๋กœ ๋งŒ๋“ค์–ด์„œ ์žฌ์‚ฌ์šฉ
โ€ข Size Classes ์ „ํ™˜ ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜ duration์€ 0.3์ดˆ๊ฐ€ ๊ฐ€์žฅ ์ž์—ฐ์Šค๋Ÿฌ์›€
โ€ข iPad ์ง€์› ์‹œ Split View ์ƒํƒœ ๋ณ€ํ™”๋„ ๊ผญ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

๐Ÿš€ ๊ณ ๊ธ‰ ๊ธฐ๋ฒ•: Custom Layout๊ณผ ์ตœ์ ํ™”

์ž, ์ด์ œ ์ง„์งœ ๊ณ ๊ธ‰ ๊ธฐ๋ฒ•์œผ๋กœ ๋„˜์–ด๊ฐ€๋ณผ๊นŒ? ์ด๊ฑด ์ค‘๊ธ‰ ์ด์ƒ ๊ฐœ๋ฐœ์ž๋“ค์„ ์œ„ํ•œ ๋‚ด์šฉ์ด์•ผ! ๐ŸŽฏ

๐ŸŽจ NSLayoutAnchor vs NSLayoutConstraint

๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์œผ๋กœ ์ œ์•ฝ์กฐ๊ฑด์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š”๋ฐ, ๊ฐ๊ฐ ์žฅ๋‹จ์ ์ด ์žˆ์–ด:

NSLayoutAnchor (์ถ”์ฒœ)

์žฅ์ :
โ€ข ํƒ€์ž… ์•ˆ์ „์„ฑ (์ปดํŒŒ์ผ ํƒ€์ž„ ์—๋Ÿฌ ์ฒดํฌ)
โ€ข ์ฝ๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ
โ€ข ์‹ค์ˆ˜ ๋ฐฉ์ง€


๋‹จ์ :
โ€ข iOS 9 ์ด์ƒ๋งŒ ์ง€์›
โ€ข ์ผ๋ถ€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ œํ•œ

NSLayoutConstraint

์žฅ์ :
โ€ข ๋ชจ๋“  iOS ๋ฒ„์ „ ์ง€์›
โ€ข ๋” ์„ธ๋ฐ€ํ•œ ์ œ์–ด ๊ฐ€๋Šฅ
โ€ข ๋™์  ์ƒ์„ฑ์— ์œ ๋ฆฌ


๋‹จ์ :
โ€ข ํƒ€์ž… ์•ˆ์ „์„ฑ ์—†์Œ
โ€ข ์ฝ”๋“œ๊ฐ€ ์žฅํ™ฉํ•จ
โ€ข ์‹ค์ˆ˜ํ•˜๊ธฐ ์‰ฌ์›€

// NSLayoutAnchor ๋ฐฉ์‹ (์ถ”์ฒœ)
view1.leadingAnchor.constraint(equalTo: view2.leadingAnchor, constant: 20).isActive = true

// NSLayoutConstraint ๋ฐฉ์‹
NSLayoutConstraint(
    item: view1,
    attribute: .leading,
    relatedBy: .equal,
    toItem: view2,
    attribute: .leading,
    multiplier: 1.0,
    constant: 20
).isActive = true

// ์ž˜๋ชป๋œ ์กฐํ•ฉ๋„ ์ปดํŒŒ์ผ๋จ (๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ!)
NSLayoutConstraint(
    item: view1,
    attribute: .leading,  // X์ถ•
    relatedBy: .equal,
    toItem: view2,
    attribute: .top,      // Y์ถ• - ์ด๊ฑด ๋ง์ด ์•ˆ ๋˜์ง€!
    multiplier: 1.0,
    constant: 20
).isActive = true

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ๋ฒ•

1. Constraint Churn ์ค„์ด๊ธฐ

์ œ์•ฝ์กฐ๊ฑด์„ ์ž์ฃผ ์ถ”๊ฐ€/์ œ๊ฑฐํ•˜๋ฉด ๋ ˆ์ด์•„์›ƒ ์—”์ง„์— ๋ถ€๋‹ด์ด ๊ฐ€. ๋Œ€์‹  ์ด๋ ‡๊ฒŒ ํ•ด:

class OptimizedViewController: UIViewController {
    
    // ์ œ์•ฝ์กฐ๊ฑด์„ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘๊ณ  ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”๋งŒ ํ•จ
    private var portraitConstraints: [NSLayoutConstraint] = []
    private var landscapeConstraints: [NSLayoutConstraint] = []
    
    func setupConstraints() {
        let myView = UIView()
        myView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myView)
        
        // ์„ธ๋กœ ๋ชจ๋“œ ์ œ์•ฝ์กฐ๊ฑด
        portraitConstraints = [
            myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            myView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            myView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
            myView.heightAnchor.constraint(equalToConstant: 200)
        ]
        
        // ๊ฐ€๋กœ ๋ชจ๋“œ ์ œ์•ฝ์กฐ๊ฑด
        landscapeConstraints = [
            myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            myView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
            myView.widthAnchor.constraint(equalToConstant: 300),
            myView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.6)
        ]
        
        // ์ดˆ๊ธฐ ํ™œ์„ฑํ™”
        updateConstraints()
    }
    
    func updateConstraints() {
        if view.bounds.width > view.bounds.height {
            // ๊ฐ€๋กœ ๋ชจ๋“œ
            NSLayoutConstraint.deactivate(portraitConstraints)
            NSLayoutConstraint.activate(landscapeConstraints)
        } else {
            // ์„ธ๋กœ ๋ชจ๋“œ
            NSLayoutConstraint.deactivate(landscapeConstraints)
            NSLayoutConstraint.activate(portraitConstraints)
        }
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate { _ in
            self.updateConstraints()
            self.view.layoutIfNeeded()
        }
    }
}

2. Layout Pass ์ตœ์†Œํ™”

๋ถˆํ•„์š”ํ•œ ๋ ˆ์ด์•„์›ƒ ๊ณ„์‚ฐ์„ ์ค„์ด๋Š” ๋ฐฉ๋ฒ•:

class EfficientLayoutViewController: UIViewController {
    
    func batchUpdateConstraints() {
        // โŒ ๋‚˜์œ ์˜ˆ - ๋งค๋ฒˆ layoutIfNeeded() ํ˜ธ์ถœ
        constraint1.constant = 100
        view.layoutIfNeeded()
        
        constraint2.constant = 200
        view.layoutIfNeeded()
        
        constraint3.constant = 300
        view.layoutIfNeeded()
        
        // โœ… ์ข‹์€ ์˜ˆ - ํ•œ ๋ฒˆ์— ์—…๋ฐ์ดํŠธ
        constraint1.constant = 100
        constraint2.constant = 200
        constraint3.constant = 300
        view.layoutIfNeeded() // ํ•œ ๋ฒˆ๋งŒ!
    }
    
    func efficientAnimation() {
        // ์—ฌ๋Ÿฌ ์ œ์•ฝ์กฐ๊ฑด์„ ๋™์‹œ์— ๋ณ€๊ฒฝ
        UIView.animate(withDuration: 0.3) {
            self.constraint1.constant = 100
            self.constraint2.constant = 200
            self.constraint3.constant = 300
            // ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ธ”๋ก ์•ˆ์—์„œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ
            self.view.layoutIfNeeded()
        }
    }
}

3. Compression Resistance์™€ Hugging Priority ์ตœ์ ํ™”

๊ธฐ๋ณธ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ์“ฐ์ง€ ๋ง๊ณ , ์˜๋„์— ๋งž๊ฒŒ ์กฐ์ •ํ•ด:

func optimizePriorities() {
    let titleLabel = UILabel()
    let subtitleLabel = UILabel()
    let button = UIButton()
    
    // ํƒ€์ดํ‹€์€ ์ ˆ๋Œ€ ์ž˜๋ฆฌ๋ฉด ์•ˆ ๋จ
    titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
    titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
    
    // ์„œ๋ธŒํƒ€์ดํ‹€์€ ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•˜๋ฉด ์ค„์–ด๋“ค์–ด๋„ ๋จ
    subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    subtitleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
    
    // ๋ฒ„ํŠผ์€ ๊ณ ์ • ํฌ๊ธฐ ์œ ์ง€
    button.setContentCompressionResistancePriority(.required, for: .horizontal)
    button.setContentHuggingPriority(.required, for: .horizontal)
}

๐ŸŽฏ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์™„๋ฒฝํ•œ ํ”„๋กœํ•„ ํ™”๋ฉด

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋ชจ๋“  ๊ฑธ ์ข…ํ•ฉํ•ด์„œ, ์ง„์งœ ์‹ค๋ฌด์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ํ”„๋กœํ•„ ํ™”๋ฉด์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿš€

import UIKit

class ProfileViewController: UIViewController {
    
    // MARK: - UI Components
    
    private let scrollView: UIScrollView = {
        let scroll = UIScrollView()
        scroll.translatesAutoresizingMaskIntoConstraints = false
        scroll.showsVerticalScrollIndicator = false
        return scroll
    }()
    
    private let contentStackView: UIStackView = {
        let stack = UIStackView()
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.axis = .vertical
        stack.spacing = 24
        return stack
    }()
    
    private let headerView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    private let profileImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.backgroundColor = .systemGray5
        imageView.layer.borderWidth = 3
        imageView.layer.borderColor = UIColor.white.cgColor
        return imageView
    }()
    
    private let nameLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .systemFont(ofSize: 28, weight: .bold)
        label.textAlignment = .center
        return label
    }()
    
    private let bioLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.font = .systemFont(ofSize: 16)
        label.textColor = .secondaryLabel
        label.textAlignment = .center
        label.numberOfLines = 0
        return label
    }()
    
    private let statsStackView: UIStackView = {
        let stack = UIStackView()
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.axis = .horizontal
        stack.distribution = .fillEqually
        stack.spacing = 16
        return stack
    }()
    
    private let editButton: UIButton = {
        let button = UIButton(type: .system)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("ํ”„๋กœํ•„ ํŽธ์ง‘", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
        button.backgroundColor = .systemBlue
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 12
        return button
    }()
    
    // MARK: - Constraints
    
    private var profileImageSizeConstraint: NSLayoutConstraint!
    private var compactConstraints: [NSLayoutConstraint] = []
    private var regularConstraints: [NSLayoutConstraint] = []
    
    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupConstraints()
        loadData()
        updateLayoutForTraitCollection()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋ฅผ ์›ํ˜•์œผ๋กœ
        profileImageView.layer.cornerRadius = profileImageView.bounds.width / 2
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
            updateLayoutForTraitCollection()
        }
    }
    
    // MARK: - Setup
    
    private func setupUI() {
        view.backgroundColor = .systemBackground
        title = "ํ”„๋กœํ•„"
        
        view.addSubview(scrollView)
        scrollView.addSubview(contentStackView)
        
        // Header ๊ตฌ์„ฑ
        headerView.addSubview(profileImageView)
        headerView.addSubview(nameLabel)
        headerView.addSubview(bioLabel)
        
        // Stats ๊ตฌ์„ฑ
        let stats = [
            ("๊ฒŒ์‹œ๋ฌผ", "127"),
            ("ํŒ”๋กœ์›Œ", "1.2K"),
            ("ํŒ”๋กœ์ž‰", "456")
        ]
        
        stats.forEach { title, value in
            let statView = createStatView(title: title, value: value)
            statsStackView.addArrangedSubview(statView)
        }
        
        // Stack View์— ์ถ”๊ฐ€
        contentStackView.addArrangedSubview(headerView)
        contentStackView.addArrangedSubview(statsStackView)
        contentStackView.addArrangedSubview(editButton)
        
        // ์ถ”๊ฐ€ ์„น์…˜๋“ค
        addInfoSection()
        addActivitySection()
    }
    
    private func setupConstraints() {
        // ScrollView
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // ContentStackView
        NSLayoutConstraint.activate([
            contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
            contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 20),
            contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -20),
            contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -20),
            contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -40)
        ])
        
        // Profile Image
        profileImageSizeConstraint = profileImageView.widthAnchor.constraint(equalToConstant: 120)
        
        NSLayoutConstraint.activate([
            profileImageView.topAnchor.constraint(equalTo: headerView.topAnchor),
            profileImageView.centerXAnchor.constraint(equalTo: headerView.centerXAnchor),
            profileImageSizeConstraint,
            profileImageView.heightAnchor.constraint(equalTo: profileImageView.widthAnchor)
        ])
        
        // Name Label
        NSLayoutConstraint.activate([
            nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
            nameLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
            nameLabel.trailingAnchor.constraint(equalTo: headerView.trailingAnchor)
        ])
        
        // Bio Label
        NSLayoutConstraint.activate([
            bioLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
            bioLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
            bioLabel.trailingAnchor.constraint(equalTo: headerView.trailingAnchor),
            bioLabel.bottomAnchor.constraint(equalTo: headerView.bottomAnchor)
        ])
        
        // Edit Button
        NSLayoutConstraint.activate([
            editButton.heightAnchor.constraint(equalToConstant: 50)
        ])
        
        // Compact/Regular ์ œ์•ฝ์กฐ๊ฑด ์ค€๋น„
        setupAdaptiveConstraints()
    }
    
    private func setupAdaptiveConstraints() {
        // Compact: ์ž‘์€ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€, ์„ธ๋กœ ๋ฐฐ์น˜
        compactConstraints = [
            profileImageSizeConstraint
        ]
        
        // Regular: ํฐ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€, ๊ฐ€๋กœ ๋ฐฐ์น˜ ๊ฐ€๋Šฅ
        let regularImageSize = profileImageView.widthAnchor.constraint(equalToConstant: 150)
        regularConstraints = [
            regularImageSize
        ]
    }
    
    private func updateLayoutForTraitCollection() {
        if traitCollection.horizontalSizeClass == .regular {
            // iPad
            NSLayoutConstraint.deactivate(compactConstraints)
            NSLayoutConstraint.activate(regularConstraints)
            
            nameLabel.font = .systemFont(ofSize: 32, weight: .bold)
            bioLabel.font = .systemFont(ofSize: 18)
            contentStackView.spacing = 32
        } else {
            // iPhone
            NSLayoutConstraint.deactivate(regularConstraints)
            NSLayoutConstraint.activate(compactConstraints)
            
            nameLabel.font = .systemFont(ofSize: 28, weight: .bold)
            bioLabel.font = .systemFont(ofSize: 16)
            contentStackView.spacing = 24
        }
        
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
    
    // MARK: - Helper Methods
    
    private func createStatView(title: String, value: String) -> UIView {
        let container = UIView()
        container.translatesAutoresizingMaskIntoConstraints = false
        
        let valueLabel = UILabel()
        valueLabel.translatesAutoresizingMaskIntoConstraints = false
        valueLabel.text = value
        valueLabel.font = .systemFont(ofSize: 20, weight: .bold)
        valueLabel.textAlignment = .center
        
        let titleLabel = UILabel()
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.text = title
        titleLabel.font = .systemFont(ofSize: 14)
        titleLabel.textColor = .secondaryLabel
        titleLabel.textAlignment = .center
        
        container.addSubview(valueLabel)
        container.addSubview(titleLabel)
        
        NSLayoutConstraint.activate([
            valueLabel.topAnchor.constraint(equalTo: container.topAnchor),
            valueLabel.centerXAnchor.constraint(equalTo: container.centerXAnchor),
            
            titleLabel.topAnchor.constraint(equalTo: valueLabel.bottomAnchor, constant: 4),
            titleLabel.centerXAnchor.constraint(equalTo: container.centerXAnchor),
            titleLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor)
        ])
        
        return container
    }
    
    private func addInfoSection() {
        let sectionTitle = createSectionTitle("์ •๋ณด")
        contentStackView.addArrangedSubview(sectionTitle)
        
        let infoItems = [
            ("๐Ÿ“ง", "์ด๋ฉ”์ผ", "user@example.com"),
            ("๐Ÿ“ฑ", "์ „ํ™”", "+82 10-1234-5678"),
            ("๐Ÿ“", "์œ„์น˜", "์„œ์šธ, ๋Œ€ํ•œ๋ฏผ๊ตญ"),
            ("๐ŸŽ‚", "์ƒ์ผ", "1990๋…„ 1์›” 1์ผ")
        ]
        
        infoItems.forEach { emoji, title, value in
            let infoRow = createInfoRow(emoji: emoji, title: title, value: value)
            contentStackView.addArrangedSubview(infoRow)
        }
    }
    
    private func addActivitySection() {
        let sectionTitle = createSectionTitle("์ตœ๊ทผ ํ™œ๋™")
        contentStackView.addArrangedSubview(sectionTitle)
        
        let activities = [
            "์ƒˆ๋กœ์šด ๊ฒŒ์‹œ๋ฌผ ์ž‘์„ฑ",
            "๋Œ“๊ธ€ 5๊ฐœ ์ž‘์„ฑ",
            "ํ”„๋กœํ•„ ์—…๋ฐ์ดํŠธ"
        ]
        
        activities.forEach { activity in
            let activityRow = createActivityRow(text: activity)
            contentStackView.addArrangedSubview(activityRow)
        }
    }
    
    private func createSectionTitle(_ text: String) -> UILabel {
        let label = UILabel()
        label.text = text
        label.font = .systemFont(ofSize: 20, weight: .bold)
        return label
    }
    
    private func createInfoRow(emoji: String, title: String, value: String) -> UIView {
        let container = UIView()
        container.translatesAutoresizingMaskIntoConstraints = false
        container.backgroundColor = .systemGray6
        container.layer.cornerRadius = 12
        
        let emojiLabel = UILabel()
        emojiLabel.translatesAutoresizingMaskIntoConstraints = false
        emojiLabel.text = emoji
        emojiLabel.font = .systemFont(ofSize: 24)
        
        let titleLabel = UILabel()
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.text = title
        titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
        titleLabel.textColor = .secondaryLabel
        
        let valueLabel = UILabel()
        valueLabel.translatesAutoresizingMaskIntoConstraints = false
        valueLabel.text = value
        valueLabel.font = .systemFont(ofSize: 16)
        
        let textStack = UIStackView(arrangedSubviews: [titleLabel, valueLabel])
        textStack.translatesAutoresizingMaskIntoConstraints = false
        textStack.axis = .vertical
        textStack.spacing = 4
        
        container.addSubview(emojiLabel)
        container.addSubview(textStack)
        
        NSLayoutConstraint.activate([
            container.heightAnchor.constraint(greaterThanOrEqualToConstant: 70),
            
            emojiLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
            emojiLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
            
            textStack.leadingAnchor.constraint(equalTo: emojiLabel.trailingAnchor, constant: 16),
            textStack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
            textStack.centerYAnchor.constraint(equalTo: container.centerYAnchor)
        ])
        
        return container
    }
    
    private func createActivityRow(text: String) -> UIView {
        let container = UIView()
        container.translatesAutoresizingMaskIntoConstraints = false
        container.backgroundColor = .systemGray6
        container.layer.cornerRadius = 12
        
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = text
        label.font = .systemFont(ofSize: 16)
        
        let timeLabel = UILabel()
        timeLabel.translatesAutoresizingMaskIntoConstraints = false
        timeLabel.text = "2์‹œ๊ฐ„ ์ „"
        timeLabel.font = .systemFont(ofSize: 14)
        timeLabel.textColor = .secondaryLabel
        
        container.addSubview(label)
        container.addSubview(timeLabel)
        
        NSLayoutConstraint.activate([
            container.heightAnchor.constraint(equalToConstant: 60),
            
            label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
            label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
            
            timeLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
            timeLabel.centerYAnchor.constraint(equalTo: container.centerYAnchor),
            timeLabel.leadingAnchor.constraint(greaterThanOrEqualTo: label.trailingAnchor, constant: 8)
        ])
        
        label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        timeLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
        
        return container
    }
    
    private func loadData() {
        // ์‹ค์ œ๋กœ๋Š” ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ด
        nameLabel.text = "ํ™๊ธธ๋™"
        bioLabel.text = "iOS ๊ฐœ๋ฐœ์ž | Swift ์ „๋ฌธ๊ฐ€\n์žฌ๋Šฅ๋„ท์—์„œ ๊ฐœ๋ฐœ ๋…ธํ•˜์šฐ๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค โœจ"
        profileImageView.image = UIImage(systemName: "person.circle.fill")
        profileImageView.tintColor = .systemBlue
    }
}

โœ… ์™„์„ฑ! ์ด ํ”„๋กœํ•„ ํ™”๋ฉด์€:

โ€ข ๋ชจ๋“  ๋””๋ฐ”์ด์Šค ํฌ๊ธฐ์— ์™„๋ฒฝ ๋Œ€์‘
โ€ข Size Classes ๊ธฐ๋ฐ˜ ์ ์‘ํ˜• ๋ ˆ์ด์•„์›ƒ
โ€ข ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ๊ธด ์ฝ˜ํ…์ธ 
โ€ข ๋™์  ๋†’์ด ์กฐ์ ˆ
โ€ข ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ
โ€ข ๊น”๋”ํ•œ ์ฝ”๋“œ ๊ตฌ์กฐ

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€... ์—ฌ๊ธฐ๊นŒ์ง€ ์™”๋‹ค๋‹ˆ ๋Œ€๋‹จํ•œ๋ฐ? ๐Ÿ‘ ์ง„์งœ ๊ธด ์—ฌ์ •์ด์—ˆ์ง€๋งŒ, ์ด์ œ ๋„ˆ๋Š” Auto Layout๊ณผ Size Classes์˜ ์ง„์ •ํ•œ ๋งˆ์Šคํ„ฐ์•ผ!

๐ŸŽฏ ํ•ต์‹ฌ ์š”์•ฝ


1. Auto Layout์˜ ๋ณธ์งˆ
โ€ข ์ ˆ๋Œ€ ์ขŒํ‘œ๊ฐ€ ์•„๋‹Œ ๊ด€๊ณ„๋กœ ๋ ˆ์ด์•„์›ƒ ์ •์˜
โ€ข ์ œ์•ฝ์กฐ๊ฑด(Constraints)์ด ๋ชจ๋“  ๊ฒƒ์˜ ๊ธฐ๋ฐ˜
โ€ข ์šฐ์„ ์ˆœ์œ„๋กœ ์œ ์—ฐํ•œ ๋ ˆ์ด์•„์›ƒ ๊ตฌํ˜„


2. Size Classes์˜ ํž˜
โ€ข Compact์™€ Regular๋กœ ํ™”๋ฉด ํฌ๊ธฐ ์ถ”์ƒํ™”
โ€ข ํ•˜๋‚˜์˜ ์ฝ”๋“œ๋กœ ๋ชจ๋“  ๋””๋ฐ”์ด์Šค ๋Œ€์‘
โ€ข traitCollectionDidChange๋กœ ๋™์  ๋Œ€์‘


3. Stack View์˜ ๋งˆ๋ฒ•
โ€ข ์ œ์•ฝ์กฐ๊ฑด ์ž๋™ ๊ด€๋ฆฌ
โ€ข ๋™์  ์ฝ˜ํ…์ธ ์— ์ตœ์ 
โ€ข ์ค‘์ฒฉ์œผ๋กœ ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ ๊ตฌํ˜„


4. ์‹ค์ „ ๋…ธํ•˜์šฐ
โ€ข Safe Area ํ•ญ์ƒ ๊ณ ๋ ค
โ€ข ์ œ์•ฝ์กฐ๊ฑด ์žฌ์‚ฌ์šฉ์œผ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
โ€ข ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜
โ€ข ๋””๋ฒ„๊น… ๋„๊ตฌ ์ ๊ทน ํ™œ์šฉ

์žˆ์ž–์•„, ์ฒ˜์Œ์—๋Š” Auto Layout์ด ์–ด๋ ต๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์–ด. ๋‚˜๋„ ๊ทธ๋žฌ๊ฑฐ๋“ . ํ•˜์ง€๋งŒ ๊ณ„์† ์—ฐ์Šตํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์†์ด ์›€์ง์ด๊ฒŒ ๋  ๊ฑฐ์•ผ. ๋งˆ์น˜ ์ž์ „๊ฑฐ ํƒ€๋Š” ๊ฒƒ์ฒ˜๋Ÿผ! ๐Ÿšดโ€โ™‚๏ธ

๊ทธ๋ฆฌ๊ณ  ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์ „๋ฌธ๊ฐ€๋“ค์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ํ˜ผ์ž ์‚ฝ์งˆํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฅด๊ฒŒ ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ !

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ์กฐ์–ธ:
โ€ข ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ๋ณต์žก๋„๋ฅผ ๋†’์—ฌ๊ฐ€
โ€ข ์‹ค์ œ ๋””๋ฐ”์ด์Šค์—์„œ ๊ผญ ํ…Œ์ŠคํŠธํ•ด๋ด (์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•ด)
โ€ข ๋‹ค์–‘ํ•œ Size Classes ์กฐํ•ฉ์„ ๋ชจ๋‘ ํ™•์ธํ•ด
โ€ข ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ›์•„์„œ ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ
โ€ข ์• ํ”Œ์˜ Human Interface Guidelines๋ฅผ ์ฐธ๊ณ ํ•ด

์ž, ์ด์ œ ๋„ˆ๋งŒ์˜ ๋ฉ‹์ง„ ๋ฐ˜์‘ํ˜• UI๋ฅผ ๋งŒ๋“ค์–ด๋ณผ ์‹œ๊ฐ„์ด์•ผ! ํ™”์ดํŒ…! ๐Ÿš€โœจ

๋ฐ˜์‘ํ˜• UI ๊ฐœ๋ฐœ ์—ฌ์ • ๐ŸŒฑ ์‹œ์ž‘ ๐Ÿ“š ํ•™์Šต ๐Ÿ’ป ์‹ค์Šต ๐Ÿ”ง ์ตœ์ ํ™” ๐Ÿ† ๋งˆ์Šคํ„ฐ! ๋‹น์‹ ์€ ์ด๋ฏธ ์—ฌ๊ธฐ๊นŒ์ง€ ์™”์Šต๋‹ˆ๋‹ค! ์ด์ œ ์‹ค์ „์—์„œ ๋ฉ‹์ง„ ์•ฑ์„ ๋งŒ๋“ค์–ด๋ณด์„ธ์š” โœจ

ํ–‰์šด์„ ๋นŒ์–ด! ๊ทธ๋ฆฌ๊ณ  ๋ฉ‹์ง„ ์•ฑ ๋งŒ๋“ค๋ฉด ๊ผญ ์ž๋ž‘ํ•ด์ค˜! ๐Ÿ˜Š๐Ÿ‘

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.