์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” iOS ์•ฑ ์ ‘๊ทผ์„ฑ์˜ ํ˜๋ช…: ํฐ ๊ธ€์”จ ์ง€์›์œผ๋กœ ๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์•ฑ ๊ฐœ๋ฐœํ•˜๊ธฐ

๐Ÿ” iOS ์•ฑ ์ ‘๊ทผ์„ฑ์˜ ํ˜๋ช…: ํฐ ๊ธ€์”จ ์ง€์›์œผ๋กœ ๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์•ฑ ๊ฐœ๋ฐœํ•˜๊ธฐ

์š”์ฆ˜ ์•ฑ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฒŒ ๋ญ”์ง€ ์•Œ์•„? ๋ฐ”๋กœ ์ ‘๊ทผ์„ฑ(Accessibility)์ด์•ผ! ํŠนํžˆ iOS์—์„œ ํฐ ๊ธ€์”จ ์ง€์›์€ ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ํ•„์ˆ˜๊ฐ€ ๋˜์—ˆ๋‹ค๊ตฌ ใ…‹ใ…‹ ์‹œ๊ฐ์  ์ ‘๊ทผ์„ฑ์„ ๊ณ ๋ คํ•˜์ง€ ์•Š์€ ์•ฑ์€ ์ด์ œ ์•ฑ์Šคํ† ์–ด์—์„œ๋„ ๋ฆฌ์ ๋‹นํ•  ์ˆ˜ ์žˆ์–ด์„œ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ง„์งœ ์‹ ๊ฒฝ ๋งŽ์ด ์“ฐ๊ณ  ์žˆ์–ด ๐Ÿ’ช

์• ํ”Œ์ด iOS์—์„œ ์ œ๊ณตํ•˜๋Š” Dynamic Type๊ณผ ํฐ ๊ธ€์”จ ์ง€์› ๊ธฐ๋Šฅ๋“ค์€ ๋‹จ์ˆœํžˆ ๊ธ€์ž๋งŒ ํฌ๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์ง„์งœ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ˜์‹ ์ ์œผ๋กœ ๋ฐ”๊พธ๋Š” ๊ธฐ์ˆ ์ด๊ฑฐ๋“ ? ์˜ค๋Š˜์€ ์ด ๋ชจ๋“  ๊ฑธ ํŒŒํ—ค์ณ๋ณด์ž๊ตฌ!

๐Ÿ“ฑ iOS ์ ‘๊ทผ์„ฑ์˜ ์ง„ํ™”: Dynamic Type์ด ๋ญ”๋ฐ?

Dynamic Type์€ iOS 7๋ถ€ํ„ฐ ๋„์ž…๋œ ๊ธฐ๋Šฅ์ธ๋ฐ, ์‚ฌ์šฉ์ž๊ฐ€ ์‹œ์Šคํ…œ ์„ค์ •์—์„œ ๊ธ€์ž ํฌ๊ธฐ๋ฅผ ์กฐ์ ˆํ•˜๋ฉด ์•ฑ์˜ ๋ชจ๋“  ํ…์ŠคํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ํฌ๊ธฐ๊ฐ€ ๋ฐ”๋€Œ๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ๊ธฐ๋Šฅ์ด์•ผ โœจ

์ž‘์€ ๊ธ€์”จ ์ฝ๊ธฐ ์–ด๋ ค์›Œ์š” ๐Ÿ˜ฐ Dynamic Type ํฐ ๊ธ€์”จ ์ฝ๊ธฐ ํŽธํ•ด์š”! ๐Ÿ˜Š A A iOS Dynamic Type ์ž‘๋™ ์›๋ฆฌ

๊ทผ๋ฐ ์ด๊ฒŒ ์™œ ์ค‘์š”ํ•˜๋ƒ๋ฉด, ์ „ ์„ธ๊ณ„์ ์œผ๋กœ ์‹œ๊ฐ ์žฅ์• ๋‚˜ ์ €์‹œ๋ ฅ์„ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๊ฐ€ 2์–ต 5์ฒœ๋งŒ ๋ช…์ด ๋„˜๊ฑฐ๋“ ? ๊ทธ๋ฆฌ๊ณ  ๋‚˜์ด๊ฐ€ ๋“ค๋ฉด์„œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‹œ๋ ฅ์ด ์•ฝํ•ด์ง€๋Š” ์‚ฌ๋žŒ๋“ค๊นŒ์ง€ ๊ณ ๋ คํ•˜๋ฉด ์ •๋ง ์—„์ฒญ๋‚œ ์ˆซ์ž์•ผ ๐Ÿ˜ฎ

๐Ÿ’ก ๊ฟ€ํŒ: ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ ‘๊ทผ์„ฑ ๊ด€๋ จ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ์ฐพ๋Š” ๊ธฐ์—…๋“ค์ด ๋งŽ์•„์ง€๊ณ  ์žˆ์–ด! ์ด ๋ถ„์•ผ ์ „๋ฌธ์„ฑ์„ ํ‚ค์šฐ๋ฉด ์ •๋ง ์ข‹์€ ๊ธฐํšŒ๊ฐ€ ๋งŽ์„ ๊ฑฐ์•ผ ใ…‹ใ…‹

๐Ÿ› ๏ธ Dynamic Type ๊ตฌํ˜„์˜ ํ•ต์‹ฌ ๊ธฐ์ˆ ๋“ค

1. Text Style๊ณผ Content Size Category

iOS์—์„œ๋Š” ๋ฏธ๋ฆฌ ์ •์˜๋œ ํ…์ŠคํŠธ ์Šคํƒ€์ผ๋“ค์ด ์žˆ์–ด. ์ด๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ Dynamic Type์ด ์ ์šฉ๋˜์ง€!

// UIKit ๊ธฐ๋ณธ ํ…์ŠคํŠธ ์Šคํƒ€์ผ๋“ค
UIFont.TextStyle.largeTitle     // 34pt (๊ธฐ๋ณธ ํฌ๊ธฐ)
UIFont.TextStyle.title1         // 28pt
UIFont.TextStyle.title2         // 22pt
UIFont.TextStyle.title3         // 20pt
UIFont.TextStyle.headline       // 17pt (Bold)
UIFont.TextStyle.body          // 17pt (๊ธฐ๋ณธ ๋ณธ๋ฌธ)
UIFont.TextStyle.callout       // 16pt
UIFont.TextStyle.subheadline   // 15pt
UIFont.TextStyle.footnote      // 13pt
UIFont.TextStyle.caption1      // 12pt
UIFont.TextStyle.caption2      // 11pt

