์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ“ Auto Layout๊ณผ Size Classes๋ฅผ ํ™œ์šฉํ•œ๋ฐ˜์‘ํ˜• UI ์„ค๊ณ„ ์ „๋žต
Swift ยท iOS ๊ฐœ๋ฐœ

๐Ÿ“ Auto Layout๊ณผ Size Classes๋ฅผ ํ™œ์šฉํ•œ
๋ฐ˜์‘ํ˜• UI ์„ค๊ณ„ ์ „๋žต

iPhone๋ถ€ํ„ฐ iPad๊นŒ์ง€, ๋ชจ๋“  ํ™”๋ฉด์„ ์ •๋ณตํ•˜๋Š” ๋ ˆ์ด์•„์›ƒ ๋งˆ์Šคํ„ฐ ๊ฐ€์ด๋“œ ๐Ÿš€

๐Ÿ—ฃ๏ธ ์ด๋Ÿฐ ๊ฒฝํ—˜ ์žˆ์ง€ ์•Š์•„์š”?
์•ผ ๋‚˜ iPhone 15์—์„œ๋Š” UI ์™„๋ฒฝํ•œ๋ฐ
iPad์—์„œ ์—ด์–ด๋ณด๋‹ˆ๊นŒ ๋ ˆ์ด์•„์›ƒ์ด ์™„์ „ ๋ฐ•์‚ด๋‚ฌ์–ด ๐Ÿ˜ญ
ใ…‹ใ…‹ใ…‹ใ…‹ ๊ทธ๊ฑฐ Auto Layout ์ œ๋Œ€๋กœ ์•ˆ ์“ด ๊ฑฐ์ž–์•„
Auto Layout์ด ๋ญ”์ง€๋Š” ์•„๋Š”๋ฐ... Size Classes๋Š” ๋˜ ๋ญ์•ผ?? ๐Ÿค”
๊ทธ๊ฑฐ ๋ชจ๋ฅด๋ฉด ๋ฐ˜์‘ํ˜• UI ๋งŒ๋“ค๊ธฐ ์ง„์งœ ํž˜๋“ค์–ด
์ง€๊ธˆ ๋ฐ”๋กœ ์•Œ๋ ค์ค„๊ฒŒ ใ„ฑใ„ฑ

iOS ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์ด๋Ÿฐ ์ƒํ™ฉ ์ง„์งœ ๋งŽ์ด ๊ฒช์ฃ  ใ…‹ใ…‹. ๋””์ž์ธ ์—ด์‹ฌํžˆ ํ–ˆ๋Š”๋ฐ ๊ธฐ๊ธฐ ๋ฐ”๊พธ๋ฉด UI๊ฐ€ ์™„์ „ ์—‰๋ง์ด ๋˜๋Š” ๊ทธ ์ฐธ๋‹ดํ•จ... ๐Ÿ˜…

์˜ค๋Š˜์€ Auto Layout๊ณผ Size Classes๋ฅผ ์ œ๋Œ€๋กœ ํŒŒํ—ค์ณ์„œ, ์–ด๋–ค ๊ธฐ๊ธฐ์—์„œ๋„ ์™„๋ฒฝํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋Š” ๋ฐ˜์‘ํ˜• UI๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”!

Auto Layout ์ œ์•ฝ(Constraint) ์‹œ์Šคํ…œ ํ—ค๋” ๋ฒ„ํŠผ ์ฝ˜ํ…์ธ  ์˜์—ญ 100% ๋„ˆ๋น„ ์นด๋“œ1 ์นด๋“œ2 iPhone SE 375 ร— 667pt 16 16 ํ—ค๋” ๋ฒ„ํŠผ (๋™์ผํ•œ ์ œ์•ฝ, ๋‹ค๋ฅธ ํฌ๊ธฐ) ์‚ฌ์ด๋“œ๋ฐ” Size Class Regular ๋ฉ”์ธ ์ฝ˜ํ…์ธ  ํ™•์žฅ๋œ ์˜์—ญ iPad Pro 11" 834 ร— 1194pt ํ—ค๋” ๋ฒ„ํŠผ ์ฝ˜ํ…์ธ  ์˜์—ญ 100% ๋„ˆ๋น„ ์นด๋“œ1 ์นด๋“œ2 iPhone 15 Pro Max 430 ร— 932pt ๋™์ผํ•œ ์ œ์•ฝ ์ž๋™ ์ ์‘ ๐Ÿ’ก Auto Layout: ํ•˜๋‚˜์˜ ์ œ์•ฝ ๊ทœ์น™์œผ๋กœ ๋ชจ๋“  ํ™”๋ฉด ํฌ๊ธฐ์— ์ž๋™ ์ ์‘ โ€” ๊ธฐ๊ธฐ๊ฐ€ ๋‹ฌ๋ผ๋„ ๋ ˆ์ด์•„์›ƒ ๋กœ์ง์€ ๋™์ผ!
๐Ÿ“Œ Auto Layout์ด ๋ญ”์ง€ ์ œ๋Œ€๋กœ ์•Œ์•„๋ณด์ž

Auto Layout์€ iOS 6๋ถ€ํ„ฐ ๋„์ž…๋œ ์ œ์•ฝ ๊ธฐ๋ฐ˜(Constraint-based) ๋ ˆ์ด์•„์›ƒ ์‹œ์Šคํ…œ์ด์—์š”. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, "์ด ๋ทฐ๋Š” ๋ถ€๋ชจ ๋ทฐ์˜ ์™ผ์ชฝ์—์„œ 16pt ๋–จ์–ด์ ธ ์žˆ์–ด์•ผ ํ•ด" ๊ฐ™์€ ๊ทœ์น™(์ œ์•ฝ)์„ ์ •์˜ํ•˜๋ฉด, ์‹œ์Šคํ…œ์ด ์•Œ์•„์„œ ๋ชจ๋“  ํ™”๋ฉด ํฌ๊ธฐ์— ๋งž๊ฒŒ ๊ณ„์‚ฐํ•ด์ฃผ๋Š” ๊ฑฐ์ฃ  ใ…‹ใ…‹

