Ver2.5 ๐ iOS ์ฑ ์ ๊ทผ์ฑ์ ํ๋ช : ํฐ ๊ธ์จ ์ง์์ผ๋ก ๋ชจ๋ ์ฌ์ฉ์๋ฅผ ์ํ ์ฑ ๊ฐ๋ฐํ๊ธฐ

๐ iOS ์ฑ ์ ๊ทผ์ฑ์ ํ๋ช : ํฐ ๊ธ์จ ์ง์์ผ๋ก ๋ชจ๋ ์ฌ์ฉ์๋ฅผ ์ํ ์ฑ ๊ฐ๋ฐํ๊ธฐ
์์ฆ ์ฑ ๊ฐ๋ฐํ๋ฉด์ ๊ฐ์ฅ ์ค์ํ ๊ฒ ๋ญ์ง ์์? ๋ฐ๋ก ์ ๊ทผ์ฑ(Accessibility)์ด์ผ! ํนํ iOS์์ ํฐ ๊ธ์จ ์ง์์ ์ ํ์ด ์๋๋ผ ํ์๊ฐ ๋์๋ค๊ตฌ ใ ใ ์๊ฐ์ ์ ๊ทผ์ฑ์ ๊ณ ๋ คํ์ง ์์ ์ฑ์ ์ด์ ์ฑ์คํ ์ด์์๋ ๋ฆฌ์ ๋นํ ์ ์์ด์ ๊ฐ๋ฐ์๋ค์ด ์ง์ง ์ ๊ฒฝ ๋ง์ด ์ฐ๊ณ ์์ด ๐ช
์ ํ์ด iOS์์ ์ ๊ณตํ๋ Dynamic Type๊ณผ ํฐ ๊ธ์จ ์ง์ ๊ธฐ๋ฅ๋ค์ ๋จ์ํ ๊ธ์๋ง ํฌ๊ฒ ๋ง๋๋ ๊ฒ ์๋๋ผ, ์ง์ง ์ฌ์ฉ์ ๊ฒฝํ์ ํ์ ์ ์ผ๋ก ๋ฐ๊พธ๋ ๊ธฐ์ ์ด๊ฑฐ๋ ? ์ค๋์ ์ด ๋ชจ๋ ๊ฑธ ํํค์ณ๋ณด์๊ตฌ!
๐ฑ iOS ์ ๊ทผ์ฑ์ ์งํ: Dynamic Type์ด ๋ญ๋ฐ?
Dynamic Type์ iOS 7๋ถํฐ ๋์ ๋ ๊ธฐ๋ฅ์ธ๋ฐ, ์ฌ์ฉ์๊ฐ ์์คํ ์ค์ ์์ ๊ธ์ ํฌ๊ธฐ๋ฅผ ์กฐ์ ํ๋ฉด ์ฑ์ ๋ชจ๋ ํ ์คํธ๊ฐ ์๋์ผ๋ก ํฌ๊ธฐ๊ฐ ๋ฐ๋๋ ๋ง๋ฒ ๊ฐ์ ๊ธฐ๋ฅ์ด์ผ โจ
๊ทผ๋ฐ ์ด๊ฒ ์ ์ค์ํ๋๋ฉด, ์ ์ธ๊ณ์ ์ผ๋ก ์๊ฐ ์ฅ์ ๋ ์ ์๋ ฅ์ ๊ฐ์ง ์ฌ์ฉ์๊ฐ 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๋จ๊ณ๋ก ๋๋ ๋จ์ด:
์ ๊ทผ์ฑ ํฌ๊ธฐ(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๊ฐ ์์ ํ ๋ง๊ฐ์ง ์ ์์ด ๐ฑ
์ค์ 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 ํฌ๊ธฐ๋ฅผ ํ ์คํธํ๋ ๊ฒ ์ง์ง ์ค์ํด! ์๋ฎฌ๋ ์ดํฐ์์ ์ฝ๊ฒ ํ ์คํธํ ์ ์๋ ๋ฐฉ๋ฒ๋ค์ ์๋ ค์ค๊ฒ ใ ใ
ํ๋ก๊ทธ๋๋งคํฑ ํ ์คํธ ์ฝ๋
// ๋ชจ๋ 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)
}
}
}
๐ ์ค์ ํ๋ก์ ํธ: ์๋ฒฝํ 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์ ๊ตฌํํ ๋ ์ฑ๋ฅ๋ ์ค์ํด! ํนํ ๋ณต์กํ ์ฑ์์๋ ํฐํธ ๋ณ๊ฒฝ ์ ์ฑ๋ฅ ์ด์๊ฐ ์๊ธธ ์ ์๊ฑฐ๋ ๐
ํฐํธ ์บ์ฑ ์์คํ ๊ตฌํ
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>
๋ฒ ์คํธ ํ๋ํฐ์ค ์ฒดํฌ๋ฆฌ์คํธ
โ ์ต์ 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์ ๋ํด ์ ๋ง ๋ง์ ๊ฑธ ๋ฐฐ์ ์ ๊ฑฐ์ผ. ์ด์ ์ ๋ฆฌํด๋ณด์๋ฉด:
ํต์ฌ ํฌ์ธํธ ์์ฝ
2. Auto Layout์ผ๋ก ์ ์ฐํ ๋ ์ด์์ ๊ตฌ์ฑ
3. ์ต์ 44pt ํฐ์น ์์ญ ๋ณด์ฅ
4. ์ ๊ทผ์ฑ ํฌ๊ธฐ์์๋ ์๋ฒฝํ๊ฒ ๋์ํด์ผ ํจ
5. ์ฑ๋ฅ ์ต์ ํ๋ฅผ ํตํ ๋ถ๋๋ฌ์ด ์ฌ์ฉ์ ๊ฒฝํ
6. ์ง์์ ์ธ ํ ์คํธ๋ก ํ์ง ๋ณด์ฅ
Dynamic Type ๊ตฌํ์ ๋จ์ํ ๊ธ์ ํฌ๊ธฐ๋ง ๋ฐ๊พธ๋ ๊ฒ ์๋์ผ. ๋ชจ๋ ์ฌ์ฉ์๊ฐ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์๋ ์ฑ์ ๋ง๋๋ ๊ฒ์ด ๋ชฉํ๊ฑฐ๋ ! ํนํ ๊ณ ๋ นํ ์ฌํ๋ก ์ ์ด๋ค๋ฉด์ ์ด๋ฐ ์ ๊ทผ์ฑ ๊ธฐ๋ฅ์ ์ค์์ฑ์ ๋์ฑ ์ปค์ง๊ณ ์์ด.
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ ๊ทผ์ฑ ์ ๋ฌธ ๊ฐ๋ฐ์์ ๋ํ ์์๊ฐ ๊ณ์ ๋์ด๋๊ณ ์์ผ๋๊น, ์ด ๊ธฐ์ ์ ์ ์ตํ๋๋ฉด ์ ๋ง ์ข์ ๊ธฐํ๋ค์ด ๋ง์ ๊ฑฐ์ผ! ๐ช
๋ค์ ๋จ๊ณ ํ์ต ๋ก๋๋งต
Dynamic Type์ ๋ง์คํฐํ๋ค๋ฉด ์ด์ ์ด๋ฐ ๊ฒ๋ค๋ ๊ณต๋ถํด๋ณด๋ฉด ์ข์ ๊ฑฐ์ผ:
- ๐ VoiceOver ์ง์: ์๊ฐ ์ฅ์ ์ธ์ ์ํ ์คํฌ๋ฆฐ ๋ฆฌ๋ ๊ธฐ๋ฅ
- ๐จ ๋คํฌ ๋ชจ๋ ๋์: ์๊ฐ์ ํธ์์ฑ ํฅ์
- ๐ ๋ค๊ตญ์ด ์ง์: ๊ธ๋ก๋ฒ ์ฑ์ ์ํ ํ์ ๊ธฐ๋ฅ
- โก ์ฑ๋ฅ ์ต์ ํ: ์ ๊ทผ์ฑ๊ณผ ์ฑ๋ฅ์ ๊ท ํ
- ๐งช ์ ๊ทผ์ฑ ํ ์คํ : ์๋ํ๋ ์ ๊ทผ์ฑ ๊ฒ์ฆ
์ง์ง ๋ง์ง๋ง์ผ๋ก, Dynamic Type์ ๊ธฐ์ ์ ๊ตฌํ๋ ์ค์ํ์ง๋ง ์ฌ์ฉ์์ ๋ํ ๋ฐฐ๋ ค์ ์ดํด๊ฐ ๋ ์ค์ํด. ๋ค์ํ ์ฌ์ฉ์๋ค์ด ์ด๋ค ์ด๋ ค์์ ๊ฒช๋์ง ์ดํดํ๊ณ , ๊ทธ๋ค์ ์ ์ฅ์์ ์๊ฐํด๋ณด๋ ๊ฐ๋ฐ์๊ฐ ๋๊ธธ ๋ฐ๋ผ! ๐
์ด์ ์ฌ๋ฌ๋ถ๋ ๋ชจ๋ ์ฌ์ฉ์๋ฅผ ์ํ ๋ฉ์ง ์ฑ์ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ. ํ์ดํ ! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.