// ์‚ฌ์šฉ ์˜ˆ์‹œ
let titleLabel = UILabel()
titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontForContentSizeCategory = true

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฑด adjustsFontForContentSizeCategory = true์•ผ! ์ด๊ฑธ ์„ค์ •ํ•ด์•ผ ์‚ฌ์šฉ์ž๊ฐ€ ์„ค์ •์—์„œ ๊ธ€์ž ํฌ๊ธฐ๋ฅผ ๋ฐ”๊ฟจ์„ ๋•Œ ์ž๋™์œผ๋กœ ๋ฐ˜์˜๋ผ ใ…‹ใ…‹

2. Content Size Category์˜ 11๋‹จ๊ณ„

iOS๋Š” ๊ธ€์ž ํฌ๊ธฐ๋ฅผ 11๋‹จ๊ณ„๋กœ ๋‚˜๋ˆ ๋†จ์–ด:

iOS Content Size Category 11๋‹จ๊ณ„ ์ผ๋ฐ˜ ํฌ๊ธฐ (7๋‹จ๊ณ„) ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ (4๋‹จ๊ณ„) XS (Extra Small) ๊ฐ€์žฅ ์ž‘์€ ํฌ๊ธฐ S (Small) ์ž‘์€ ํฌ๊ธฐ M (Medium) ์ค‘๊ฐ„ ํฌ๊ธฐ L (Large) โญ ๊ธฐ๋ณธ ํฌ๊ธฐ XL (Extra Large) ํฐ ํฌ๊ธฐ XXL ๋” ํฐ ํฌ๊ธฐ XXXL ๊ฐ€์žฅ ํฐ ์ผ๋ฐ˜ ํฌ๊ธฐ AX1 ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ 1๋‹จ๊ณ„ AX2 ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ 2๋‹จ๊ณ„ AX3 ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ 3๋‹จ๊ณ„ AX4 ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ 4๋‹จ๊ณ„ AX5 ์ตœ๋Œ€ ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ ํฌ๊ธฐ ์ฆ๊ฐ€ โ†’

์ ‘๊ทผ์„ฑ ํฌ๊ธฐ(AX1~AX5)๋Š” ์ง„์งœ ํฌ๋‹ค๊ตฌ! AX5๋Š” ์ผ๋ฐ˜ ํฌ๊ธฐ์˜ ๊ฑฐ์˜ 3๋ฐฐ๊นŒ์ง€ ์ปค์ ธ์„œ, ์ €์‹œ๋ ฅ ์‚ฌ์šฉ์ž๋“ค๋„ ํŽธํ•˜๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ์–ด ๐Ÿ‘€

๐Ÿ’ป SwiftUI์—์„œ์˜ Dynamic Type ๊ตฌํ˜„

SwiftUI์—์„œ๋Š” ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ Dynamic Type์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด! ์ง„์งœ ์• ํ”Œ์ด ๊ฐœ๋ฐœ์ž๋“ค ํŽธ์˜๋ฅผ ๋งŽ์ด ์ƒ๊ฐํ•œ ๊ฑฐ ๊ฐ™์•„ ใ…‹ใ…‹

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            // ๊ธฐ๋ณธ Dynamic Type ์ ์šฉ
            Text("์ œ๋ชฉ์ž…๋‹ˆ๋‹ค")
                .font(.largeTitle)
            
            Text("๋ณธ๋ฌธ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ์ด ํ…์ŠคํŠธ๋Š” ์‚ฌ์šฉ์ž ์„ค์ •์— ๋”ฐ๋ผ ํฌ๊ธฐ๊ฐ€ ์ž๋™์œผ๋กœ ์กฐ์ ˆ๋ฉ๋‹ˆ๋‹ค.")
                .font(.body)
            
            // ์ปค์Šคํ…€ ํฐํŠธ์— Dynamic Type ์ ์šฉ
            Text("์ปค์Šคํ…€ ํฐํŠธ")
                .font(.custom("HelveticaNeue-Bold", size: 18, relativeTo: .headline))
            
            // ์ตœ๋Œ€/์ตœ์†Œ ํฌ๊ธฐ ์ œํ•œ
            Text("ํฌ๊ธฐ ์ œํ•œ๋œ ํ…์ŠคํŠธ")
                .font(.body)
                .dynamicTypeSize(.medium ... .accessibility3)
        }
        .padding()
    }
}

// ํ™˜๊ฒฝ๊ฐ’์œผ๋กœ ํ˜„์žฌ Dynamic Type ํฌ๊ธฐ ๊ฐ์ง€
struct DynamicTypeAwareView: View {
    @Environment(\.dynamicTypeSize) var dynamicTypeSize
    
    var body: some View {
        VStack {
            if dynamicTypeSize >= .accessibility1 {
                // ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ์ผ ๋•Œ ๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝ
                VStack {
                    Image(systemName: "star")
                    Text("์ ‘๊ทผ์„ฑ ๋ชจ๋“œ")
                }
            } else {
                // ์ผ๋ฐ˜ ํฌ๊ธฐ์ผ ๋•Œ ๋ ˆ์ด์•„์›ƒ
                HStack {
                    Image(systemName: "star")
                    Text("์ผ๋ฐ˜ ๋ชจ๋“œ")
                }
            }
        }
    }
}

SwiftUI์˜ .dynamicTypeSize() modifier๋Š” ์ง„์งœ ๊ฟ€๊ธฐ๋Šฅ์ด์•ผ! ํŠน์ • ๋ทฐ์˜ Dynamic Type ๋ฒ”์œ„๋ฅผ ์ œํ•œํ•  ์ˆ˜ ์žˆ์–ด์„œ, ๋ ˆ์ด์•„์›ƒ์ด ๊นจ์ง€๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“  ๐Ÿ›ก๏ธ

๐ŸŽจ ๋ ˆ์ด์•„์›ƒ ๋Œ€์‘ ์ „๋žต: ๊นจ์ง€์ง€ ์•Š๋Š” UI ๋งŒ๋“ค๊ธฐ

ํฐ ๊ธ€์”จ๋ฅผ ์ง€์›ํ•  ๋•Œ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ๊ฐ€ ๋ญ”์ง€ ์•Œ์•„? ๋ฐ”๋กœ ๋ ˆ์ด์•„์›ƒ์ด ๊นจ์ง€๋Š” ๊ฒƒ! ํŠนํžˆ ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ์—์„œ๋Š” ๊ธ€์ž๊ฐ€ ๋„ˆ๋ฌด ์ปค์ ธ์„œ UI๊ฐ€ ์™„์ „ํžˆ ๋ง๊ฐ€์งˆ ์ˆ˜ ์žˆ์–ด ๐Ÿ˜ฑ