๐Ÿ’ก ํ•ต์‹ฌ ๊ฐœ๋… Auto Layout์€ ์ˆ˜ํ•™์  ๋ฐฉ์ •์‹ ์‹œ์Šคํ…œ์ด์—์š”. ๊ฐ ์ œ์•ฝ์€ ํ•˜๋‚˜์˜ ์„ ํ˜• ๋ฐฉ์ •์‹์œผ๋กœ ํ‘œํ˜„๋˜๊ณ , ์‹œ์Šคํ…œ์€ ์ด ๋ฐฉ์ •์‹๋“ค์„ ํ’€์–ด์„œ ๊ฐ ๋ทฐ์˜ ์ •ํ™•ํ•œ ์œ„์น˜์™€ ํฌ๊ธฐ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
๐Ÿ”‘ ์ œ์•ฝ(Constraint)์˜ 4๊ฐ€์ง€ ํ•ต์‹ฌ ์†์„ฑ
๐Ÿ“ ์œ„์น˜ ์ œ์•ฝ leading trailing
top bottom
centerX centerY
๐Ÿ“ ํฌ๊ธฐ ์ œ์•ฝ width height
aspectRatio
equalWidth equalHeight
โš–๏ธ ์ œ์•ฝ์˜ ์šฐ์„ ์ˆœ์œ„(Priority)

์ œ์•ฝ์—๋Š” 1~1000 ์‚ฌ์ด์˜ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์žˆ์–ด์š”. ์ถฉ๋Œ์ด ์ƒ๊ธฐ๋ฉด ๋†’์€ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์ด๊ฒจ์š”!

์šฐ์„ ์ˆœ์œ„ ์ด๋ฆ„ ์šฉ๋„
1000 Required ์ ˆ๋Œ€ ์–ด๊ฒจ์„  ์•ˆ ๋˜๋Š” ํ•„์ˆ˜ ์ œ์•ฝ
750 High ๊ธฐ๋ณธ Content Hugging Priority
500 Medium ์ผ๋ฐ˜์ ์ธ ์„ ํƒ์  ์ œ์•ฝ
250 Low ๊ธฐ๋ณธ Content Compression Resistance
โš ๏ธ ์ฃผ์˜! Required(1000) ์ œ์•ฝ๋ผ๋ฆฌ ์ถฉ๋Œํ•˜๋ฉด ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด์š”. Xcode ์ฝ˜์†”์— ๋นจ๊ฐ„ ๊ฒฝ๊ณ ๊ฐ€ ๋œจ๋Š” ๊ทธ ์ƒํ™ฉ ใ…‹ใ…‹. ์ถฉ๋Œ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ์ œ์•ฝ์€ 999 ์ดํ•˜๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด์š”!
๐Ÿ’ป ์ฝ”๋“œ๋กœ Auto Layout ์ž‘์„ฑํ•˜๊ธฐ

Interface Builder(์Šคํ† ๋ฆฌ๋ณด๋“œ)๋กœ๋„ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์š”์ฆ˜ ํŠธ๋ Œ๋“œ๋Š” ์ฝ”๋“œ ๊ธฐ๋ฐ˜ ๋ ˆ์ด์•„์›ƒ์ด์ฃ ! ํ˜‘์—…ํ•  ๋•Œ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ถฉ๋Œ ๋‚˜๋Š” ๊ฑฐ ์ง„์งœ ์ŠคํŠธ๋ ˆ์Šค์ž–์•„์š” ใ…‹ใ…‹ใ…‹

๐Ÿ”ง ๋ฐฉ๋ฒ• 1: NSLayoutConstraint (๋„ค์ดํ‹ฐ๋ธŒ)
// ๊ธฐ๋ณธ NSLayoutConstraint ์‚ฌ์šฉ๋ฒ•
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false  // โญ ํ•„์ˆ˜!
view.addSubview(button)

NSLayoutConstraint.activate([
    button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
    button.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
    button.heightAnchor.constraint(equalToConstant: 50)
])
๐Ÿ’ก translatesAutoresizingMaskIntoConstraints = false ์ฝ”๋“œ๋กœ Auto Layout ์“ธ ๋•Œ ์ด๊ฑฐ ๋นผ๋จน์œผ๋ฉด ์ œ์•ฝ์ด ์ถฉ๋Œํ•ด์š”! ์ฝ”๋“œ๋กœ ๋ทฐ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ๋Š” ๋ฐ˜๋“œ์‹œ false๋กœ ์„ค์ •ํ•ด์•ผ ํ•ด์š”. ์ด๊ฑฐ ๋•Œ๋ฌธ์— ์‚ฝ์งˆํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ „ ์„ธ๊ณ„์— ์ˆ˜๋ฐฑ๋งŒ ๋ช…์€ ๋  ๊ฑฐ์˜ˆ์š” ใ…‹ใ…‹
๐Ÿš€ ๋ฐฉ๋ฒ• 2: NSLayoutAnchor (๋” ์ฝ๊ธฐ ์‰ฌ์šด ๋ฐฉ์‹)
// Anchor ๋ฐฉ์‹ - ํƒ€์ž… ์•ˆ์ „ํ•˜๊ณ  ๊ฐ€๋…์„ฑ ์ข‹์Œ
let titleLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(titleLabel)

NSLayoutConstraint.activate([
    // ๋น„์œจ ๊ธฐ๋ฐ˜ ๋„ˆ๋น„ ์„ค์ •
    titleLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
    titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
    
    // ์ตœ์†Œ/์ตœ๋Œ€ ๋†’์ด ์„ค์ •
    titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
])
โœจ ๋ฐฉ๋ฒ• 3: Swift ํ™•์žฅ(Extension)์œผ๋กœ ํŽธํ•˜๊ฒŒ
// UIView Extension์œผ๋กœ ํŽธ์˜ ๋ฉ”์„œ๋“œ ๋งŒ๋“ค๊ธฐ
extension UIView {
    func pinToEdges(of view: UIView, padding: UIEdgeInsets = .zero) {
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            topAnchor.constraint(equalTo: view.topAnchor, constant: padding.top),
            leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding.left),
            trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding.right),
            bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -padding.bottom)
        ])
    }
    
    func center(in view: UIView) {
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            centerXAnchor.constraint(equalTo: view.centerXAnchor),
            centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
contentView.pinToEdges(of: containerView, padding: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))
โœ… ๊ฟ€ํŒ! ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ iOS ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•  ๋•Œ ๋ณด๋ฉด, ์ฝ”๋“œ ๊ธฐ๋ฐ˜ Auto Layout ๋Šฅ์ˆ™๋„๊ฐ€ ์‹œ๋‹ˆ์–ด ๊ฐœ๋ฐœ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ธฐ์ค€ ์ค‘ ํ•˜๋‚˜์˜ˆ์š”. Extension ํŒจํ„ด ์ž˜ ํ™œ์šฉํ•˜๋ฉด ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์ด ํ›จ์”ฌ ์˜ฌ๋ผ๊ฐ€์š”!
๐Ÿคผ Content Hugging vs Compression Resistance

