Ver3.0 ๐ฑ 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ํฌ์ธํธ ๋จ์ด์ ธ ์์ด"๋ผ๊ณ ๊ด๊ณ๋ก ์ ์ํ๋ ๊ฑฐ์ง.
๐ง ์ ์ฝ์กฐ๊ฑด(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์ด ๋จ์ ๊ณต๊ฐ์ ๋ค ์ฐจ์งํจ
์ด๋ ๊ฒ ํ๋ฉด "์ด๋ฆ"์ด๋ผ๋ ์งง์ ํ ์คํธ๋ ํ์ํ ๋งํผ๋ง ๊ณต๊ฐ์ ์ฐจ์งํ๊ณ , ๊ธด ์ด๋ฆ ๊ฐ์ด ๋๋จธ์ง ๊ณต๊ฐ์ ์ ์ฐํ๊ฒ ์ฌ์ฉํ ์ ์์ด. ์์ ์ค๋งํธํ์ง? ๐ง
๐ฑ 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()
}
}
}
๐ ๏ธ ์ค์ ํ๋ก์ ํธ: ๋ฐ์ํ ๋ก๊ทธ์ธ ํ๋ฉด ๋ง๋ค๊ธฐ
์, ์ด์ ๋ฐฐ์ด ๊ฑธ ๋ค ํฉ์ณ์ ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ๋ชจ๋ ๋๋ฐ์ด์ค์์ ์๋ฒฝํ๊ฒ ๋ณด์ด๋ ๋ก๊ทธ์ธ ํ๋ฉด์ ๋ง๋ค ๊ฑฐ์ผ. ๐ฏ
๐ ์๊ตฌ์ฌํญ
โข 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์ ๋ค๋ฃจ๋ ๋ฐฉ๋ฒ์ ์๋ ค์ค๊ฒ! ์ด๊ฑด ๋ด๊ฐ ์๋ ๊ฐ ์ฝ์งํ๋ฉด์ ๋ฐฐ์ด ๋ ธํ์ฐ์ผ. ๐
๐ ํฉ๊ธ ๊ท์น 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๋ฅผ ๋ง๋ค์ด๋ณผ ์๊ฐ์ด์ผ! ํ์ดํ ! ๐โจ
ํ์ด์ ๋น์ด! ๊ทธ๋ฆฌ๊ณ ๋ฉ์ง ์ฑ ๋ง๋ค๋ฉด ๊ผญ ์๋ํด์ค! ๐๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.