Dynamic Type ๋ ˆ์ด์•„์›ƒ ๋Œ€์‘ ์ „๋žต โŒ ์ž˜๋ชป๋œ ๊ตฌํ˜„ ๋ฒ„ํŠผ์ด ์ž‘์•„์š” ํ…์ŠคํŠธ๊ฐ€ ์ž˜๋ ค์š”... ๊ณ ์ • ํฌ๊ธฐ๋กœ ์ธํ•œ ๋ฌธ์ œ์  โ€ข ํ…์ŠคํŠธ ์ž˜๋ฆผ โ€ข ํ„ฐ์น˜ ์˜์—ญ ๋ถ€์กฑ โ€ข ๊ฐ€๋…์„ฑ ์ €ํ•˜ โœ… ์˜ฌ๋ฐ”๋ฅธ ๊ตฌํ˜„ ์ ์‘ํ˜• ๋ฒ„ํŠผ ํฌ๊ธฐ ์ž๋™ ์กฐ์ ˆ ์œ ์—ฐํ•œ ๋ ˆ์ด์•„์›ƒ์˜ ์žฅ์  โ€ข ์ž๋™ ํฌ๊ธฐ ์กฐ์ ˆ โ€ข ์ถฉ๋ถ„ํ•œ ํ„ฐ์น˜ ์˜์—ญ โ€ข ๋ชจ๋“  ํฌ๊ธฐ ์ง€์› ํ•ต์‹ฌ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•๋“ค 1 Auto Layout ํ™œ์šฉ ์ œ์•ฝ ์กฐ๊ฑด์œผ๋กœ ์œ ์—ฐํ•œ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ 2 Priority ์„ค์ • ์ค‘์š”๋„์— ๋”ฐ๋ฅธ ์ œ์•ฝ ์šฐ์„ ์ˆœ์œ„ ์กฐ์ • 3 Stack View ํ™œ์šฉ ์ž๋™ ๋ฐฐ์น˜์™€ ๊ฐ„๊ฒฉ ์กฐ์ ˆ 4 ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์•„์›ƒ ํฌ๊ธฐ์— ๋”ฐ๋ฅธ ๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝ ๐Ÿ’ก Pro Tip: ์ตœ์†Œ 44pt์˜ ํ„ฐ์น˜ ์˜์—ญ์„ ๋ณด์žฅํ•˜๊ณ , ํ…์ŠคํŠธ๊ฐ€ 2์ค„ ์ด์ƒ์ผ ๋•Œ๋ฅผ ๋Œ€๋น„ํ•œ ๋†’์ด ์กฐ์ ˆ์„ ํ•ญ์ƒ ๊ณ ๋ คํ•˜์„ธ์š”!

์‹ค์ „ Auto Layout ์ฝ”๋“œ

class DynamicTypeButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButton()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupButton()
    }
    
    private func setupButton() {
        // Dynamic Type ํฐํŠธ ์„ค์ •
        titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
        titleLabel?.adjustsFontForContentSizeCategory = true
        
        // ๋‹ค์ค‘ ๋ผ์ธ ์ง€์›
        titleLabel?.numberOfLines = 0
        titleLabel?.lineBreakMode = .byWordWrapping
        
        // ์ตœ์†Œ ํ„ฐ์น˜ ์˜์—ญ ๋ณด์žฅ (44pt)
        NSLayoutConstraint.activate([
            heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
            widthAnchor.constraint(greaterThanOrEqualToConstant: 44)
        ])
        
        // ๋‚ด์šฉ์— ๋งž๋Š” ํฌ๊ธฐ ์กฐ์ ˆ
        setContentHuggingPriority(.required, for: .vertical)
        setContentCompressionResistancePriority(.required, for: .vertical)
        
        // ํŒจ๋”ฉ ์„ค์ •
        contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
        
        // ํฐํŠธ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์•Œ๋ฆผ ์ˆ˜์‹ 
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(contentSizeCategoryDidChange),
            name: UIContentSizeCategory.didChangeNotification,
            object: nil
        )
    }
    
    @objc private func contentSizeCategoryDidChange() {
        // ํฐํŠธ ํฌ๊ธฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ
        titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
        invalidateIntrinsicContentSize()
        setNeedsLayout()
    }
}

๐Ÿ”ง ๊ณ ๊ธ‰ Dynamic Type ๊ธฐ๋ฒ•๋“ค

1. ์ปค์Šคํ…€ ํฐํŠธ์— Dynamic Type ์ ์šฉํ•˜๊ธฐ

๋ธŒ๋žœ๋“œ ํฐํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ๋„ Dynamic Type์„ ์ง€์›ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด? ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ผ! ใ…‹ใ…‹