์ด ๋‘ ๊ฐœ๋…์€ Auto Layout์—์„œ ์ง„์งœ ํ—ท๊ฐˆ๋ฆฌ๋Š” ๋ถ€๋ถ„์ธ๋ฐ, ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๋ฉด ๋ ˆ์ด์•„์›ƒ ๋ฌธ์ œ์˜ ์ ˆ๋ฐ˜์€ ํ•ด๊ฒฐ๋ผ์š” ใ…‹ใ…‹

๐Ÿค— Content Hugging Priority "๋‚˜ ์ปค์ง€๊ธฐ ์‹ซ์–ด!"

๊ฐ’์ด ๋†’์„์ˆ˜๋ก ์ž์‹ ์˜ ๊ณ ์œ  ํฌ๊ธฐ(intrinsic size)๋ฅผ ์œ ์ง€ํ•˜๋ ค ํ•ด์š”.

๊ธฐ๋ณธ๊ฐ’: 250 (์ˆ˜ํ‰)
๊ธฐ๋ณธ๊ฐ’: 250 (์ˆ˜์ง)
๐Ÿ’ช Compression Resistance Priority "๋‚˜ ์ž‘์•„์ง€๊ธฐ ์‹ซ์–ด!"

๊ฐ’์ด ๋†’์„์ˆ˜๋ก ์ž˜๋ฆฌ๊ฑฐ๋‚˜ ์••์ถ•๋˜๋Š” ๊ฑธ ๊ฑฐ๋ถ€ํ•ด์š”.

๊ธฐ๋ณธ๊ฐ’: 750 (์ˆ˜ํ‰)
๊ธฐ๋ณธ๊ฐ’: 750 (์ˆ˜์ง)
// ์‹ค์ „ ์˜ˆ์‹œ: ๋‘ ๋ ˆ์ด๋ธ”์ด ๋‚˜๋ž€ํžˆ ์žˆ์„ ๋•Œ
// ์™ผ์ชฝ ๋ ˆ์ด๋ธ”์€ ๊ณ ์ •, ์˜ค๋ฅธ์ชฝ ๋ ˆ์ด๋ธ”์€ ๋Š˜์–ด๋‚˜์•ผ ํ•  ๋•Œ

let leftLabel = UILabel()
let rightLabel = UILabel()

// ์™ผ์ชฝ ๋ ˆ์ด๋ธ”: ์ž๊ธฐ ํฌ๊ธฐ ์œ ์ง€ (Hugging ๋†’๊ฒŒ)
leftLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
leftLabel.setContentCompressionResistancePriority(.required, for: .horizontal)

// ์˜ค๋ฅธ์ชฝ ๋ ˆ์ด๋ธ”: ๋Š˜์–ด๋‚˜๋„ ๋จ (Hugging ๋‚ฎ๊ฒŒ)
rightLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)

// ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด leftLabel์€ ํ…์ŠคํŠธ ํฌ๊ธฐ๋งŒํผ๋งŒ,
// rightLabel์€ ๋‚˜๋จธ์ง€ ๊ณต๊ฐ„์„ ์ฑ„์›Œ์š”!
Size Classes ๋งคํ•‘ ์ฐจํŠธ ๊ธฐ๊ธฐ๋ณ„ ์ˆ˜ํ‰(Horizontal) ร— ์ˆ˜์ง(Vertical) Size Class ์กฐํ•ฉ Compact (C) Regular (R) ๊ธฐ๊ธฐ / ๋ฐฉํ–ฅ ์ˆ˜ํ‰(H) Horizontal ์ˆ˜์ง(V) Vertical ๋ ˆ์ด์•„์›ƒ ํŠน์„ฑ ๐Ÿ“ฑ iPhone ์„ธ๋กœ ๋ฐฉํ–ฅ Compact Regular ๋‹จ์ผ ์ปฌ๋Ÿผ, ์ „์ฒด ๋„ˆ๋น„ ์ฝ˜ํ…์ธ  Navigation, TabBar ํ‘œ์ค€ ํŒจํ„ด ๐Ÿ“ฑ iPhone ๊ฐ€๋กœ ๋ฐฉํ–ฅ Compact Compact ์ขŒ์šฐ ๋ถ„ํ•  ๊ฐ€๋Šฅ, ๋ฏธ๋””์–ด ์ „์ฒดํ™”๋ฉด iPhone Plus/Max๋Š” H: Regular ๐Ÿ“ฒ iPad ์„ธ๋กœ ๋ฐฉํ–ฅ Regular Regular ์‚ฌ์ด๋“œ๋ฐ” + ๋ฉ”์ธ ์ฝ˜ํ…์ธ  ๋ถ„ํ•  UISplitViewController ํ™œ์šฉ ์ตœ์  ๐Ÿ“ฒ iPad ๊ฐ€๋กœ ๋ฐฉํ–ฅ Regular Regular ๋ฉ€ํ‹ฐํƒœ์Šคํ‚น, 3์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ ๊ฐ€๋Šฅ Stage Manager ์ง€์› ํ™˜๊ฒฝ ๐Ÿ“ฑ iPhone Plus/Max ๊ฐ€๋กœ ๋ฐฉํ–ฅ Regular Compact iPad์ฒ˜๋Ÿผ ์‚ฌ์ด๋“œ๋ฐ” ํ‘œ์‹œ ๊ฐ€๋Šฅ! H: Regular ๋•๋ถ„์— ๋ถ„ํ•  ๋ ˆ์ด์•„์›ƒ ๐ŸŽฏ Size Class ํ™œ์šฉ ํ•ต์‹ฌ ์›์น™ Compact ร— Compact = ๊ณต๊ฐ„ ์ตœ์†Œํ™” / Regular ร— Regular = ํ’๋ถ€ํ•œ ๋ ˆ์ด์•„์›ƒ / ํ˜ผํ•ฉ = ์ƒํ™ฉ์— ๋งž๋Š” ์ ์‘ํ˜• UI
๐Ÿ“ Size Classes: ๋ฐ˜์‘ํ˜• UI์˜ ํ•ต์‹ฌ ์—ด์‡ 

Size Classes๋Š” iOS 8๋ถ€ํ„ฐ ๋„์ž…๋œ ๊ฐœ๋…์œผ๋กœ, ํ™”๋ฉด์˜ ํฌ๊ธฐ๋ฅผ Compact์™€ Regular ๋‘ ๊ฐ€์ง€๋กœ ๋ถ„๋ฅ˜ํ•ด์š”. ์ •ํ™•ํ•œ ํ”ฝ์…€ ํฌ๊ธฐ ๋Œ€์‹  ์ƒ๋Œ€์ ์ธ ํฌ๊ธฐ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์ฃ !

๐ŸŽฏ ์ฝ”๋“œ์—์„œ Size Class ๊ฐ์ง€ํ•˜๊ธฐ
// UITraitCollection์œผ๋กœ ํ˜„์žฌ Size Class ํ™•์ธ
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    
    // ์ˆ˜ํ‰ Size Class ๋ณ€๊ฒฝ ๊ฐ์ง€
    if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
        updateLayoutForSizeClass()
    }
}

func updateLayoutForSizeClass() {
    switch traitCollection.horizontalSizeClass {
    case .compact:
        // iPhone ์„ธ๋กœ ๋ชจ๋“œ ๋ ˆ์ด์•„์›ƒ
        setupCompactLayout()
    case .regular:
        // iPad ๋˜๋Š” iPhone Plus ๊ฐ€๋กœ ๋ ˆ์ด์•„์›ƒ
        setupRegularLayout()
    case .unspecified:
        break
    @unknown default:
        break
    }
}

// ์‹ค์ „ ์˜ˆ์‹œ: ์ปฌ๋Ÿผ ์ˆ˜ ๋™์  ๋ณ€๊ฒฝ
func setupCompactLayout() {
    // 1์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ
    collectionViewFlowLayout.minimumInteritemSpacing = 0
    collectionViewFlowLayout.itemSize = CGSize(
        width: collectionView.bounds.width - 32,
        height: 120
    )
}

func setupRegularLayout() {
    // 2์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ
    let itemWidth = (collectionView.bounds.width - 48) / 2
    collectionViewFlowLayout.itemSize = CGSize(width: itemWidth, height: 150)
}
๐Ÿ”„ UITraitCollection ํ™œ์šฉ ์‹ฌํ™”
// iOS 17+์—์„œ๋Š” registerForTraitChanges ์‚ฌ์šฉ ๊ถŒ์žฅ
// (traitCollectionDidChange๋Š” deprecated ์˜ˆ์ •)
override func viewDidLoad() {
    super.viewDidLoad()
    
    // iOS 17+ ์ƒˆ๋กœ์šด ๋ฐฉ์‹
    if #available(iOS 17.0, *) {
        registerForTraitChanges([UITraitHorizontalSizeClass.self]) { 
            (self: Self, previousTraitCollection: UITraitCollection) in
            self.updateLayoutForSizeClass()
        }
    }
}

// ํŠน์ • Size Class์—์„œ๋งŒ ์ ์šฉํ•  ์ œ์•ฝ ๊ด€๋ฆฌ
var compactConstraints: [NSLayoutConstraint] = []
var regularConstraints: [NSLayoutConstraint] = []

func setupConstraints() {
    // ๊ณตํ†ต ์ œ์•ฝ
    NSLayoutConstraint.activate([
        titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
        titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16)
    ])
    
    // Compact ์ „์šฉ ์ œ์•ฝ (iPhone ์„ธ๋กœ)
    compactConstraints = [
        titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
        subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
        subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16)
    ]
    
    // Regular ์ „์šฉ ์ œ์•ฝ (iPad)
    regularConstraints = [
        titleLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
        subtitleLabel.topAnchor.constraint(equalTo: titleLabel.topAnchor),
        subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 20)
    ]
    
    activateConstraintsForCurrentSizeClass()
}

func activateConstraintsForCurrentSizeClass() {
    if traitCollection.horizontalSizeClass == .compact {
        NSLayoutConstraint.deactivate(regularConstraints)
        NSLayoutConstraint.activate(compactConstraints)
    } else {
        NSLayoutConstraint.deactivate(compactConstraints)
        NSLayoutConstraint.activate(regularConstraints)
    }
}
๐Ÿ“ฆ UIStackView: Auto Layout์˜ ์ตœ๊ฐ• ํŒŒํŠธ๋„ˆ

UIStackView๋Š” Auto Layout์„ ํ›จ์”ฌ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ์ปจํ…Œ์ด๋„ˆ ๋ทฐ์˜ˆ์š”. ๋‚ด๋ถ€ ๋ทฐ๋“ค์„ ์ž๋™์œผ๋กœ ๋ฐฐ์น˜ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์ œ์•ฝ์„ ์ผ์ผ์ด ์„ค์ •ํ•  ํ•„์š”๊ฐ€ ์—†์–ด์š” ใ…‹ใ…‹ ์ง„์งœ ํŽธํ•จ!