extension UIFont {
    static func customFont(style: TextStyle, weight: Weight = .regular) -> UIFont {
        // ํ˜„์žฌ Content Size Category ๊ฐ€์ ธ์˜ค๊ธฐ
        let contentSize = UIApplication.shared.preferredContentSizeCategory
        
        // ๊ธฐ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ
        let baseSize = UIFont.preferredFont(forTextStyle: style).pointSize
        
        // ์ปค์Šคํ…€ ํฐํŠธ ์ด๋ฆ„
        let fontName = weight == .bold ? "MyCustomFont-Bold" : "MyCustomFont-Regular"
        
        // ์ปค์Šคํ…€ ํฐํŠธ ์ƒ์„ฑ
        if let customFont = UIFont(name: fontName, size: baseSize) {
            // Dynamic Type ๋ฉ”ํŠธ๋ฆญ์Šค ์ ์šฉ
            return UIFontMetrics(forTextStyle: style).scaledFont(for: customFont)
        }
        
        // ํด๋ฐฑ: ์‹œ์Šคํ…œ ํฐํŠธ
        return UIFont.preferredFont(forTextStyle: style)
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
let titleLabel = UILabel()
titleLabel.font = UIFont.customFont(style: .headline, weight: .bold)
titleLabel.adjustsFontForContentSizeCategory = true

2. ์ด๋ฏธ์ง€์™€ ์•„์ด์ฝ˜์˜ Dynamic Type ๋Œ€์‘

ํ…์ŠคํŠธ๋งŒ ํฌ๊ฒŒ ํ•˜๋ฉด ์•ˆ ๋ผ! ์•„์ด์ฝ˜์ด๋‚˜ ์ด๋ฏธ์ง€๋„ ํ•จ๊ป˜ ์ปค์ ธ์•ผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ์ผ๊ด€์„ฑ ์žˆ์–ด์ ธ ๐ŸŽฏ

class DynamicImageView: UIImageView {
    private let baseSize: CGSize
    private let textStyle: UIFont.TextStyle
    
    init(baseSize: CGSize, textStyle: UIFont.TextStyle = .body) {
        self.baseSize = baseSize
        self.textStyle = textStyle
        super.init(frame: .zero)
        setupDynamicSizing()
    }
    
    required init?(coder: NSCoder) {
        self.baseSize = CGSize(width: 24, height: 24)
        self.textStyle = .body
        super.init(coder: coder)
        setupDynamicSizing()
    }
    
    private func setupDynamicSizing() {
        updateSize()
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(contentSizeCategoryDidChange),
            name: UIContentSizeCategory.didChangeNotification,
            object: nil
        )
    }
    
    @objc private func contentSizeCategoryDidChange() {
        updateSize()
    }
    
    private func updateSize() {
        let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
        let scaledWidth = fontMetrics.scaledValue(for: baseSize.width)
        let scaledHeight = fontMetrics.scaledValue(for: baseSize.height)
        
        NSLayoutConstraint.activate([
            widthAnchor.constraint(equalToConstant: scaledWidth),
            heightAnchor.constraint(equalToConstant: scaledHeight)
        ])
    }
}

๐Ÿงช ํ…Œ์ŠคํŠธ์™€ ๋””๋ฒ„๊น…: ์™„๋ฒฝํ•œ ์ ‘๊ทผ์„ฑ ๋ณด์žฅํ•˜๊ธฐ

๊ฐœ๋ฐœํ•  ๋•Œ ๋ชจ๋“  Dynamic Type ํฌ๊ธฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒŒ ์ง„์งœ ์ค‘์š”ํ•ด! ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ๋ ค์ค„๊ฒŒ ใ…‹ใ…‹

Dynamic Type ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์ถ• Xcode ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ Settings โ†’ Accessibility โ†’ Display & Text Size โ†’ Larger Text ๋‹จ์ถ•ํ‚ค: โŒ˜ + Shift + A Accessibility Inspector ์‹คํ–‰ Environment Overrides Xcode Debug Navigator Dynamic Type Size XS AX5 ์‹ค์‹œ๊ฐ„ ํฌ๊ธฐ ์กฐ์ ˆ ์•ฑ ์žฌ์‹œ์ž‘ ๋ถˆํ•„์š” ์žฅ์  โœ“ ์‹ค์‹œ๊ฐ„ ํ…Œ์ŠคํŠธ โœ“ ๋ชจ๋“  ํฌ๊ธฐ ์ง€์› โœ“ ๋น ๋ฅธ ๊ฒ€์ฆ ์ž๋™ํ™” ํ…Œ์ŠคํŠธ XCUITest ์˜ˆ์‹œ func testDynamicType() { let sizes = [ "UICTContentSizeXS", "UICTContentSizeL", "UICTContentSizeAX5" ] for size in sizes { app.setContentSize(size) validateLayout() } } ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โ–ก ๋ชจ๋“  ํฌ๊ธฐ์—์„œ ํ…์ŠคํŠธ ํ‘œ์‹œ โ–ก ๋ฒ„ํŠผ ํ„ฐ์น˜ ์˜์—ญ ์ถฉ๋ถ„

ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

// ๋ชจ๋“  Content Size Category ํ…Œ์ŠคํŠธ
class DynamicTypeTestHelper {
    static let allContentSizes: [UIContentSizeCategory] = [
        .extraSmall, .small, .medium, .large,
        .extraLarge, .extraExtraLarge, .extraExtraExtraLarge,
        .accessibilityMedium, .accessibilityLarge,
        .accessibilityExtraLarge, .accessibilityExtraExtraLarge,
        .accessibilityExtraExtraExtraLarge
    ]
    
    static func testAllSizes(for view: UIView, completion: @escaping (UIContentSizeCategory) -> Void) {
        for contentSize in allContentSizes {
            // Content Size Category ๋ณ€๊ฒฝ
            let traitCollection = UITraitCollection(preferredContentSizeCategory: contentSize)
            view.overrideUserInterfaceStyle = traitCollection.userInterfaceStyle
            
            // ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ
            view.setNeedsLayout()
            view.layoutIfNeeded()
            
            // ํ…Œ์ŠคํŠธ ์‹คํ–‰
            completion(contentSize)
        }
    }
    
    // ์Šคํฌ๋ฆฐ์ƒท ์ž๋™ ์ƒ์„ฑ
    static func captureScreenshots(for viewController: UIViewController) {
        for (index, contentSize) in allContentSizes.enumerated() {
            let traitCollection = UITraitCollection(preferredContentSizeCategory: contentSize)
            viewController.setOverrideTraitCollection(traitCollection, forChild: viewController)
            
            viewController.view.setNeedsLayout()
            viewController.view.layoutIfNeeded()
            
            // ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ
            let screenshot = viewController.view.asImage()
            saveScreenshot(screenshot, name: "DynamicType_\(index)_\(contentSize.rawValue)")
        }
    }
}

extension UIView {
    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}
๐ŸŽฏ ์‹ค์ „ ํŒ: ์žฌ๋Šฅ๋„ท์—์„œ iOS ๊ฐœ๋ฐœ์ž๋“ค์ด ๊ฐ€์žฅ ๋งŽ์ด ์ฐพ๋Š” ์Šคํ‚ฌ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ”๋กœ ์ ‘๊ทผ์„ฑ ๊ตฌํ˜„์ด์•ผ! ์ด ๊ธฐ์ˆ ์„ ๋งˆ์Šคํ„ฐํ•˜๋ฉด ์ •๋ง ๊ฒฝ์Ÿ๋ ฅ ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด ใ…‹ใ…‹

๐Ÿš€ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์™„๋ฒฝํ•œ Dynamic Type ์•ฑ ๋งŒ๋“ค๊ธฐ

์ด๋ก ๋งŒ ์•Œ๋ฉด ๋ญํ•ด? ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด๋ด์•ผ์ง€! ์™„๋ฒฝํ•œ Dynamic Type ์ง€์› ์•ฑ์„ ๋‹จ๊ณ„๋ณ„๋กœ ๋งŒ๋“ค์–ด๋ณด์ž ๐Ÿ’ช

1๋‹จ๊ณ„: ๊ธฐ๋ณธ ๊ตฌ์กฐ ์„ค์ •

import UIKit

class DynamicTypeViewController: UIViewController {
    
    // MARK: - UI Components
    private lazy var scrollView: UIScrollView = {
        let scrollView = UIScrollView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        return scrollView
    }()
    
    private lazy var contentStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 20
        stackView.translatesAutoresizingMaskIntoConstraints = false
        return stackView
    }()
    