๐Ÿ“Š ์ฃผ์š” ์†์„ฑ axis ์ˆ˜ํ‰/์ˆ˜์ง ๋ฐฉํ–ฅ
distribution ํฌ๊ธฐ ๋ถ„๋ฐฐ ๋ฐฉ์‹
alignment ์ •๋ ฌ ๋ฐฉ์‹
spacing ๊ฐ„๊ฒฉ
isLayoutMarginsRelativeArrangement
๐ŸŽ›๏ธ Distribution ์˜ต์…˜ fill ์ฒซ ๋ฒˆ์งธ ๋ทฐ๊ฐ€ ์ฑ„์›€
fillEqually ๊ท ๋“ฑ ๋ถ„๋ฐฐ
fillProportionally ๋น„์œจ ๋ถ„๋ฐฐ
equalSpacing ๋™์ผ ๊ฐ„๊ฒฉ
equalCentering ์ค‘์‹ฌ ๋™์ผ ๊ฐ„๊ฒฉ
// ๋ฐ˜์‘ํ˜• ์นด๋“œ ๋ ˆ์ด์•„์›ƒ ์˜ˆ์‹œ
class AdaptiveCardViewController: UIViewController {
    
    private lazy var mainStackView: UIStackView = {
        let stack = UIStackView()
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.spacing = 16
        stack.isLayoutMarginsRelativeArrangement = true
        stack.layoutMargins = UIEdgeInsets(top: 20, left: 16, bottom: 20, right: 16)
        return stack
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        view.addSubview(mainStackView)
        
        NSLayoutConstraint.activate([
            mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            mainStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mainStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        
        // ์นด๋“œ ๋ทฐ๋“ค ์ถ”๊ฐ€
        ["์นด๋“œ 1", "์นด๋“œ 2", "์นด๋“œ 3"].forEach { title in
            mainStackView.addArrangedSubview(makeCard(title: title))
        }
        
        updateStackViewAxis()
    }
    
    private func updateStackViewAxis() {
        // Size Class์— ๋”ฐ๋ผ ๋ฐฉํ–ฅ ๋ณ€๊ฒฝ!
        if traitCollection.horizontalSizeClass == .regular {
            mainStackView.axis = .horizontal
            mainStackView.distribution = .fillEqually
        } else {
            mainStackView.axis = .vertical
            mainStackView.distribution = .fill
        }
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        UIView.animate(withDuration: 0.3) {
            self.updateStackViewAxis()
            self.view.layoutIfNeeded()  // ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ!
        }
    }
}
๐Ÿ’ก layoutIfNeeded() vs setNeedsLayout() setNeedsLayout(): "๋‹ค์Œ ๋ ˆ์ด์•„์›ƒ ์‚ฌ์ดํด์— ์—…๋ฐ์ดํŠธํ•ด์ค˜" (๋น„๋™๊ธฐ์ )
layoutIfNeeded(): "์ง€๊ธˆ ๋‹น์žฅ ์—…๋ฐ์ดํŠธํ•ด์ค˜" (๋™๊ธฐ์ )
์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝํ•  ๋•Œ๋Š” animate ๋ธ”๋ก ์•ˆ์—์„œ layoutIfNeeded()๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ด์š”!
โšก SwiftUI์—์„œ์˜ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ

์š”์ฆ˜์€ SwiftUI๋„ ๋งŽ์ด ์“ฐ์ž–์•„์š”! SwiftUI์—์„œ๋Š” Auto Layout ๊ฐœ๋…์ด ๋‹ค๋ฅด๊ฒŒ ํ‘œํ˜„๋˜๋Š”๋ฐ, ์ด๊ฒƒ๋„ ์•Œ์•„๋‘๋ฉด ์ข‹์•„์š” ใ…‹ใ…‹

// SwiftUI์—์„œ Size Class ํ™œ์šฉ
struct AdaptiveContentView: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    @Environment(\.verticalSizeClass) var verticalSizeClass
    
    var body: some View {
        Group {
            if horizontalSizeClass == .compact {
                // iPhone ์„ธ๋กœ: ์ˆ˜์ง ๋ ˆ์ด์•„์›ƒ
                VStack(spacing: 16) {
                    CardView(title: "์นด๋“œ 1")
                    CardView(title: "์นด๋“œ 2")
                    CardView(title: "์นด๋“œ 3")
                }
                .padding()
            } else {
                // iPad / iPhone Plus ๊ฐ€๋กœ: ์ˆ˜ํ‰ ๋ ˆ์ด์•„์›ƒ
                HStack(spacing: 20) {
                    CardView(title: "์นด๋“œ 1")
                    CardView(title: "์นด๋“œ 2")
                    CardView(title: "์นด๋“œ 3")
                }
                .padding()
            }
        }
    }
}

// ViewThatFits - iOS 16+์—์„œ ์ž๋™ ์ ์‘!
struct SmartButtonView: View {
    var body: some View {
        ViewThatFits {
            // ๊ณต๊ฐ„์ด ์ถฉ๋ถ„ํ•˜๋ฉด ๊ฐ€๋กœ ๋ฐฐ์น˜
            HStack {
                Button("์ทจ์†Œ") { }
                Button("ํ™•์ธ") { }
            }
            // ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•˜๋ฉด ์„ธ๋กœ ๋ฐฐ์น˜
            VStack {
                Button("ํ™•์ธ") { }
                Button("์ทจ์†Œ") { }
            }
        }
    }
}

// GeometryReader๋กœ ์ •๋ฐ€ํ•œ ํฌ๊ธฐ ์ œ์–ด
struct ResponsiveGridView: View {
    var body: some View {
        GeometryReader { geometry in
            let columns = geometry.size.width > 600 ? 3 : 2
            let itemWidth = (geometry.size.width - CGFloat(columns + 1) * 16) / CGFloat(columns)
            
            LazyVGrid(columns: Array(repeating: GridItem(.fixed(itemWidth)), count: columns), spacing: 16) {
                ForEach(0..<12) { index in
                    RoundedRectangle(cornerRadius: 12)
                        .fill(Color.blue.opacity(0.2))
                        .frame(height: itemWidth * 0.75)
                        .overlay(Text("์•„์ดํ…œ \(index + 1)"))
                }
            }
            .padding(16)
        }
    }
}
๐ŸŽจ ์‹ค์ „ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ํŒจํ„ด ํŒจํ„ด 1: ์ปฌ๋Ÿผ ์ ์‘ํ˜• Compact (iPhone) ์•„์ดํ…œ 1 ์•„์ดํ…œ 2 ์•„์ดํ…œ 3 ์•„์ดํ…œ 4 โ†’ Regular (iPad) ์•„์ดํ…œ 1 ์•„์ดํ…œ 2 ์•„์ดํ…œ 3 ์•„์ดํ…œ 4 ํŒจํ„ด 2: ์‚ฌ์ด๋“œ๋ฐ” ์ ์‘ํ˜• Compact (iPhone) ๋ฉ”์ธ ์ฝ˜ํ…์ธ  ํ™ˆ ๊ฒ€์ƒ‰ ์„ค์ • โ†’ Regular (iPad) ํ™ˆ ๊ฒ€์ƒ‰ ์„ค์ • ๋ฉ”์ธ ์ฝ˜ํ…์ธ  UICollectionView + CompositionalLayout NSCollectionLayoutEnvironment๋กœ Size Class ๊ฐ์ง€ ํ›„ ์„น์…˜ ์žฌ๊ตฌ์„ฑ UISplitViewController preferredDisplayMode๋กœ ์ž๋™ ์‚ฌ์ด๋“œ๋ฐ” ์ „ํ™˜ ์ฒ˜๋ฆฌ ํŒจํ„ด 3: Safe Area ์ธ์‹ ๋ ˆ์ด์•„์›ƒ safeAreaLayoutGuide ํ™œ์šฉ ๋…ธ์น˜, Dynamic Island, ํ™ˆ ์ธ๋””์ผ€์ดํ„ฐ, ์ƒํƒœ๋ฐ” ์˜์—ญ์„ ์ž๋™์œผ๋กœ ํ”ผํ•ด์„œ ๋ ˆ์ด์•„์›ƒ ๋ฐฐ์น˜ view.safeAreaLayoutGuide.topAnchor / bottomAnchor ์‚ฌ์šฉ ํ•„์ˆ˜!
๐Ÿ—๏ธ ์‹ค์ „ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ํŒจํ„ด
๐Ÿ”ฒ UICollectionView Compositional Layout

iOS 13+์—์„œ ๋„์ž…๋œ Compositional Layout์€ Size Class์— ๋”ฐ๋ฅธ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ ๊ตฌํ˜„์— ์ตœ๊ฐ•์ด์—์š”!

// Size Class์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ๋ณ€ํ•˜๋Š” CollectionView ๋ ˆ์ด์•„์›ƒ
func createAdaptiveLayout() -> UICollectionViewLayout {
    let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
        
        // layoutEnvironment.container.effectiveContentSizeCategory ํ™œ์šฉ!
        let isRegular = layoutEnvironment.traitCollection.horizontalSizeClass == .regular
        
        // ์ปฌ๋Ÿผ ์ˆ˜ ๊ฒฐ์ •
        let columnCount = isRegular ? 3 : 2
        
        // ์•„์ดํ…œ ํฌ๊ธฐ ์„ค์ •
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0 / CGFloat(columnCount)),
            heightDimension: .fractionalHeight(1.0)
        )
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
        
        // ๊ทธ๋ฃน ์„ค์ •
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .absolute(isRegular ? 200 : 150)
        )
        let group = NSCollectionLayoutGroup.horizontal(
            layoutSize: groupSize,
            subitems: [item]
        )
        
        // ์„น์…˜ ์„ค์ •
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
        
        // ํ—ค๋” ์ถ”๊ฐ€ (Regular์—์„œ๋งŒ)
        if isRegular {
            let headerSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(60)
            )
            let header = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: headerSize,
                elementKind: UICollectionView.elementKindSectionHeader,
                alignment: .top
            )
            section.boundarySupplementaryItems = [header]
        }
        
        return section
    }
    
    return layout
}
๐Ÿ—‚๏ธ UISplitViewController ๋ฐ˜์‘ํ˜• ์„ค์ •
// iPad์—์„œ ์‚ฌ์ด๋“œ๋ฐ”, iPhone์—์„œ ํƒญ๋ฐ”๋กœ ์ž๋™ ์ „ํ™˜
class MainSplitViewController: UISplitViewController {
    
    init() {
        super.init(style: .doubleColumn)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ๊ธฐ๋ณธ ํ‘œ์‹œ ๋ชจ๋“œ ์„ค์ •
        preferredDisplayMode = .oneBesideSecondary
        
        // iPhone์—์„œ ์ž๋™์œผ๋กœ ํƒญ๋ฐ” ์Šคํƒ€์ผ๋กœ ์ „ํ™˜
        preferredSplitBehavior = .tile
        
        // ์ตœ์†Œ/์ตœ๋Œ€ ๋„ˆ๋น„ ์„ค์ •
        minimumPrimaryColumnWidth = 280
        maximumPrimaryColumnWidth = 320
        preferredPrimaryColumnWidthFraction = 0.35
        
        // ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ ์„ค์ •
        setViewController(SidebarViewController(), for: .primary)
        setViewController(ContentViewController(), for: .secondary)
        setViewController(CompactTabBarController(), for: .compact)
    }
}
๐Ÿ“ฑ Safe Area ์™„๋ฒฝ ๋Œ€์‘
// Safe Area๋ฅผ ๊ณ ๋ คํ•œ ์™„๋ฒฝํ•œ ๋ ˆ์ด์•„์›ƒ
class SafeAreaAwareViewController: UIViewController {
    
    private let bottomButton = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBottomButton()
    }
    
    private func setupBottomButton() {
        bottomButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(bottomButton)
        
        NSLayoutConstraint.activate([
            bottomButton.leadingAnchor.constraint(
                equalTo: view.safeAreaLayoutGuide.leadingAnchor, 
                constant: 16
            ),
            bottomButton.trailingAnchor.constraint(
                equalTo: view.safeAreaLayoutGuide.trailingAnchor, 
                constant: -16
            ),
            // Safe Area ํ•˜๋‹จ ๊ธฐ์ค€์œผ๋กœ ๋ฐฐ์น˜ (ํ™ˆ ์ธ๋””์ผ€์ดํ„ฐ ์œ„)
            bottomButton.bottomAnchor.constraint(
                equalTo: view.safeAreaLayoutGuide.bottomAnchor, 
                constant: -16
            ),
            bottomButton.heightAnchor.constraint(equalToConstant: 52)
        ])
    }
    
    // Safe Area ๋ณ€๊ฒฝ ๊ฐ์ง€ (iPad ๋ฉ€ํ‹ฐํƒœ์Šคํ‚น ๋“ฑ)
    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        // Safe Area๊ฐ€ ๋ฐ”๋€” ๋•Œ ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ
        print("Safe Area ๋ณ€๊ฒฝ: \(view.safeAreaInsets)")
    }
}
๐Ÿ› Auto Layout ๋””๋ฒ„๊น… ๊ฟ€ํŒ