    private lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.text = "Dynamic Type ์˜ˆ์ œ"
        label.font = UIFont.preferredFont(forTextStyle: .largeTitle)
        label.adjustsFontForContentSizeCategory = true
        label.numberOfLines = 0
        return label
    }()
    
    private lazy var bodyLabel: UILabel = {
        let label = UILabel()
        label.text = "์ด ํ…์ŠคํŠธ๋Š” ์‚ฌ์šฉ์ž์˜ ์„ค์ •์— ๋”ฐ๋ผ ํฌ๊ธฐ๊ฐ€ ์ž๋™์œผ๋กœ ์กฐ์ ˆ๋ฉ๋‹ˆ๋‹ค. ์ ‘๊ทผ์„ฑ์„ ๊ณ ๋ คํ•œ ์•ฑ ๊ฐœ๋ฐœ์˜ ์ข‹์€ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค."
        label.font = UIFont.preferredFont(forTextStyle: .body)
        label.adjustsFontForContentSizeCategory = true
        label.numberOfLines = 0
        return label
    }()
    
    private lazy var actionButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("๋™์ž‘ ๋ฒ„ํŠผ", for: .normal)
        button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline)
        button.titleLabel?.adjustsFontForContentSizeCategory = true
        button.backgroundColor = .systemBlue
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 8
        button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupConstraints()
        observeContentSizeChanges()
    }
    
    private func setupUI() {
        view.backgroundColor = .systemBackground
        
        view.addSubview(scrollView)
        scrollView.addSubview(contentStackView)
        
        contentStackView.addArrangedSubview(titleLabel)
        contentStackView.addArrangedSubview(bodyLabel)
        contentStackView.addArrangedSubview(actionButton)
    }
    
    private func setupConstraints() {
        NSLayoutConstraint.activate([
            // ScrollView ์ œ์•ฝ์กฐ๊ฑด
            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 ์ œ์•ฝ์กฐ๊ฑด
            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),
            
            // ๋ฒ„ํŠผ ์ตœ์†Œ ํ„ฐ์น˜ ์˜์—ญ
            actionButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
        ])
    }
    
    private func observeContentSizeChanges() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(contentSizeCategoryDidChange),
            name: UIContentSizeCategory.didChangeNotification,
            object: nil
        )
    }
    
    @objc private func contentSizeCategoryDidChange() {
        // ํฐํŠธ ์—…๋ฐ์ดํŠธ
        titleLabel.font = UIFont.preferredFont(forTextStyle: .largeTitle)
        bodyLabel.font = UIFont.preferredFont(forTextStyle: .body)
        actionButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline)
        
        // ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ
        view.setNeedsLayout()
        view.layoutIfNeeded()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

2๋‹จ๊ณ„: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

// ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์•„์›ƒ์„ ์œ„ํ•œ ํ™•์žฅ
extension DynamicTypeViewController {
    
    private func updateLayoutForContentSize() {
        let currentCategory = UIApplication.shared.preferredContentSizeCategory
        
        // ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ์ผ ๋•Œ ๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝ
        if currentCategory.isAccessibilityCategory {
            contentStackView.spacing = 30
            actionButton.contentEdgeInsets = UIEdgeInsets(top: 16, left: 20, bottom: 16, right: 20)
        } else {
            contentStackView.spacing = 20
            actionButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
        }
    }
    
    // ์ปค์Šคํ…€ ํฐํŠธ ์Šค์ผ€์ผ๋ง
    private func scaledFont(name: String, textStyle: UIFont.TextStyle) -> UIFont {
        guard let customFont = UIFont(name: name, size: UIFont.preferredFont(forTextStyle: textStyle).pointSize) else {
            return UIFont.preferredFont(forTextStyle: textStyle)
        }
        
        return UIFontMetrics(forTextStyle: textStyle).scaledFont(for: customFont)
    }
}

// Content Size Category ํ™•์žฅ
extension UIContentSizeCategory {
    var isAccessibilityCategory: Bool {
        switch self {
        case .accessibilityMedium, .accessibilityLarge, .accessibilityExtraLarge,
             .accessibilityExtraExtraLarge, .accessibilityExtraExtraExtraLarge:
            return true
        default:
            return false
        }
    }
    
    var scaleFactor: CGFloat {
        switch self {
        case .extraSmall: return 0.8
        case .small: return 0.9
        case .medium: return 1.0
        case .large: return 1.0
        case .extraLarge: return 1.1
        case .extraExtraLarge: return 1.2
        case .extraExtraExtraLarge: return 1.3
        case .accessibilityMedium: return 1.4
        case .accessibilityLarge: return 1.5
        case .accessibilityExtraLarge: return 1.6
        case .accessibilityExtraExtraLarge: return 1.7
        case .accessibilityExtraExtraExtraLarge: return 1.8
        default: return 1.0
        }
    }
}

๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

Dynamic Type์„ ๊ตฌํ˜„ํ•  ๋•Œ ์„ฑ๋Šฅ๋„ ์ค‘์š”ํ•ด! ํŠนํžˆ ๋ณต์žกํ•œ ์•ฑ์—์„œ๋Š” ํฐํŠธ ๋ณ€๊ฒฝ ์‹œ ์„ฑ๋Šฅ ์ด์Šˆ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๊ฑฐ๋“  ๐Ÿš€

Dynamic Type ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต โš ๏ธ ์„ฑ๋Šฅ ๋ฌธ์ œ์  ํฐํŠธ ๋ณ€๊ฒฝ ์‹œ ์ „์ฒด ๋ทฐ ์žฌ๊ณ„์‚ฐ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋Š๊น€ ํ˜„์ƒ ๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ์ฆ๊ฐ€ CPU ์‚ฌ์šฉ๋ฅ  ์ตœ์ ํ™” ์ „ ์ตœ์ ํ™” ํ›„ โœ… ์ตœ์ ํ™” ๋ฐฉ๋ฒ• ํฐํŠธ ์บ์‹ฑ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ ์ฒ˜๋ฆฌ ์ง€์—ฐ ๋กœ๋”ฉ ๊ตฌํ˜„ ๋ฉ”๋ชจ๋ฆฌ ํ’€๋ง ํ™œ์šฉ ํฐํŠธ ์บ์‹œ ์˜ˆ์‹œ FontManager.shared .cachedFont(style: .body) ๐Ÿ“ˆ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง Instruments Time Profiler CPU ์‚ฌ์šฉ๋Ÿ‰ ์ธก์ • Memory Graph Debugger ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฒ€์‚ฌ Core Animation ๋ Œ๋”๋ง ์„ฑ๋Šฅ ๋ถ„์„ ๋ชฉํ‘œ ์„ฑ๋Šฅ ์ง€ํ‘œ < 16ms ๋ Œ๋”๋ง ์‹œ๊ฐ„