Auto Layout ์—๋Ÿฌ ๋””๋ฒ„๊น…์€ ์ง„์งœ ๊ณ ํ†ต์Šค๋Ÿฝ์ฃ  ใ…‹ใ…‹ใ…‹ ๊ทธ๋ž˜๋„ ์ด ๋ฐฉ๋ฒ•๋“ค ์•Œ๋ฉด ํ›จ์”ฌ ํŽธํ•ด์š”!

1
์ฝ˜์†” ์—๋Ÿฌ ์ฝ๊ธฐ
UIViewAlertForUnsatisfiableConstraints ์—๋Ÿฌ๊ฐ€ ๋œจ๋ฉด ์ œ์•ฝ ์ถฉ๋Œ์ด์—์š”. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์—์„œ ๋ทฐ ์ฃผ์†Œ๋ฅผ ์ฐพ์•„ po 0x7f8a1234๋กœ ์–ด๋–ค ๋ทฐ์ธ์ง€ ํ™•์ธํ•˜์„ธ์š”!
2
accessibilityIdentifier ํ™œ์šฉ
๋ทฐ์— accessibilityIdentifier = "myButton" ์„ค์ •ํ•˜๋ฉด ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์—์„œ ์ด๋ฆ„์œผ๋กœ ์‹๋ณ„ ๊ฐ€๋Šฅํ•ด์š”!
3
Xcode View Debugger
Debug โ†’ View Debugging โ†’ Capture View Hierarchy๋กœ 3D ๋ทฐ ๊ณ„์ธต ํ™•์ธ! ๊ฒน์น˜๊ฑฐ๋‚˜ ์ž˜๋ฆฐ ๋ทฐ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด์š”.
4
๋Ÿฐํƒ€์ž„ ์ œ์•ฝ ํ™•์ธ
view.constraints๋กœ ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ์ œ์•ฝ ๋ชฉ๋ก ํ™•์ธ, UIView._autolayoutTrace()๋กœ ์ „์ฒด ๊ณ„์ธต ํŠธ๋ ˆ์ด์Šค ๊ฐ€๋Šฅ!
// ๋””๋ฒ„๊น…์šฉ ์œ ํ‹ธ๋ฆฌํ‹ฐ Extension
extension UIView {
    // ๋ทฐ ํ…Œ๋‘๋ฆฌ ์ƒ‰์ƒ์œผ๋กœ ๋ ˆ์ด์•„์›ƒ ์‹œ๊ฐํ™”
    func debugLayout(color: UIColor = .red) {
        #if DEBUG
        layer.borderColor = color.cgColor
        layer.borderWidth = 1
        subviews.forEach { $0.debugLayout(color: color.withAlphaComponent(0.7)) }
        #endif
    }
    
    // ํ˜„์žฌ ์ œ์•ฝ ์ถœ๋ ฅ
    func printConstraints() {
        #if DEBUG
        print("=== \(type(of: self)) ์ œ์•ฝ ๋ชฉ๋ก ===")
        constraints.forEach { print($0) }
        #endif
    }
}

// ์‚ฌ์šฉ๋ฒ•
override func viewDidLoad() {
    super.viewDidLoad()
    // DEBUG ๋นŒ๋“œ์—์„œ๋งŒ ๋™์ž‘
    view.debugLayout()
}
โš ๏ธ ํ”ํ•œ ์‹ค์ˆ˜ TOP 3 1๏ธโƒฃ translatesAutoresizingMaskIntoConstraints = false ๋นผ๋จน๊ธฐ
2๏ธโƒฃ ์ œ์•ฝ์„ activate ์•ˆ ํ•˜๊ณ  ๊ทธ๋ƒฅ ์ƒ์„ฑ๋งŒ ํ•˜๊ธฐ
3๏ธโƒฃ safeAreaLayoutGuide ๋Œ€์‹  view์— ์ง์ ‘ ์ œ์•ฝ ๊ฑธ์–ด์„œ ๋…ธ์น˜์— ๊ฐ€๋ฆฌ๊ธฐ
โšก Auto Layout ์„ฑ๋Šฅ ์ตœ์ ํ™”

Auto Layout์ด ํŽธ๋ฆฌํ•˜๊ธด ํ•œ๋ฐ, ์ž˜๋ชป ์“ฐ๋ฉด ์„ฑ๋Šฅ ์ด์Šˆ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด์š”. ํŠนํžˆ ์Šคํฌ๋กค๋ทฐ ์•ˆ์—์„œ ๋ณต์žกํ•œ ์ œ์•ฝ์ด ๋งŽ์œผ๋ฉด ํ”„๋ ˆ์ž„ ๋“œ๋ž์ด ๋‚  ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”!

๐Ÿš€ ์„ฑ๋Šฅ ํ–ฅ์ƒ ํŒ
  • โœ… ์ œ์•ฝ ์ˆ˜ ์ตœ์†Œํ™”
  • โœ… UIStackView ์ ๊ทน ํ™œ์šฉ
  • โœ… ๋ถˆํ•„์š”ํ•œ ๋ทฐ ๊ณ„์ธต ์ค„์ด๊ธฐ
  • โœ… ์…€ ์žฌ์‚ฌ์šฉ ์‹œ ์ œ์•ฝ ์žฌ์ƒ์„ฑ ๊ธˆ์ง€
  • โœ… ๋ณต์žกํ•œ ์…€์€ ์ˆ˜๋™ ๋ ˆ์ด์•„์›ƒ ๊ณ ๋ ค