ํฐํŠธ ์บ์‹ฑ ์‹œ์Šคํ…œ ๊ตฌํ˜„

class FontManager {
    static let shared = FontManager()
    private init() {}
    
    private var fontCache: [String: UIFont] = [:]
    private let cacheQueue = DispatchQueue(label: "font.cache.queue", attributes: .concurrent)
    
    func cachedFont(forTextStyle textStyle: UIFont.TextStyle, weight: UIFont.Weight = .regular) -> UIFont {
        let cacheKey = "\(textStyle.rawValue)_\(weight.rawValue)_\(UIApplication.shared.preferredContentSizeCategory.rawValue)"
        
        return cacheQueue.sync {
            if let cachedFont = fontCache[cacheKey] {
                return cachedFont
            }
            
            let font = UIFont.systemFont(ofSize: UIFont.preferredFont(forTextStyle: textStyle).pointSize, weight: weight)
            let scaledFont = UIFontMetrics(forTextStyle: textStyle).scaledFont(for: font)
            
            cacheQueue.async(flags: .barrier) {
                self.fontCache[cacheKey] = scaledFont
            }
            
            return scaledFont
        }
    }
    
    func clearCache() {
        cacheQueue.async(flags: .barrier) {
            self.fontCache.removeAll()
        }
    }
    
    // ๋ฉ”๋ชจ๋ฆฌ ๊ฒฝ๊ณ  ์‹œ ์บ์‹œ ์ •๋ฆฌ
    @objc private func handleMemoryWarning() {
        clearCache()
    }
}

// ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ ๋งค๋‹ˆ์ €
class DynamicTypeUpdateManager {
    static let shared = DynamicTypeUpdateManager()
    private init() {}
    
    private var pendingUpdates: Set<weakviewreference> = []
    private var updateTimer: Timer?
    
    func scheduleUpdate(for view: UIView) {
        pendingUpdates.insert(WeakViewReference(view: view))
        
        // 50ms ํ›„์— ๋ฐฐ์น˜ ์—…๋ฐ์ดํŠธ ์‹คํ–‰
        updateTimer?.invalidate()
        updateTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in
            self.performBatchUpdate()
        }
    }
    
    private func performBatchUpdate() {
        DispatchQueue.main.async {
            for viewRef in self.pendingUpdates {
                viewRef.view?.setNeedsLayout()
            }
            self.pendingUpdates.removeAll()
        }
    }
}

class WeakViewReference: Hashable {
    weak var view: UIView?
    private let identifier = UUID()
    
    init(view: UIView) {
        self.view = view
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }
    
    static func == (lhs: WeakViewReference, rhs: WeakViewReference) -> Bool {
        return lhs.identifier == rhs.identifier
    }
}
</weakviewreference>

๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๐ŸŽฏ Dynamic Type ๊ตฌํ˜„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ โœ… ๋ชจ๋“  ํ…์ŠคํŠธ์— adjustsFontForContentSizeCategory = true ์„ค์ •
โœ… ์ตœ์†Œ 44pt ํ„ฐ์น˜ ์˜์—ญ ๋ณด์žฅ
โœ… ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ์—์„œ ๋ ˆ์ด์•„์›ƒ ํ…Œ์ŠคํŠธ
โœ… ์ปค์Šคํ…€ ํฐํŠธ์—๋„ Dynamic Type ์ ์šฉ
โœ… ์ด๋ฏธ์ง€์™€ ์•„์ด์ฝ˜ ํฌ๊ธฐ๋„ ํ•จ๊ป˜ ์กฐ์ ˆ
โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ
โœ… ์ž๋™ํ™” ํ…Œ์ŠคํŠธ ๊ตฌํ˜„
โœ… VoiceOver์™€์˜ ํ˜ธํ™˜์„ฑ ํ™•์ธ

๐ŸŒŸ ๋ฏธ๋ž˜์˜ Dynamic Type: iOS 17+ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๋“ค

์• ํ”Œ์ด ๊ณ„์†ํ•ด์„œ ์ ‘๊ทผ์„ฑ ๊ธฐ๋Šฅ์„ ๋ฐœ์ „์‹œํ‚ค๊ณ  ์žˆ์–ด! iOS 17๋ถ€ํ„ฐ ์ถ”๊ฐ€๋œ ์ƒˆ๋กœ์šด Dynamic Type ๊ธฐ๋Šฅ๋“ค์„ ์‚ดํŽด๋ณด์ž ใ…‹ใ…‹

1. ์•ฑ๋ณ„ ๊ธ€์ž ํฌ๊ธฐ ์„ค์ •

์ด์ œ ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ๋งˆ๋‹ค ๋‹ค๋ฅธ ๊ธ€์ž ํฌ๊ธฐ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด! ์ด๊ฒŒ ์ง„์งœ ํ˜์‹ ์ ์ด์•ผ ๐Ÿ˜

// iOS 17+ ์•ฑ๋ณ„ Dynamic Type ๊ฐ์ง€
@available(iOS 17.0, *)
extension UIViewController {
    func observeAppSpecificContentSizeChanges() {
        NotificationCenter.default.addObserver(
            forName: UIApplication.didChangeContentSizeCategoryNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            // ์•ฑ๋ณ„ ์„ค์ •์ธ์ง€ ํ™•์ธ
            if let isAppSpecific = notification.userInfo?["isAppSpecific"] as? Bool,
               isAppSpecific {
                self?.handleAppSpecificContentSizeChange()
            }
        }
    }
    
    private func handleAppSpecificContentSizeChange() {
        // ์•ฑ๋ณ„ ์„ค์ •์— ํŠนํ™”๋œ ์ฒ˜๋ฆฌ
        print("์•ฑ๋ณ„ Dynamic Type ์„ค์ •์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!")
    }
}

2. ํ–ฅ์ƒ๋œ ํฐํŠธ ๋ฉ”ํŠธ๋ฆญ์Šค

// iOS 17+ ์ƒˆ๋กœ์šด ํฐํŠธ ๋ฉ”ํŠธ๋ฆญ์Šค API
@available(iOS 17.0, *)
class AdvancedFontManager {
    static func adaptiveFont(
        baseFont: UIFont,
        textStyle: UIFont.TextStyle,
        maximumPointSize: CGFloat? = nil,
        minimumScaleFactor: CGFloat = 0.5
    ) -> UIFont {
        let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
        
        var scaledFont = fontMetrics.scaledFont(for: baseFont)
        
        // ์ตœ๋Œ€ ํฌ๊ธฐ ์ œํ•œ
        if let maxSize = maximumPointSize,
           scaledFont.pointSize > maxSize {
            scaledFont = baseFont.withSize(maxSize)
        }
        
        // ์ตœ์†Œ ์Šค์ผ€์ผ ํŒฉํ„ฐ ์ ์šฉ
        let minimumSize = baseFont.pointSize * minimumScaleFactor
        if scaledFont.pointSize < minimumSize {
            scaledFont = baseFont.withSize(minimumSize)
        }
        
        return scaledFont
    }
}

3. ์‹ค์‹œ๊ฐ„ ํฐํŠธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ

์„ค์ • ์•ฑ์—์„œ ๊ธ€์ž ํฌ๊ธฐ๋ฅผ ์กฐ์ ˆํ•  ๋•Œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์•ฑ์˜ ๋ณ€ํ™”๋ฅผ ๋ฏธ๋ฆฌ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋์–ด! ์ด๊ฑด ์ •๋ง ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œ์ผฐ์ง€ ๐ŸŽ‰

๐Ÿ’ก ์‹ค๋ฌด์—์„œ ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ์ฑ…

์‹ค์ œ ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์ด๋ก ๊ณผ ๋‹ค๋ฅธ ์ƒํ™ฉ๋“ค์ด ๋งŽ์ด ์ƒ๊ฒจ ใ…‹ใ…‹ ๋‚ด๊ฐ€ ๊ฒฝํ—˜ํ•œ ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ์ฑ…์„ ๊ณต์œ ํ•ด๋ณผ๊ฒŒ!

๋ฌธ์ œ 1: ํ…Œ์ด๋ธ”๋ทฐ ์…€ ๋†’์ด ์ž๋™ ๊ณ„์‚ฐ ์ด์Šˆ

// ๋ฌธ์ œ: Dynamic Type ๋ณ€๊ฒฝ ์‹œ ํ…Œ์ด๋ธ”๋ทฐ ์…€ ๋†’์ด๊ฐ€ ์ œ๋Œ€๋กœ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Œ

class DynamicTableViewCell: UITableViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var bodyLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        setupDynamicType()
    }
    
    private func setupDynamicType() {
        titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
        titleLabel.adjustsFontForContentSizeCategory = true
        
        bodyLabel.font = UIFont.preferredFont(forTextStyle: .body)
        bodyLabel.adjustsFontForContentSizeCategory = true
        
        // ํ•ต์‹ฌ: ์…€ ๋†’์ด ์ž๋™ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ์ œ์•ฝ์กฐ๊ฑด ์„ค์ •
        titleLabel.setContentHuggingPriority(.required, for: .vertical)
        titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        
        bodyLabel.setContentHuggingPriority(.required, for: .vertical)
        bodyLabel.setContentCompressionResistancePriority(.required, for: .vertical)
    }
}

// ํ…Œ์ด๋ธ”๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ
class DynamicTableViewController: UITableViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ์ž๋™ ๋†’์ด ๊ณ„์‚ฐ ์„ค์ •
        tableView.estimatedRowHeight = 100
        tableView.rowHeight = UITableView.automaticDimension
        
        // Dynamic Type ๋ณ€๊ฒฝ ๊ฐ์ง€
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(contentSizeCategoryDidChange),
            name: UIContentSizeCategory.didChangeNotification,
            object: nil
        )
    }
    
    @objc private func contentSizeCategoryDidChange() {
        // ๋ชจ๋“  ์…€์˜ ๋†’์ด ์žฌ๊ณ„์‚ฐ
        tableView.beginUpdates()
        tableView.endUpdates()
        
        // ๋˜๋Š” ๋” ๊ฐ•๋ ฅํ•œ ๋ฐฉ๋ฒ•
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }
}

๋ฌธ์ œ 2: ์ปฌ๋ ‰์…˜๋ทฐ ๋ ˆ์ด์•„์›ƒ ๊นจ์ง

// ๋ฌธ์ œ: ์ปฌ๋ ‰์…˜๋ทฐ์—์„œ Dynamic Type ๋ณ€๊ฒฝ ์‹œ ์•„์ดํ…œ ํฌ๊ธฐ๊ฐ€ ๊ณ ์ •๋˜์–ด ์žˆ์Œ

class DynamicCollectionViewFlowLayout: UICollectionViewFlowLayout {
    
    override func prepare() {
        super.prepare()
        updateItemSizeForCurrentContentSize()
    }
    
    private func updateItemSizeForCurrentContentSize() {
        guard let collectionView = collectionView else { return }
        
        let contentSize = UIApplication.shared.preferredContentSizeCategory
        let baseWidth = (collectionView.bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing) / 2
        
        // Dynamic Type์— ๋”ฐ๋ฅธ ๋†’์ด ๊ณ„์‚ฐ
        let scaleFactor = contentSize.scaleFactor
        let baseHeight: CGFloat = 120
        let dynamicHeight = baseHeight * scaleFactor
        
        itemSize = CGSize(width: baseWidth, height: dynamicHeight)
    }
    
    // Content Size Category ๋ณ€๊ฒฝ ์‹œ ๋ ˆ์ด์•„์›ƒ ๋ฌดํšจํ™”
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}

class DynamicCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        titleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
        titleLabel.adjustsFontForContentSizeCategory = true
        titleLabel.numberOfLines = 0
        
        // ์…€ ๋‚ด์šฉ์— ๋งž๋Š” ํฌ๊ธฐ ์กฐ์ ˆ
        titleLabel.setContentHuggingPriority(.required, for: .vertical)
    }
    
    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        let attributes = super.preferredLayoutAttributesFitting(layoutAttributes)
        
        // ๋‚ด์šฉ์— ๋งž๋Š” ๋†’์ด ๊ณ„์‚ฐ
        let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
        let fittingSize = contentView.systemLayoutSizeFitting(
            targetSize,
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel
        )
        
        attributes.frame.size.height = fittingSize.height
        return attributes
    }
}

๋ฌธ์ œ 3: ์ปค์Šคํ…€ ๋ทฐ์—์„œ Dynamic Type ๋ฏธ์ ์šฉ

// ๋ฌธ์ œ: ์ปค์Šคํ…€ ๋ทฐ์—์„œ ๊ทธ๋ฆฐ ํ…์ŠคํŠธ๊ฐ€ Dynamic Type์— ๋ฐ˜์‘ํ•˜์ง€ ์•Š์Œ

class CustomDrawingView: UIView {
    private var dynamicFont: UIFont {
        return UIFont.preferredFont(forTextStyle: .body)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupDynamicType()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupDynamicType()
    }
    