โš ๏ธ ํ”ผํ•ด์•ผ ํ•  ๊ฒƒ๋“ค
  • โŒ ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ œ์•ฝ ๋ณ€๊ฒฝ
  • โŒ ๋ถˆํ•„์š”ํ•œ ์ œ์•ฝ activate/deactivate
  • โŒ ์ค‘์ฒฉ๋œ UIStackView ๊ณผ๋‹ค ์‚ฌ์šฉ
  • โŒ ์…€๋งˆ๋‹ค ์ƒˆ ์ œ์•ฝ ์ƒ์„ฑ
  • โŒ ๋ณต์žกํ•œ ์ œ์•ฝ ๊ณ„์‚ฐ์„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ
// ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์…€ ์žฌ์‚ฌ์šฉ ์‹œ ์ œ์•ฝ ๊ด€๋ฆฌ
class OptimizedTableViewCell: UITableViewCell {
    
    private let titleLabel = UILabel()
    private var dynamicHeightConstraint: NSLayoutConstraint?
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupOnce()  // ์ œ์•ฝ์€ ํ•œ ๋ฒˆ๋งŒ ์„ค์ •!
    }
    
    required init?(coder: NSCoder) {
        fatalError()
    }
    
    private func setupOnce() {
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(titleLabel)
        
        // ๊ณ ์ • ์ œ์•ฝ์€ ํ•œ ๋ฒˆ๋งŒ activate
        NSLayoutConstraint.activate([
            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12)
        ])
        
        // ๋™์ ์œผ๋กœ ๋ณ€ํ•˜๋Š” ์ œ์•ฝ์€ ์ฐธ์กฐ ๋ณด๊ด€
        dynamicHeightConstraint = titleLabel.heightAnchor.constraint(equalToConstant: 44)
        dynamicHeightConstraint?.isActive = true
    }
    
    func configure(with height: CGFloat) {
        // ์ œ์•ฝ ์žฌ์ƒ์„ฑ ์—†์ด constant๋งŒ ๋ณ€๊ฒฝ! (ํ›จ์”ฌ ๋น ๋ฆ„)
        dynamicHeightConstraint?.constant = height
    }
}
๐ŸŽฏ ํ•ต์‹ฌ ์ •๋ฆฌ

Auto Layout๊ณผ Size Classes๋ฅผ ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ฉด iPhone SE๋ถ€ํ„ฐ iPad Pro๊นŒ์ง€ ํ•˜๋‚˜์˜ ์ฝ”๋“œ๋ฒ ์ด์Šค๋กœ ์™„๋ฒฝํ•œ ๋ฐ˜์‘ํ˜• UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”!

Auto Layout ํ•ต์‹ฌ
โ€ข ์ œ์•ฝ ๊ธฐ๋ฐ˜ ์ˆ˜ํ•™์  ๋ ˆ์ด์•„์›ƒ
โ€ข Priority๋กœ ์ถฉ๋Œ ํ•ด๊ฒฐ
โ€ข Anchor API๋กœ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ
โ€ข translatesAutoresizingMask = false ํ•„์ˆ˜
Size Classes ํ•ต์‹ฌ
โ€ข Compact vs Regular ๋‘ ๊ฐ€์ง€
โ€ข traitCollectionDidChange ๊ฐ์ง€
โ€ข ์ œ์•ฝ ๊ทธ๋ฃน activate/deactivate
โ€ข iOS 17+ registerForTraitChanges
์‹ค์ „ ํ™œ์šฉ ํŒจํ„ด
UIStackView axis ๋™์  ๋ณ€๊ฒฝ โ†’ Compositional Layout โ†’ UISplitViewController โ†’ Safe Area ๋Œ€์‘ โ†’ ์„ฑ๋Šฅ ์ตœ์ ํ™”(์ œ์•ฝ ์žฌ์‚ฌ์šฉ)
๐Ÿ’ฌ ๋งˆ์น˜๋ฉฐ

Auto Layout๊ณผ Size Classes๋Š” ์ฒ˜์Œ์—” ์ง„์งœ ์–ด๋ ต๊ฒŒ ๋А๊ปด์ง€๋Š”๋ฐ, ํ•ต์‹ฌ ๊ฐœ๋…๋งŒ ์žก์œผ๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์ฒด๊ณ„์ ์ด์—์š” ใ…‹ใ…‹. ์ œ์•ฝ์ด ์ˆ˜ํ•™ ๋ฐฉ์ •์‹์ด๋ผ๋Š” ๊ฑธ ์ดํ•ดํ•˜๊ณ , Size Class๋กœ ๊ธฐ๊ธฐ ํ™˜๊ฒฝ์„ ๋ถ„๋ฅ˜ํ•˜๋Š” ๊ฐœ๋…๋งŒ ์žก์œผ๋ฉด ๋‚˜๋จธ์ง€๋Š” ์‘์šฉ์ด๊ฑฐ๋“ ์š”!

iOS ๊ฐœ๋ฐœ ์‹ค๋ ฅ์„ ํ‚ค์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ Swift/iOS ๊ฐœ๋ฐœ ๊ด€๋ จ ๋ฉ˜ํ† ๋ง์ด๋‚˜ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์„œ๋น„์Šค๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”. ์‹ค์ œ ํ”„๋กœ์ ํŠธ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์ง์ ‘ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์œผ๋ฉด ์„ฑ์žฅ ์†๋„๊ฐ€ ํ›จ์”ฌ ๋นจ๋ผ์ง€๊ฑฐ๋“ ์š”! ๐Ÿš€

๐ŸŽ“ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๊ธฐ Auto Layout ๋งˆ์Šคํ„ฐ ํ›„์—๋Š” โ†’ UIKit Dynamics(๋ฌผ๋ฆฌ ๊ธฐ๋ฐ˜ ์• ๋‹ˆ๋ฉ”์ด์…˜), Core Animation(๋ ˆ์ด์–ด ๊ธฐ๋ฐ˜ ์• ๋‹ˆ๋ฉ”์ด์…˜), SwiftUI + UIKit ํ˜ผํ•ฉ ์‚ฌ์šฉ์„ ๊ณต๋ถ€ํ•ด๋ณด์„ธ์š”! ๋ฐ˜์‘ํ˜• UI์— ์ƒ๋™๊ฐ๊นŒ์ง€ ๋”ํ•  ์ˆ˜ ์žˆ์–ด์š” โœจ
๋Œ“๊ธ€ ์ž‘์„ฑ

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

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

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