    private func setupDynamicType() {
        // Dynamic Type ๋ณ€๊ฒฝ ๊ฐ์ง€
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(contentSizeCategoryDidChange),
            name: UIContentSizeCategory.didChangeNotification,
            object: nil
        )
    }
    
    @objc private func contentSizeCategoryDidChange() {
        // ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ
        setNeedsDisplay()
        invalidateIntrinsicContentSize()
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        let text = "์ปค์Šคํ…€ ๊ทธ๋ฆฌ๊ธฐ ํ…์ŠคํŠธ"
        let attributes: [NSAttributedString.Key: Any] = [
            .font: dynamicFont,
            .foregroundColor: UIColor.label
        ]
        
        let attributedString = NSAttributedString(string: text, attributes: attributes)
        let textSize = attributedString.size()
        
        let textRect = CGRect(
            x: (bounds.width - textSize.width) / 2,
            y: (bounds.height - textSize.height) / 2,
            width: textSize.width,
            height: textSize.height
        )
        
        attributedString.draw(in: textRect)
    }
    
    override var intrinsicContentSize: CGSize {
        let text = "์ปค์Šคํ…€ ๊ทธ๋ฆฌ๊ธฐ ํ…์ŠคํŠธ"
        let attributes: [NSAttributedString.Key: Any] = [.font: dynamicFont]
        let size = text.size(withAttributes: attributes)
        return CGSize(width: size.width + 20, height: size.height + 20)
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌ: Dynamic Type ๋งˆ์Šคํ„ฐ๊ฐ€ ๋˜๋Š” ๊ธธ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ๋‹ค๋ฉด ์ •๋ง ๋Œ€๋‹จํ•ด ใ…‹ใ…‹ Dynamic Type์— ๋Œ€ํ•ด ์ •๋ง ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ์„ ๊ฑฐ์•ผ. ์ด์ œ ์ •๋ฆฌํ•ด๋ณด์ž๋ฉด:

ํ•ต์‹ฌ ํฌ์ธํŠธ ์š”์•ฝ

๐ŸŽฏ ๋ฐ˜๋“œ์‹œ ๊ธฐ์–ตํ•ด์•ผ ํ•  ๊ฒƒ๋“ค 1. adjustsFontForContentSizeCategory = true๋Š” ํ•„์ˆ˜!
2. Auto Layout์œผ๋กœ ์œ ์—ฐํ•œ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ
3. ์ตœ์†Œ 44pt ํ„ฐ์น˜ ์˜์—ญ ๋ณด์žฅ
4. ์ ‘๊ทผ์„ฑ ํฌ๊ธฐ์—์„œ๋„ ์™„๋ฒฝํ•˜๊ฒŒ ๋™์ž‘ํ•ด์•ผ ํ•จ
5. ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ํ†ตํ•œ ๋ถ€๋“œ๋Ÿฌ์šด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜
6. ์ง€์†์ ์ธ ํ…Œ์ŠคํŠธ๋กœ ํ’ˆ์งˆ ๋ณด์žฅ

Dynamic Type ๊ตฌํ˜„์€ ๋‹จ์ˆœํžˆ ๊ธ€์ž ํฌ๊ธฐ๋งŒ ๋ฐ”๊พธ๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ. ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์•ฑ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ๊ฑฐ๋“ ! ํŠนํžˆ ๊ณ ๋ นํ™” ์‚ฌํšŒ๋กœ ์ ‘์–ด๋“ค๋ฉด์„œ ์ด๋Ÿฐ ์ ‘๊ทผ์„ฑ ๊ธฐ๋Šฅ์˜ ์ค‘์š”์„ฑ์€ ๋”์šฑ ์ปค์ง€๊ณ  ์žˆ์–ด.

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ ‘๊ทผ์„ฑ ์ „๋ฌธ ๊ฐœ๋ฐœ์ž์— ๋Œ€ํ•œ ์ˆ˜์š”๊ฐ€ ๊ณ„์† ๋Š˜์–ด๋‚˜๊ณ  ์žˆ์œผ๋‹ˆ๊นŒ, ์ด ๊ธฐ์ˆ ์„ ์ž˜ ์ตํ˜€๋‘๋ฉด ์ •๋ง ์ข‹์€ ๊ธฐํšŒ๋“ค์ด ๋งŽ์„ ๊ฑฐ์•ผ! ๐Ÿ’ช

๋‹ค์Œ ๋‹จ๊ณ„ ํ•™์Šต ๋กœ๋“œ๋งต

Dynamic Type์„ ๋งˆ์Šคํ„ฐํ–ˆ๋‹ค๋ฉด ์ด์ œ ์ด๋Ÿฐ ๊ฒƒ๋“ค๋„ ๊ณต๋ถ€ํ•ด๋ณด๋ฉด ์ข‹์„ ๊ฑฐ์•ผ:

  • ๐Ÿ”Š VoiceOver ์ง€์›: ์‹œ๊ฐ ์žฅ์• ์ธ์„ ์œ„ํ•œ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ๊ธฐ๋Šฅ
  • ๐ŸŽจ ๋‹คํฌ ๋ชจ๋“œ ๋Œ€์‘: ์‹œ๊ฐ์  ํŽธ์˜์„ฑ ํ–ฅ์ƒ
  • ๐ŸŒ ๋‹ค๊ตญ์–ด ์ง€์›: ๊ธ€๋กœ๋ฒŒ ์•ฑ์„ ์œ„ํ•œ ํ•„์ˆ˜ ๊ธฐ๋Šฅ
  • โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์ ‘๊ทผ์„ฑ๊ณผ ์„ฑ๋Šฅ์˜ ๊ท ํ˜•
  • ๐Ÿงช ์ ‘๊ทผ์„ฑ ํ…Œ์ŠคํŒ…: ์ž๋™ํ™”๋œ ์ ‘๊ทผ์„ฑ ๊ฒ€์ฆ

์ง„์งœ ๋งˆ์ง€๋ง‰์œผ๋กœ, Dynamic Type์€ ๊ธฐ์ˆ ์  ๊ตฌํ˜„๋„ ์ค‘์š”ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž์— ๋Œ€ํ•œ ๋ฐฐ๋ ค์™€ ์ดํ•ด๊ฐ€ ๋” ์ค‘์š”ํ•ด. ๋‹ค์–‘ํ•œ ์‚ฌ์šฉ์ž๋“ค์ด ์–ด๋–ค ์–ด๋ ค์›€์„ ๊ฒช๋Š”์ง€ ์ดํ•ดํ•˜๊ณ , ๊ทธ๋“ค์˜ ์ž…์žฅ์—์„œ ์ƒ๊ฐํ•ด๋ณด๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๊ธธ ๋ฐ”๋ผ! ๐ŸŒŸ

์ด์ œ ์—ฌ๋Ÿฌ๋ถ„๋„ ๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ๋ฉ‹์ง„ ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ํ™”์ดํŒ…! ๐Ÿš€

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

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

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

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